lit-html
Advanced tools
Comparing version 0.13.0 to 0.14.0
@@ -13,8 +13,24 @@ # Change Log | ||
## Unreleased | ||
<!-- ## Unreleased --> | ||
<!-- ### Changed --> | ||
<!-- ### Added --> | ||
<!-- ### Changed --> | ||
<!-- ### Removed --> | ||
<!-- ### Fixed --> | ||
## Unreleased | ||
### Changed | ||
* `until()` can now take any number of sync or async arguments. ([#555](https://github.com/Polymer/lit-html/pull/555)) | ||
* [Breaking] `guard()` supports multiple dependencies. If the first argument to `guard()` is an array, the array items are checked for equality to previous values. ([#666](https://github.com/Polymer/lit-html/pull/666)) | ||
* [Breaking] Renamed `classMap.js` and `styleMap.js` files to kebab-case. ([#644](https://github.com/Polymer/lit-html/pull/644)) | ||
### Added | ||
* Added `cache()` directive. ([#646](https://github.com/Polymer/lit-html/pull/646)) | ||
* Removed Promise as a supposed node-position value type. ([#555](https://github.com/Polymer/lit-html/pull/555)) | ||
* Added a minimal `<template>` polyfill. | ||
### Removed | ||
* [Breaking] Removed the `when()` directive. Users may achieve similar behavior by wrapping a ternary with the `cache()` directive. | ||
### Fixed | ||
* Bound attribute names are rewritten to avoid IE/Edge removing SVG and style attributes. ([#640](https://github.com/Polymer/lit-html/pull/640)) | ||
* Ensure shady-render prepares styling for a scope before attaching child elements. ([#664](https://github.com/Polymer/lit-html/pull/664)) | ||
* Handle CSS Custom Variables in the styleMap directive. [#642](https://github.com/Polymer/lit-html/pull/642)) | ||
## [0.13.0] - 2018-11-08 | ||
@@ -38,4 +54,2 @@ ### Changed | ||
<!-- ### Removed --> | ||
<!-- ### Fixed --> | ||
@@ -42,0 +56,0 @@ ## [0.11.4] - 2018-09-17 |
@@ -57,2 +57,7 @@ /** | ||
let v = value_1_1.value; | ||
// Check to make sure that value is the still the current value of | ||
// the part, and if not bail because a new value owns this part | ||
if (part.value !== value) { | ||
break; | ||
} | ||
// When we get the first value, clear the part. This lets the | ||
@@ -63,7 +68,2 @@ // previous value display until we can replace it. | ||
} | ||
// Check to make sure that value is the still the current value of | ||
// the part, and if not bail because a new value owns this part | ||
if (part.value !== value) { | ||
break; | ||
} | ||
// As a convenience, because functional-programming-style | ||
@@ -70,0 +70,0 @@ // transforms of iterables and async iterables requires a library, |
@@ -58,2 +58,7 @@ /** | ||
let v = value_1_1.value; | ||
// Check to make sure that value is the still the current value of | ||
// the part, and if not bail because a new value owns this part | ||
if (part.value !== value) { | ||
break; | ||
} | ||
// When we get the first value, clear the part. This let's the | ||
@@ -65,7 +70,2 @@ // previous value display until we can replace it. | ||
} | ||
// Check to make sure that value is the still the current value of | ||
// the part, and if not bail because a new value owns this part | ||
if (part.value !== value) { | ||
break; | ||
} | ||
// As a convenience, because functional-programming-style | ||
@@ -72,0 +72,0 @@ // transforms of iterables and async iterables requires a library, |
@@ -16,21 +16,35 @@ /** | ||
/** | ||
* Creates a guard directive. Prevents any re-render until the identity of the | ||
* expression changes, for example when a primitive changes value or when an | ||
* object reference changes. | ||
* Prevents re-render of a template function until a single value or an array of | ||
* values changes. | ||
* | ||
* This useful with immutable data patterns, by preventing expensive work until | ||
* data updates. Example: | ||
* Example: | ||
* | ||
* ```js | ||
* html` | ||
* <div> | ||
* ${guard(items, () => items.map(item => html`${item}`))} | ||
* ${guard([user.id, company.id], () => html`...`)} | ||
* </div> | ||
* ` | ||
* ``` | ||
* | ||
* In this case, the template only renders if either `user.id` or `company.id` | ||
* changes. | ||
* | ||
* guard() is useful with immutable data patterns, by preventing expensive work | ||
* until data updates. | ||
* | ||
* Example: | ||
* | ||
* ```js | ||
* html` | ||
* <div> | ||
* ${guard([immutableItems], () => immutableItems.map(i => html`${i}`))} | ||
* </div> | ||
* ``` | ||
* | ||
* In this case, items are mapped over only when the array reference changes. | ||
* | ||
* @param expression the expression to check before re-rendering | ||
* @param valueFn function which returns the render value | ||
* @param value the value to check before re-rendering | ||
* @param f the template function | ||
*/ | ||
export declare const guard: (expression: any, valueFn: () => any) => (part: Part) => void; | ||
export declare const guard: (value: unknown, f: () => unknown) => (part: Part) => void; | ||
//# sourceMappingURL=guard.d.ts.map |
@@ -15,30 +15,56 @@ /** | ||
import { directive } from '../lit-html.js'; | ||
const previousExpressions = new WeakMap(); | ||
const previousValues = new WeakMap(); | ||
/** | ||
* Creates a guard directive. Prevents any re-render until the identity of the | ||
* expression changes, for example when a primitive changes value or when an | ||
* object reference changes. | ||
* Prevents re-render of a template function until a single value or an array of | ||
* values changes. | ||
* | ||
* This useful with immutable data patterns, by preventing expensive work until | ||
* data updates. Example: | ||
* Example: | ||
* | ||
* ```js | ||
* html` | ||
* <div> | ||
* ${guard(items, () => items.map(item => html`${item}`))} | ||
* ${guard([user.id, company.id], () => html`...`)} | ||
* </div> | ||
* ` | ||
* ``` | ||
* | ||
* In this case, the template only renders if either `user.id` or `company.id` | ||
* changes. | ||
* | ||
* guard() is useful with immutable data patterns, by preventing expensive work | ||
* until data updates. | ||
* | ||
* Example: | ||
* | ||
* ```js | ||
* html` | ||
* <div> | ||
* ${guard([immutableItems], () => immutableItems.map(i => html`${i}`))} | ||
* </div> | ||
* ``` | ||
* | ||
* In this case, items are mapped over only when the array reference changes. | ||
* | ||
* @param expression the expression to check before re-rendering | ||
* @param valueFn function which returns the render value | ||
* @param value the value to check before re-rendering | ||
* @param f the template function | ||
*/ | ||
export const guard = directive((expression, valueFn) => (part) => { | ||
// Dirty check previous expression | ||
if (previousExpressions.get(part) === expression) { | ||
export const guard = directive((value, f) => (part) => { | ||
const previousValue = previousValues.get(part); | ||
if (Array.isArray(value)) { | ||
// Dirty-check arrays by item | ||
if (Array.isArray(previousValue) && | ||
previousValue.length === value.length && | ||
value.every((v, i) => v === previousValue[i])) { | ||
return; | ||
} | ||
} | ||
else if (previousValue === value && | ||
(value !== undefined || previousValues.has(part))) { | ||
// Dirty-check non-arrays by identity | ||
return; | ||
} | ||
part.setValue(valueFn()); | ||
previousExpressions.set(part, expression); | ||
part.setValue(f()); | ||
// Copy the value if it's an array so that if it's mutated we don't forget | ||
// what the previous values were. | ||
previousValues.set(part, Array.isArray(value) ? Array.from(value) : value); | ||
}); | ||
//# sourceMappingURL=guard.js.map |
@@ -16,5 +16,21 @@ /** | ||
/** | ||
* Display `defaultContent` until `promise` resolves. | ||
* Renders one of a series of values, including Promises, to a Part. | ||
* | ||
* Values are rendered in priority order, with the first argument having the | ||
* highest priority and the last argument having the lowest priority. If a | ||
* value is a Promise, low-priority values will be rendered until it resolves. | ||
* | ||
* The priority of values can be used to create placeholder content for async | ||
* data. For example, a Promise with pending content can be the first, | ||
* highest-priority, argument, and a non_promise loading indicator template can | ||
* be used as the second, lower-priority, argument. The loading indicator will | ||
* render immediately, and the primary content will render when the Promise | ||
* resolves. | ||
* | ||
* Example: | ||
* | ||
* const content = fetch('./content.txt').then(r => r.text()); | ||
* html`${until(content, html`<span>Loading...</span>`)}` | ||
*/ | ||
export declare const until: (promise: Promise<any>, defaultContent: any) => (part: Part) => void; | ||
export declare const until: (...args: any[]) => (part: Part) => void; | ||
//# sourceMappingURL=until.d.ts.map |
@@ -14,11 +14,68 @@ /** | ||
*/ | ||
import { isPrimitive } from '../lib/parts.js'; | ||
import { directive } from '../lit-html.js'; | ||
const _state = new WeakMap(); | ||
/** | ||
* Display `defaultContent` until `promise` resolves. | ||
* Renders one of a series of values, including Promises, to a Part. | ||
* | ||
* Values are rendered in priority order, with the first argument having the | ||
* highest priority and the last argument having the lowest priority. If a | ||
* value is a Promise, low-priority values will be rendered until it resolves. | ||
* | ||
* The priority of values can be used to create placeholder content for async | ||
* data. For example, a Promise with pending content can be the first, | ||
* highest-priority, argument, and a non_promise loading indicator template can | ||
* be used as the second, lower-priority, argument. The loading indicator will | ||
* render immediately, and the primary content will render when the Promise | ||
* resolves. | ||
* | ||
* Example: | ||
* | ||
* const content = fetch('./content.txt').then(r => r.text()); | ||
* html`${until(content, html`<span>Loading...</span>`)}` | ||
*/ | ||
export const until = directive((promise, defaultContent) => (part) => { | ||
part.setValue(defaultContent); | ||
part.commit(); | ||
part.setValue(promise); | ||
export const until = directive((...args) => (part) => { | ||
let state = _state.get(part); | ||
if (state === undefined) { | ||
state = { | ||
values: [], | ||
}; | ||
_state.set(part, state); | ||
} | ||
const previousValues = state.values; | ||
let changedSinceLastRender = false; | ||
state.values = args; | ||
for (let i = 0; i < args.length; i++) { | ||
const value = args[i]; | ||
// If we've seen this value before, we've already handled it. | ||
if (value === previousValues[i] && !changedSinceLastRender) { | ||
continue; | ||
} | ||
changedSinceLastRender = true; | ||
// Render non-Promise values immediately | ||
if (isPrimitive(value) || typeof value.then !== 'function') { | ||
part.setValue(value); | ||
state.lastRenderedIndex = i; | ||
// Since a lower-priority value will never overwrite a higher-priority | ||
// synchronous value, we can stop processsing now. | ||
break; | ||
} | ||
// We have a Promise that we haven't seen before, so priorities may have | ||
// changed. Forget what we rendered before. | ||
state.lastRenderedIndex = undefined; | ||
Promise.resolve(value).then((resolvedValue) => { | ||
const index = state.values.indexOf(value); | ||
// If state.values doesn't contain the value, we've re-rendered without | ||
// the value, so don't render it. Then, only render if the value is | ||
// higher-priority than what's already been rendered. | ||
if (index > -1 && | ||
(state.lastRenderedIndex === undefined || | ||
index < state.lastRenderedIndex)) { | ||
state.lastRenderedIndex = index; | ||
part.setValue(resolvedValue); | ||
part.commit(); | ||
} | ||
}); | ||
} | ||
}); | ||
//# sourceMappingURL=until.js.map |
@@ -82,3 +82,2 @@ /** | ||
private _commitIterable; | ||
private _commitPromise; | ||
clear(startNode?: Node): void; | ||
@@ -85,0 +84,0 @@ } |
@@ -173,5 +173,2 @@ /** | ||
} | ||
else if (value.then !== undefined) { | ||
this._commitPromise(value); | ||
} | ||
else { | ||
@@ -268,11 +265,2 @@ // Fallback, will render the string representation | ||
} | ||
_commitPromise(value) { | ||
this.value = value; | ||
value.then((v) => { | ||
if (this.value === value) { | ||
this.setValue(v); | ||
this.commit(); | ||
} | ||
}); | ||
} | ||
clear(startNode = this.startNode) { | ||
@@ -407,4 +395,4 @@ removeNodes(this.startNode.parentNode, startNode.nextSibling, this.endNode); | ||
} | ||
this._options = getOptions(newListener); | ||
if (shouldAddListener) { | ||
this._options = getOptions(newListener); | ||
this.element.addEventListener(this.eventName, this._boundHandleEvent, this._options); | ||
@@ -411,0 +399,0 @@ } |
@@ -20,3 +20,59 @@ /** | ||
} | ||
/** | ||
* Extension to the standard `render` method which supports rendering | ||
* to ShadowRoots when the ShadyDOM (https://github.com/webcomponents/shadydom) | ||
* and ShadyCSS (https://github.com/webcomponents/shadycss) polyfills are used | ||
* or when the webcomponentsjs | ||
* (https://github.com/webcomponents/webcomponentsjs) polyfill is used. | ||
* | ||
* Adds a `scopeName` option which is used to scope element DOM and stylesheets | ||
* when native ShadowDOM is unavailable. The `scopeName` will be added to | ||
* the class attribute of all rendered DOM. In addition, any style elements will | ||
* be automatically re-written with this `scopeName` selector and moved out | ||
* of the rendered DOM and into the document <head>. | ||
* | ||
* It is common to use this render method in conjunction with a custom element | ||
* which renders a shadowRoot. When this is done, typically the element's | ||
* `localName` should be used as the `scopeName`. | ||
* | ||
* In addition to DOM scoping, ShadyCSS also supports a basic shim for css | ||
* custom properties (needed only on older browsers like IE11) and a shim for | ||
* a deprecated feature called `@apply` that supports applying a set of css | ||
* custom properties to a given location. | ||
* | ||
* Usage considerations: | ||
* | ||
* * Part values in <style> elements are only applied the first time a given | ||
* `scopeName` renders. Subsequent changes to parts in style elements will have | ||
* no effect. Because of this, parts in style elements should only be used for | ||
* values that will never change, for example parts that set scope-wide theme | ||
* values or parts which render shared style elements. | ||
* | ||
* * Note, due to a limitation of the ShadyDOM polyfill, rendering in a | ||
* custom element's `constructor` is not supported. Instead rendering should | ||
* either done asynchronously, for example at microtask timing (e.g. | ||
* Promise.resolve()), or be deferred until the element's `connectedCallback` | ||
* first runs. | ||
* | ||
* Usage considerations when using shimmed custom properties or `@apply`: | ||
* | ||
* * Whenever any dynamic changes are made which affect | ||
* css custom properties, `ShadyCSS.styleElement(element)` must be called | ||
* to update the element. There are two cases when this is needed: | ||
* (1) the element is connected to a new parent, (2) a class is added to the | ||
* element that causes it to match different custom properties. | ||
* To address the first case when rendering a custom element, `styleElement` | ||
* should be called in the element's `connectedCallback`. | ||
* | ||
* * Shimmed custom properties may only be defined either for an entire | ||
* shadowRoot (e.g. via `:host`) or via a rule that directly matches an element | ||
* with a shadowRoot. In other words, instead of flowing from parent to child as | ||
* do native css custom properties, shimmed custom properties flow only from | ||
* shadowRoots to nested shadowRoots. | ||
* | ||
* * When using `@apply` mixing css shorthand property names with | ||
* non-shorthand names (for example `border` and `border-width`) is not | ||
* supported. | ||
*/ | ||
export declare const render: (result: TemplateResult, container: Element | DocumentFragment, options: ShadyRenderOptions) => void; | ||
//# sourceMappingURL=shady-render.d.ts.map |
@@ -14,7 +14,9 @@ /** | ||
*/ | ||
import { removeNodes } from './dom.js'; | ||
import { insertNodeIntoTemplate, removeNodesFromTemplate } from './modify-template.js'; | ||
import { parts, render as litRender } from './render.js'; | ||
import { templateCaches } from './template-factory.js'; | ||
import { TemplateInstance } from './template-instance.js'; | ||
import { TemplateResult } from './template-result.js'; | ||
import { Template } from './template.js'; | ||
import { marker, Template } from './template.js'; | ||
export { html, svg, TemplateResult } from '../lit-html.js'; | ||
@@ -41,6 +43,14 @@ // Get a key to lookup in `templateCaches`. | ||
if (templateCache === undefined) { | ||
templateCache = new Map(); | ||
templateCache = { | ||
stringsArray: new WeakMap(), | ||
keyString: new Map() | ||
}; | ||
templateCaches.set(cacheKey, templateCache); | ||
} | ||
let template = templateCache.get(result.strings); | ||
let template = templateCache.stringsArray.get(result.strings); | ||
if (template !== undefined) { | ||
return template; | ||
} | ||
const key = result.strings.join(marker); | ||
template = templateCache.keyString.get(key); | ||
if (template === undefined) { | ||
@@ -52,4 +62,5 @@ const element = result.getTemplateElement(); | ||
template = new Template(result, element); | ||
templateCache.set(result.strings, template); | ||
templateCache.keyString.set(key, template); | ||
} | ||
templateCache.stringsArray.set(result.strings, template); | ||
return template; | ||
@@ -65,3 +76,3 @@ }; | ||
if (templates !== undefined) { | ||
templates.forEach((template) => { | ||
templates.keyString.forEach((template) => { | ||
const { element: { content } } = template; | ||
@@ -140,21 +151,98 @@ // IE 11 doesn't support the iterable param Set constructor | ||
}; | ||
/** | ||
* Extension to the standard `render` method which supports rendering | ||
* to ShadowRoots when the ShadyDOM (https://github.com/webcomponents/shadydom) | ||
* and ShadyCSS (https://github.com/webcomponents/shadycss) polyfills are used | ||
* or when the webcomponentsjs | ||
* (https://github.com/webcomponents/webcomponentsjs) polyfill is used. | ||
* | ||
* Adds a `scopeName` option which is used to scope element DOM and stylesheets | ||
* when native ShadowDOM is unavailable. The `scopeName` will be added to | ||
* the class attribute of all rendered DOM. In addition, any style elements will | ||
* be automatically re-written with this `scopeName` selector and moved out | ||
* of the rendered DOM and into the document <head>. | ||
* | ||
* It is common to use this render method in conjunction with a custom element | ||
* which renders a shadowRoot. When this is done, typically the element's | ||
* `localName` should be used as the `scopeName`. | ||
* | ||
* In addition to DOM scoping, ShadyCSS also supports a basic shim for css | ||
* custom properties (needed only on older browsers like IE11) and a shim for | ||
* a deprecated feature called `@apply` that supports applying a set of css | ||
* custom properties to a given location. | ||
* | ||
* Usage considerations: | ||
* | ||
* * Part values in <style> elements are only applied the first time a given | ||
* `scopeName` renders. Subsequent changes to parts in style elements will have | ||
* no effect. Because of this, parts in style elements should only be used for | ||
* values that will never change, for example parts that set scope-wide theme | ||
* values or parts which render shared style elements. | ||
* | ||
* * Note, due to a limitation of the ShadyDOM polyfill, rendering in a | ||
* custom element's `constructor` is not supported. Instead rendering should | ||
* either done asynchronously, for example at microtask timing (e.g. | ||
* Promise.resolve()), or be deferred until the element's `connectedCallback` | ||
* first runs. | ||
* | ||
* Usage considerations when using shimmed custom properties or `@apply`: | ||
* | ||
* * Whenever any dynamic changes are made which affect | ||
* css custom properties, `ShadyCSS.styleElement(element)` must be called | ||
* to update the element. There are two cases when this is needed: | ||
* (1) the element is connected to a new parent, (2) a class is added to the | ||
* element that causes it to match different custom properties. | ||
* To address the first case when rendering a custom element, `styleElement` | ||
* should be called in the element's `connectedCallback`. | ||
* | ||
* * Shimmed custom properties may only be defined either for an entire | ||
* shadowRoot (e.g. via `:host`) or via a rule that directly matches an element | ||
* with a shadowRoot. In other words, instead of flowing from parent to child as | ||
* do native css custom properties, shimmed custom properties flow only from | ||
* shadowRoots to nested shadowRoots. | ||
* | ||
* * When using `@apply` mixing css shorthand property names with | ||
* non-shorthand names (for example `border` and `border-width`) is not | ||
* supported. | ||
*/ | ||
export const render = (result, container, options) => { | ||
const scopeName = options.scopeName; | ||
const hasRendered = parts.has(container); | ||
litRender(result, container, Object.assign({ templateFactory: shadyTemplateFactory(scopeName) }, options)); | ||
// When rendering a TemplateResult, scope the template with ShadyCSS | ||
if (container instanceof ShadowRoot && compatibleShadyCSSVersion && | ||
result instanceof TemplateResult) { | ||
// Scope the element template one time only for this scope. | ||
if (!shadyRenderSet.has(scopeName)) { | ||
const part = parts.get(container); | ||
const instance = part.value; | ||
prepareTemplateStyles(container, instance.template, scopeName); | ||
const needsScoping = container instanceof ShadowRoot && | ||
compatibleShadyCSSVersion && result instanceof TemplateResult; | ||
// Handle first render to a scope specially... | ||
const firstScopeRender = needsScoping && !shadyRenderSet.has(scopeName); | ||
// On first scope render, render into a fragment; this cannot be a single | ||
// fragment that is reused since nested renders can occur synchronously. | ||
const renderContainer = firstScopeRender ? document.createDocumentFragment() : container; | ||
litRender(result, renderContainer, Object.assign({ templateFactory: shadyTemplateFactory(scopeName) }, options)); | ||
// When performing first scope render, | ||
// (1) We've rendered into a fragment so that there's a chance to | ||
// `prepareTemplateStyles` before sub-elements hit the DOM | ||
// (which might cause them to render based on a common pattern of | ||
// rendering in a custom element's `connectedCallback`); | ||
// (2) Scope the template with ShadyCSS one time only for this scope. | ||
// (3) Render the fragment into the container and make sure the | ||
// container knows its `part` is the one we just rendered. This ensures | ||
// DOM will be re-used on subsequent renders. | ||
if (firstScopeRender) { | ||
const part = parts.get(renderContainer); | ||
parts.delete(renderContainer); | ||
if (part.value instanceof TemplateInstance) { | ||
prepareTemplateStyles(renderContainer, part.value.template, scopeName); | ||
} | ||
// Update styling if this is the initial render to this container. | ||
if (!hasRendered) { | ||
window.ShadyCSS.styleElement(container.host); | ||
} | ||
removeNodes(container, container.firstChild); | ||
container.appendChild(renderContainer); | ||
parts.set(container, part); | ||
} | ||
// After elements have hit the DOM, update styling if this is the | ||
// initial render to this container. | ||
// This is needed whenever dynamic changes are made so it would be | ||
// safest to do every render; however, this would regress performance | ||
// so we leave it up to the user to call `ShadyCSSS.styleElement` | ||
// for dynamic changes. | ||
if (!hasRendered && needsScoping) { | ||
window.ShadyCSS.styleElement(container.host); | ||
} | ||
}; | ||
//# sourceMappingURL=shady-render.js.map |
@@ -42,3 +42,17 @@ /** | ||
export declare function templateFactory(result: TemplateResult): Template; | ||
export declare const templateCaches: Map<string, Map<TemplateStringsArray, Template>>; | ||
/** | ||
* The first argument to JS template tags retain identity across multiple | ||
* calls to a tag for the same literal, so we can cache work done per literal | ||
* in a Map. | ||
* | ||
* Safari currently has a bug which occasionally breaks this behaviour, so we | ||
* need to cache the Template at two levels. We first cache the | ||
* TemplateStringsArray, and if that fails, we cache a key constructed by | ||
* joining the strings array. | ||
*/ | ||
export declare type templateCache = { | ||
stringsArray: WeakMap<TemplateStringsArray, Template>; | ||
keyString: Map<string, Template>; | ||
}; | ||
export declare const templateCaches: Map<string, templateCache>; | ||
//# sourceMappingURL=template-factory.d.ts.map |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { Template } from './template.js'; | ||
import { marker, Template } from './template.js'; | ||
/** | ||
@@ -23,16 +23,28 @@ * The default TemplateFactory which caches Templates keyed on | ||
if (templateCache === undefined) { | ||
templateCache = new Map(); | ||
templateCache = { | ||
stringsArray: new WeakMap(), | ||
keyString: new Map() | ||
}; | ||
templateCaches.set(result.type, templateCache); | ||
} | ||
let template = templateCache.get(result.strings); | ||
let template = templateCache.stringsArray.get(result.strings); | ||
if (template !== undefined) { | ||
return template; | ||
} | ||
// If the TemplateStringsArray is new, generate a key from the strings | ||
// This key is shared between all templates with identical content | ||
const key = result.strings.join(marker); | ||
// Check if we already have a Template for this key | ||
template = templateCache.keyString.get(key); | ||
if (template === undefined) { | ||
// If we have not seen this key before, create a new Template | ||
template = new Template(result, result.getTemplateElement()); | ||
templateCache.set(result.strings, template); | ||
// Cache the Template for this key | ||
templateCache.keyString.set(key, template); | ||
} | ||
// Cache all future queries for this TemplateStringsArray | ||
templateCache.stringsArray.set(result.strings, template); | ||
return template; | ||
} | ||
// The first argument to JS template tags retain identity across multiple | ||
// calls to a tag for the same literal, so we can cache work done per literal | ||
// in a Map. | ||
export const templateCaches = new Map(); | ||
//# sourceMappingURL=template-factory.js.map |
@@ -15,3 +15,3 @@ /** | ||
import { reparentNodes } from './dom.js'; | ||
import { lastAttributeNameRegex, marker, nodeMarker, rewritesStyleAttribute } from './template.js'; | ||
import { boundAttributeSuffix, lastAttributeNameRegex, marker, nodeMarker } from './template.js'; | ||
/** | ||
@@ -32,27 +32,25 @@ * The return type of `html`, which holds a Template and the values from | ||
getHTML() { | ||
const l = this.strings.length - 1; | ||
const endIndex = this.strings.length - 1; | ||
let html = ''; | ||
let isTextBinding = true; | ||
for (let i = 0; i < l; i++) { | ||
for (let i = 0; i < endIndex; i++) { | ||
const s = this.strings[i]; | ||
html += s; | ||
const close = s.lastIndexOf('>'); | ||
// We're in a text position if the previous string closed its last tag, an | ||
// attribute position if the string opened an unclosed tag, and unchanged | ||
// if the string had no brackets at all: | ||
// | ||
// "...>...": text position. open === -1, close > -1 | ||
// "...<...": attribute position. open > -1 | ||
// "...": no change. open === -1, close === -1 | ||
isTextBinding = | ||
(close > -1 || isTextBinding) && s.indexOf('<', close + 1) === -1; | ||
if (!isTextBinding && rewritesStyleAttribute) { | ||
html = html.replace(lastAttributeNameRegex, (match, p1, p2, p3) => { | ||
return (p2 === 'style') ? `${p1}style$${p3}` : match; | ||
}); | ||
// This replace() call does two things: | ||
// 1) Appends a suffix to all bound attribute names to opt out of special | ||
// attribute value parsing that IE11 and Edge do, like for style and | ||
// many SVG attributes. The Template class also appends the same suffix | ||
// when looking up attributes to creat Parts. | ||
// 2) Adds an unquoted-attribute-safe marker for the first expression in | ||
// an attribute. Subsequent attribute expressions will use node markers, | ||
// and this is safe since attributes with multiple expressions are | ||
// guaranteed to be quoted. | ||
let addedMarker = false; | ||
html += s.replace(lastAttributeNameRegex, (_match, whitespace, name, value) => { | ||
addedMarker = true; | ||
return whitespace + name + boundAttributeSuffix + value + marker; | ||
}); | ||
if (!addedMarker) { | ||
html += nodeMarker; | ||
} | ||
html += isTextBinding ? nodeMarker : marker; | ||
} | ||
html += this.strings[l]; | ||
return html; | ||
return html + this.strings[endIndex]; | ||
} | ||
@@ -59,0 +57,0 @@ getTemplateElement() { |
@@ -21,9 +21,12 @@ /** | ||
/** | ||
* An expression marker used text-positions, not attribute positions, | ||
* in template. | ||
* An expression marker used text-positions, multi-binding attributes, and | ||
* attributes with markup-like text values. | ||
*/ | ||
export declare const nodeMarker: string; | ||
export declare const markerRegex: RegExp; | ||
export declare const rewritesStyleAttribute: boolean; | ||
/** | ||
* Suffix appended to all bound attribute names. | ||
*/ | ||
export declare const boundAttributeSuffix = "$lit$"; | ||
/** | ||
* An updateable Template that tracks the location of dynamic parts. | ||
@@ -30,0 +33,0 @@ */ |
@@ -20,13 +20,12 @@ /** | ||
/** | ||
* An expression marker used text-positions, not attribute positions, | ||
* in template. | ||
* An expression marker used text-positions, multi-binding attributes, and | ||
* attributes with markup-like text values. | ||
*/ | ||
export const nodeMarker = `<!--${marker}-->`; | ||
export const markerRegex = new RegExp(`${marker}|${nodeMarker}`); | ||
export const rewritesStyleAttribute = (() => { | ||
const el = document.createElement('div'); | ||
el.setAttribute('style', '{{bad value}}'); | ||
return el.getAttribute('style') !== '{{bad value}}'; | ||
})(); | ||
/** | ||
* Suffix appended to all bound attribute names. | ||
*/ | ||
export const boundAttributeSuffix = '$lit$'; | ||
/** | ||
* An updateable Template that tracks the location of dynamic parts. | ||
@@ -77,13 +76,7 @@ */ | ||
// Find the corresponding attribute | ||
// If the attribute name contains special characters, lower-case | ||
// it so that on XML nodes with case-sensitive getAttribute() we | ||
// can still find the attribute, which will have been lower-cased | ||
// by the parser. | ||
// | ||
// If the attribute name doesn't contain special character, it's | ||
// important to _not_ lower-case it, in case the name is | ||
// case-sensitive, like with XML attributes like "viewBox". | ||
const attributeLookupName = (rewritesStyleAttribute && name === 'style') ? | ||
'style$' : | ||
/^[a-zA-Z-]*$/.test(name) ? name : name.toLowerCase(); | ||
// All bound attributes have had a suffix added in | ||
// TemplateResult#getHTML to opt out of special attribute | ||
// handling. To look up the attribute value we also need to add | ||
// the suffix. | ||
const attributeLookupName = name.toLowerCase() + boundAttributeSuffix; | ||
const attributeValue = node.getAttribute(attributeLookupName); | ||
@@ -90,0 +83,0 @@ const strings = attributeValue.split(markerRegex); |
@@ -20,5 +20,7 @@ /** | ||
export { AttributeCommitter, AttributePart, BooleanAttributePart, EventPart, isPrimitive, NodePart, PropertyCommitter, PropertyPart } from './lib/parts.js'; | ||
export { RenderOptions } from './lib/render-options.js'; | ||
export { parts, render } from './lib/render.js'; | ||
export { templateCaches, templateFactory } from './lib/template-factory.js'; | ||
export { TemplateInstance } from './lib/template-instance.js'; | ||
export { TemplateProcessor } from './lib/template-processor.js'; | ||
export { SVGTemplateResult, TemplateResult } from './lib/template-result.js'; | ||
@@ -25,0 +27,0 @@ export { createMarker, isTemplatePartActive, Template } from './lib/template.js'; |
{ | ||
"name": "lit-html", | ||
"version": "0.13.0", | ||
"version": "0.14.0", | ||
"description": "HTML template literals in JavaScript", | ||
@@ -19,2 +19,3 @@ "license": "BSD-3-Clause", | ||
"/directives/", | ||
"/polyfills", | ||
"/src/", | ||
@@ -26,3 +27,3 @@ "!/src/test/" | ||
"checksize": "rollup -c ; cat lit-html.bundled.js | gzip -9 | wc -c ; rm lit-html.bundled.js", | ||
"gen-docs": "typedoc --readme none --mode modules --excludeNotExported --excludePrivate --exclude **/*_test.ts --out ./docs/api ./src", | ||
"gen-docs": "typedoc --readme none --mode modules --ignoreCompilerErrors --excludeNotExported --excludePrivate --exclude **/*_test.ts --out ./docs/api --gaID UA-39334307-23 ./src", | ||
"test": "npm run build && npm run lint && wct --npm", | ||
@@ -47,3 +48,3 @@ "quicktest": "wct -l chrome -p --npm", | ||
"tslint": "^5.11.0", | ||
"typedoc": "^0.12.0", | ||
"typedoc": "^0.13.0", | ||
"typescript": "^3.1.1", | ||
@@ -50,0 +51,0 @@ "uglify-es": "^3.3.5", |
@@ -13,3 +13,3 @@ > ## 🛠 Status: In Development | ||
Full documentation is available at [polymer.github.io/lit-html](https://polymer.github.io/lit-html). | ||
Full documentation is available at [lit-html.polymer-project.org](https://lit-html.polymer-project.org). | ||
@@ -16,0 +16,0 @@ ## Overview |
@@ -53,2 +53,8 @@ /** | ||
for await (let v of value) { | ||
// Check to make sure that value is the still the current value of | ||
// the part, and if not bail because a new value owns this part | ||
if (part.value !== value) { | ||
break; | ||
} | ||
// When we get the first value, clear the part. This lets the | ||
@@ -60,8 +66,2 @@ // previous value display until we can replace it. | ||
// Check to make sure that value is the still the current value of | ||
// the part, and if not bail because a new value owns this part | ||
if (part.value !== value) { | ||
break; | ||
} | ||
// As a convenience, because functional-programming-style | ||
@@ -68,0 +68,0 @@ // transforms of iterables and async iterables requires a library, |
@@ -55,2 +55,8 @@ /** | ||
for await (let v of value) { | ||
// Check to make sure that value is the still the current value of | ||
// the part, and if not bail because a new value owns this part | ||
if (part.value !== value) { | ||
break; | ||
} | ||
// When we get the first value, clear the part. This let's the | ||
@@ -63,8 +69,2 @@ // previous value display until we can replace it. | ||
// Check to make sure that value is the still the current value of | ||
// the part, and if not bail because a new value owns this part | ||
if (part.value !== value) { | ||
break; | ||
} | ||
// As a convenience, because functional-programming-style | ||
@@ -71,0 +71,0 @@ // transforms of iterables and async iterables requires a library, |
@@ -17,32 +17,59 @@ /** | ||
const previousExpressions = new WeakMap<Part, any>(); | ||
const previousValues = new WeakMap<Part, unknown>(); | ||
/** | ||
* Creates a guard directive. Prevents any re-render until the identity of the | ||
* expression changes, for example when a primitive changes value or when an | ||
* object reference changes. | ||
* Prevents re-render of a template function until a single value or an array of | ||
* values changes. | ||
* | ||
* This useful with immutable data patterns, by preventing expensive work until | ||
* data updates. Example: | ||
* Example: | ||
* | ||
* ```js | ||
* html` | ||
* <div> | ||
* ${guard(items, () => items.map(item => html`${item}`))} | ||
* ${guard([user.id, company.id], () => html`...`)} | ||
* </div> | ||
* ` | ||
* ``` | ||
* | ||
* In this case, the template only renders if either `user.id` or `company.id` | ||
* changes. | ||
* | ||
* guard() is useful with immutable data patterns, by preventing expensive work | ||
* until data updates. | ||
* | ||
* Example: | ||
* | ||
* ```js | ||
* html` | ||
* <div> | ||
* ${guard([immutableItems], () => immutableItems.map(i => html`${i}`))} | ||
* </div> | ||
* ``` | ||
* | ||
* In this case, items are mapped over only when the array reference changes. | ||
* | ||
* @param expression the expression to check before re-rendering | ||
* @param valueFn function which returns the render value | ||
* @param value the value to check before re-rendering | ||
* @param f the template function | ||
*/ | ||
export const guard = | ||
directive((expression: any, valueFn: () => any) => (part: Part): void => { | ||
// Dirty check previous expression | ||
if (previousExpressions.get(part) === expression) { | ||
directive((value: unknown, f: () => unknown) => (part: Part): void => { | ||
const previousValue = previousValues.get(part); | ||
if (Array.isArray(value)) { | ||
// Dirty-check arrays by item | ||
if (Array.isArray(previousValue) && | ||
previousValue.length === value.length && | ||
value.every((v, i) => v === previousValue[i])) { | ||
return; | ||
} | ||
} else if ( | ||
previousValue === value && | ||
(value !== undefined || previousValues.has(part))) { | ||
// Dirty-check non-arrays by identity | ||
return; | ||
} | ||
part.setValue(valueFn()); | ||
previousExpressions.set(part, expression); | ||
part.setValue(f()); | ||
// Copy the value if it's an array so that if it's mutated we don't forget | ||
// what the previous values were. | ||
previousValues.set( | ||
part, Array.isArray(value) ? Array.from(value) : value); | ||
}); |
@@ -15,12 +15,84 @@ /** | ||
import {isPrimitive} from '../lib/parts.js'; | ||
import {directive, Part} from '../lit-html.js'; | ||
interface AsyncState { | ||
/** | ||
* The last rendered index of a call to until(). A value only renders if its | ||
* index is less than the `lastRenderedIndex`. | ||
*/ | ||
lastRenderedIndex?: number; | ||
values: unknown[]; | ||
} | ||
const _state = new WeakMap<Part, AsyncState>(); | ||
/** | ||
* Display `defaultContent` until `promise` resolves. | ||
* Renders one of a series of values, including Promises, to a Part. | ||
* | ||
* Values are rendered in priority order, with the first argument having the | ||
* highest priority and the last argument having the lowest priority. If a | ||
* value is a Promise, low-priority values will be rendered until it resolves. | ||
* | ||
* The priority of values can be used to create placeholder content for async | ||
* data. For example, a Promise with pending content can be the first, | ||
* highest-priority, argument, and a non_promise loading indicator template can | ||
* be used as the second, lower-priority, argument. The loading indicator will | ||
* render immediately, and the primary content will render when the Promise | ||
* resolves. | ||
* | ||
* Example: | ||
* | ||
* const content = fetch('./content.txt').then(r => r.text()); | ||
* html`${until(content, html`<span>Loading...</span>`)}` | ||
*/ | ||
export const until = | ||
directive((promise: Promise<any>, defaultContent: any) => (part: Part) => { | ||
part.setValue(defaultContent); | ||
part.commit(); | ||
part.setValue(promise); | ||
export const until = directive((...args: any[]) => (part: Part) => { | ||
let state = _state.get(part)!; | ||
if (state === undefined) { | ||
state = { | ||
values: [], | ||
}; | ||
_state.set(part, state); | ||
} | ||
const previousValues = state.values; | ||
let changedSinceLastRender = false; | ||
state.values = args; | ||
for (let i = 0; i < args.length; i++) { | ||
const value = args[i]; | ||
// If we've seen this value before, we've already handled it. | ||
if (value === previousValues[i] && !changedSinceLastRender) { | ||
continue; | ||
} | ||
changedSinceLastRender = true; | ||
// Render non-Promise values immediately | ||
if (isPrimitive(value) || typeof value.then !== 'function') { | ||
part.setValue(value); | ||
state.lastRenderedIndex = i; | ||
// Since a lower-priority value will never overwrite a higher-priority | ||
// synchronous value, we can stop processsing now. | ||
break; | ||
} | ||
// We have a Promise that we haven't seen before, so priorities may have | ||
// changed. Forget what we rendered before. | ||
state.lastRenderedIndex = undefined; | ||
Promise.resolve(value).then((resolvedValue: unknown) => { | ||
const index = state.values.indexOf(value); | ||
// If state.values doesn't contain the value, we've re-rendered without | ||
// the value, so don't render it. Then, only render if the value is | ||
// higher-priority than what's already been rendered. | ||
if (index > -1 && | ||
(state.lastRenderedIndex === undefined || | ||
index < state.lastRenderedIndex)) { | ||
state.lastRenderedIndex = index; | ||
part.setValue(resolvedValue); | ||
part.commit(); | ||
} | ||
}); | ||
} | ||
}); |
@@ -199,4 +199,2 @@ /** | ||
this._commitIterable(value); | ||
} else if (value.then !== undefined) { | ||
this._commitPromise(value); | ||
} else { | ||
@@ -303,12 +301,2 @@ // Fallback, will render the string representation | ||
private _commitPromise(value: Promise<any>): void { | ||
this.value = value; | ||
value.then((v: any) => { | ||
if (this.value === value) { | ||
this.setValue(v); | ||
this.commit(); | ||
} | ||
}); | ||
} | ||
clear(startNode: Node = this.startNode) { | ||
@@ -471,4 +459,4 @@ removeNodes( | ||
} | ||
this._options = getOptions(newListener); | ||
if (shouldAddListener) { | ||
this._options = getOptions(newListener); | ||
this.element.addEventListener( | ||
@@ -475,0 +463,0 @@ this.eventName, this._boundHandleEvent, this._options); |
@@ -15,2 +15,3 @@ /** | ||
import {removeNodes} from './dom.js'; | ||
import {insertNodeIntoTemplate, removeNodesFromTemplate} from './modify-template.js'; | ||
@@ -22,7 +23,6 @@ import {RenderOptions} from './render-options.js'; | ||
import {TemplateResult} from './template-result.js'; | ||
import {Template} from './template.js'; | ||
import {marker, Template} from './template.js'; | ||
export {html, svg, TemplateResult} from '../lit-html.js'; | ||
// Get a key to lookup in `templateCaches`. | ||
@@ -53,6 +53,16 @@ const getTemplateCacheKey = (type: string, scopeName: string) => | ||
if (templateCache === undefined) { | ||
templateCache = new Map<TemplateStringsArray, Template>(); | ||
templateCache = { | ||
stringsArray: new WeakMap<TemplateStringsArray, Template>(), | ||
keyString: new Map<string, Template>() | ||
}; | ||
templateCaches.set(cacheKey, templateCache); | ||
} | ||
let template = templateCache.get(result.strings); | ||
let template = templateCache.stringsArray.get(result.strings); | ||
if (template !== undefined) { | ||
return template; | ||
} | ||
const key = result.strings.join(marker); | ||
template = templateCache.keyString.get(key); | ||
if (template === undefined) { | ||
@@ -64,8 +74,8 @@ const element = result.getTemplateElement(); | ||
template = new Template(result, element); | ||
templateCache.set(result.strings, template); | ||
templateCache.keyString.set(key, template); | ||
} | ||
templateCache.stringsArray.set(result.strings, template); | ||
return template; | ||
}; | ||
const TEMPLATE_TYPES = ['html', 'svg']; | ||
@@ -80,3 +90,3 @@ | ||
if (templates !== undefined) { | ||
templates.forEach((template) => { | ||
templates.keyString.forEach((template) => { | ||
const {element: {content}} = template; | ||
@@ -164,2 +174,58 @@ // IE 11 doesn't support the iterable param Set constructor | ||
/** | ||
* Extension to the standard `render` method which supports rendering | ||
* to ShadowRoots when the ShadyDOM (https://github.com/webcomponents/shadydom) | ||
* and ShadyCSS (https://github.com/webcomponents/shadycss) polyfills are used | ||
* or when the webcomponentsjs | ||
* (https://github.com/webcomponents/webcomponentsjs) polyfill is used. | ||
* | ||
* Adds a `scopeName` option which is used to scope element DOM and stylesheets | ||
* when native ShadowDOM is unavailable. The `scopeName` will be added to | ||
* the class attribute of all rendered DOM. In addition, any style elements will | ||
* be automatically re-written with this `scopeName` selector and moved out | ||
* of the rendered DOM and into the document <head>. | ||
* | ||
* It is common to use this render method in conjunction with a custom element | ||
* which renders a shadowRoot. When this is done, typically the element's | ||
* `localName` should be used as the `scopeName`. | ||
* | ||
* In addition to DOM scoping, ShadyCSS also supports a basic shim for css | ||
* custom properties (needed only on older browsers like IE11) and a shim for | ||
* a deprecated feature called `@apply` that supports applying a set of css | ||
* custom properties to a given location. | ||
* | ||
* Usage considerations: | ||
* | ||
* * Part values in <style> elements are only applied the first time a given | ||
* `scopeName` renders. Subsequent changes to parts in style elements will have | ||
* no effect. Because of this, parts in style elements should only be used for | ||
* values that will never change, for example parts that set scope-wide theme | ||
* values or parts which render shared style elements. | ||
* | ||
* * Note, due to a limitation of the ShadyDOM polyfill, rendering in a | ||
* custom element's `constructor` is not supported. Instead rendering should | ||
* either done asynchronously, for example at microtask timing (e.g. | ||
* Promise.resolve()), or be deferred until the element's `connectedCallback` | ||
* first runs. | ||
* | ||
* Usage considerations when using shimmed custom properties or `@apply`: | ||
* | ||
* * Whenever any dynamic changes are made which affect | ||
* css custom properties, `ShadyCSS.styleElement(element)` must be called | ||
* to update the element. There are two cases when this is needed: | ||
* (1) the element is connected to a new parent, (2) a class is added to the | ||
* element that causes it to match different custom properties. | ||
* To address the first case when rendering a custom element, `styleElement` | ||
* should be called in the element's `connectedCallback`. | ||
* | ||
* * Shimmed custom properties may only be defined either for an entire | ||
* shadowRoot (e.g. via `:host`) or via a rule that directly matches an element | ||
* with a shadowRoot. In other words, instead of flowing from parent to child as | ||
* do native css custom properties, shimmed custom properties flow only from | ||
* shadowRoots to nested shadowRoots. | ||
* | ||
* * When using `@apply` mixing css shorthand property names with | ||
* non-shorthand names (for example `border` and `border-width`) is not | ||
* supported. | ||
*/ | ||
export const render = | ||
@@ -171,21 +237,46 @@ (result: TemplateResult, | ||
const hasRendered = parts.has(container); | ||
litRender(result, container, { | ||
templateFactory: shadyTemplateFactory(scopeName), | ||
...options, | ||
} as RenderOptions); | ||
// When rendering a TemplateResult, scope the template with ShadyCSS | ||
if (container instanceof ShadowRoot && compatibleShadyCSSVersion && | ||
result instanceof TemplateResult) { | ||
// Scope the element template one time only for this scope. | ||
if (!shadyRenderSet.has(scopeName)) { | ||
const part = parts.get(container)!; | ||
const instance = part.value as TemplateInstance; | ||
const needsScoping = container instanceof ShadowRoot && | ||
compatibleShadyCSSVersion && result instanceof TemplateResult; | ||
// Handle first render to a scope specially... | ||
const firstScopeRender = needsScoping && !shadyRenderSet.has(scopeName); | ||
// On first scope render, render into a fragment; this cannot be a single | ||
// fragment that is reused since nested renders can occur synchronously. | ||
const renderContainer = | ||
firstScopeRender ? document.createDocumentFragment() : container; | ||
litRender( | ||
result, | ||
renderContainer, | ||
{templateFactory: shadyTemplateFactory(scopeName), ...options} as | ||
RenderOptions); | ||
// When performing first scope render, | ||
// (1) We've rendered into a fragment so that there's a chance to | ||
// `prepareTemplateStyles` before sub-elements hit the DOM | ||
// (which might cause them to render based on a common pattern of | ||
// rendering in a custom element's `connectedCallback`); | ||
// (2) Scope the template with ShadyCSS one time only for this scope. | ||
// (3) Render the fragment into the container and make sure the | ||
// container knows its `part` is the one we just rendered. This ensures | ||
// DOM will be re-used on subsequent renders. | ||
if (firstScopeRender) { | ||
const part = parts.get(renderContainer)!; | ||
parts.delete(renderContainer); | ||
if (part.value instanceof TemplateInstance) { | ||
prepareTemplateStyles( | ||
(container as ShadowRoot), instance.template, scopeName); | ||
renderContainer as DocumentFragment, | ||
part.value.template, | ||
scopeName); | ||
} | ||
// Update styling if this is the initial render to this container. | ||
if (!hasRendered) { | ||
window.ShadyCSS!.styleElement((container as ShadowRoot).host); | ||
} | ||
removeNodes(container, container.firstChild); | ||
container.appendChild(renderContainer); | ||
parts.set(container, part); | ||
} | ||
}; | ||
// After elements have hit the DOM, update styling if this is the | ||
// initial render to this container. | ||
// This is needed whenever dynamic changes are made so it would be | ||
// safest to do every render; however, this would regress performance | ||
// so we leave it up to the user to call `ShadyCSSS.styleElement` | ||
// for dynamic changes. | ||
if (!hasRendered && needsScoping) { | ||
window.ShadyCSS!.styleElement((container as ShadowRoot).host); | ||
} | ||
}; |
@@ -16,3 +16,3 @@ /** | ||
import {TemplateResult} from './template-result.js'; | ||
import {Template} from './template.js'; | ||
import {marker, Template} from './template.js'; | ||
@@ -48,17 +48,47 @@ /** | ||
if (templateCache === undefined) { | ||
templateCache = new Map<TemplateStringsArray, Template>(); | ||
templateCache = { | ||
stringsArray: new WeakMap<TemplateStringsArray, Template>(), | ||
keyString: new Map<string, Template>() | ||
}; | ||
templateCaches.set(result.type, templateCache); | ||
} | ||
let template = templateCache.get(result.strings); | ||
let template = templateCache.stringsArray.get(result.strings); | ||
if (template !== undefined) { | ||
return template; | ||
} | ||
// If the TemplateStringsArray is new, generate a key from the strings | ||
// This key is shared between all templates with identical content | ||
const key = result.strings.join(marker); | ||
// Check if we already have a Template for this key | ||
template = templateCache.keyString.get(key); | ||
if (template === undefined) { | ||
// If we have not seen this key before, create a new Template | ||
template = new Template(result, result.getTemplateElement()); | ||
templateCache.set(result.strings, template); | ||
// Cache the Template for this key | ||
templateCache.keyString.set(key, template); | ||
} | ||
// Cache all future queries for this TemplateStringsArray | ||
templateCache.stringsArray.set(result.strings, template); | ||
return template; | ||
} | ||
// The first argument to JS template tags retain identity across multiple | ||
// calls to a tag for the same literal, so we can cache work done per literal | ||
// in a Map. | ||
export const templateCaches = | ||
new Map<string, Map<TemplateStringsArray, Template>>(); | ||
/** | ||
* The first argument to JS template tags retain identity across multiple | ||
* calls to a tag for the same literal, so we can cache work done per literal | ||
* in a Map. | ||
* | ||
* Safari currently has a bug which occasionally breaks this behaviour, so we | ||
* need to cache the Template at two levels. We first cache the | ||
* TemplateStringsArray, and if that fails, we cache a key constructed by | ||
* joining the strings array. | ||
*/ | ||
export type templateCache = { | ||
stringsArray: WeakMap<TemplateStringsArray, Template>; | ||
keyString: Map<string, Template>; | ||
}; | ||
export const templateCaches = new Map<string, templateCache>(); |
@@ -19,3 +19,2 @@ /** | ||
import {RenderOptions} from './render-options.js'; | ||
// import {TemplateFactory} from './template-factory.js'; | ||
import {TemplateProcessor} from './template-processor.js'; | ||
@@ -22,0 +21,0 @@ import {isTemplatePartActive, Template} from './template.js'; |
@@ -17,3 +17,3 @@ /** | ||
import {TemplateProcessor} from './template-processor.js'; | ||
import {lastAttributeNameRegex, marker, nodeMarker, rewritesStyleAttribute} from './template.js'; | ||
import {boundAttributeSuffix, lastAttributeNameRegex, marker, nodeMarker} from './template.js'; | ||
@@ -43,28 +43,26 @@ /** | ||
getHTML(): string { | ||
const l = this.strings.length - 1; | ||
const endIndex = this.strings.length - 1; | ||
let html = ''; | ||
let isTextBinding = true; | ||
for (let i = 0; i < l; i++) { | ||
for (let i = 0; i < endIndex; i++) { | ||
const s = this.strings[i]; | ||
html += s; | ||
const close = s.lastIndexOf('>'); | ||
// We're in a text position if the previous string closed its last tag, an | ||
// attribute position if the string opened an unclosed tag, and unchanged | ||
// if the string had no brackets at all: | ||
// | ||
// "...>...": text position. open === -1, close > -1 | ||
// "...<...": attribute position. open > -1 | ||
// "...": no change. open === -1, close === -1 | ||
isTextBinding = | ||
(close > -1 || isTextBinding) && s.indexOf('<', close + 1) === -1; | ||
if (!isTextBinding && rewritesStyleAttribute) { | ||
html = html.replace(lastAttributeNameRegex, (match, p1, p2, p3) => { | ||
return (p2 === 'style') ? `${p1}style$${p3}` : match; | ||
}); | ||
// This replace() call does two things: | ||
// 1) Appends a suffix to all bound attribute names to opt out of special | ||
// attribute value parsing that IE11 and Edge do, like for style and | ||
// many SVG attributes. The Template class also appends the same suffix | ||
// when looking up attributes to creat Parts. | ||
// 2) Adds an unquoted-attribute-safe marker for the first expression in | ||
// an attribute. Subsequent attribute expressions will use node markers, | ||
// and this is safe since attributes with multiple expressions are | ||
// guaranteed to be quoted. | ||
let addedMarker = false; | ||
html += s.replace( | ||
lastAttributeNameRegex, (_match, whitespace, name, value) => { | ||
addedMarker = true; | ||
return whitespace + name + boundAttributeSuffix + value + marker; | ||
}); | ||
if (!addedMarker) { | ||
html += nodeMarker; | ||
} | ||
html += isTextBinding ? nodeMarker : marker; | ||
} | ||
html += this.strings[l]; | ||
return html; | ||
return html + this.strings[endIndex]; | ||
} | ||
@@ -71,0 +69,0 @@ |
@@ -24,4 +24,4 @@ /** | ||
/** | ||
* An expression marker used text-positions, not attribute positions, | ||
* in template. | ||
* An expression marker used text-positions, multi-binding attributes, and | ||
* attributes with markup-like text values. | ||
*/ | ||
@@ -32,7 +32,6 @@ export const nodeMarker = `<!--${marker}-->`; | ||
export const rewritesStyleAttribute = (() => { | ||
const el = document.createElement('div'); | ||
el.setAttribute('style', '{{bad value}}'); | ||
return el.getAttribute('style') !== '{{bad value}}'; | ||
})(); | ||
/** | ||
* Suffix appended to all bound attribute names. | ||
*/ | ||
export const boundAttributeSuffix = '$lit$'; | ||
@@ -92,14 +91,8 @@ /** | ||
// Find the corresponding attribute | ||
// If the attribute name contains special characters, lower-case | ||
// it so that on XML nodes with case-sensitive getAttribute() we | ||
// can still find the attribute, which will have been lower-cased | ||
// by the parser. | ||
// | ||
// If the attribute name doesn't contain special character, it's | ||
// important to _not_ lower-case it, in case the name is | ||
// case-sensitive, like with XML attributes like "viewBox". | ||
// All bound attributes have had a suffix added in | ||
// TemplateResult#getHTML to opt out of special attribute | ||
// handling. To look up the attribute value we also need to add | ||
// the suffix. | ||
const attributeLookupName = | ||
(rewritesStyleAttribute && name === 'style') ? | ||
'style$' : | ||
/^[a-zA-Z-]*$/.test(name) ? name : name.toLowerCase(); | ||
name.toLowerCase() + boundAttributeSuffix; | ||
const attributeValue = node.getAttribute(attributeLookupName)!; | ||
@@ -106,0 +99,0 @@ const strings = attributeValue.split(markerRegex); |
@@ -24,5 +24,7 @@ /** | ||
export {AttributeCommitter, AttributePart, BooleanAttributePart, EventPart, isPrimitive, NodePart, PropertyCommitter, PropertyPart} from './lib/parts.js'; | ||
export {RenderOptions} from './lib/render-options.js'; | ||
export {parts, render} from './lib/render.js'; | ||
export {templateCaches, templateFactory} from './lib/template-factory.js'; | ||
export {TemplateInstance} from './lib/template-instance.js'; | ||
export {TemplateProcessor} from './lib/template-processor.js'; | ||
export {SVGTemplateResult, TemplateResult} from './lib/template-result.js'; | ||
@@ -29,0 +31,0 @@ export {createMarker, isTemplatePartActive, Template} from './lib/template.js'; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
466079
135
6317