Comparing version 0.11.0-dev.1 to 0.11.0-dev.2
@@ -29,4 +29,17 @@ /** | ||
export declare class TemplateProcessor { | ||
/** | ||
* Create parts for an attribute-position binding, given the event, attribute | ||
* name, and string literals. | ||
* | ||
* @param element The element containing the binding | ||
* @param name The attribute name | ||
* @param strings The string literals. There are always at least two strings, | ||
* event for fully-controlled bindings with a single expression. | ||
*/ | ||
handleAttributeExpressions(element: Element, name: string, strings: string[]): Part[]; | ||
handleTextExpression(node: Node, templateFactory: TemplateFactory): NodePart; | ||
/** | ||
* Create parts for a text-position binding. | ||
* @param templateFactory | ||
*/ | ||
handleTextExpression(templateFactory: TemplateFactory): NodePart; | ||
} | ||
@@ -106,3 +119,16 @@ export declare const defaultTemplateProcessor: TemplateProcessor; | ||
/** | ||
* Internal placeholder for a dynamic expression in an HTML template. | ||
* A placeholder for a dynamic expression in an HTML template. | ||
* | ||
* There are two built-in part types: AttributePart and NodePart. NodeParts | ||
* always represent a single dynamic expression, while AttributeParts may | ||
* represent as many expressions are contained in the attribute. | ||
* | ||
* A Template's parts are mutable, so parts can be replaced or modified | ||
* (possibly to implement different template semantics). The contract is that | ||
* parts can only be replaced, not removed, added or reordered, and parts must | ||
* always consume the correct number of values in their `update()` method. | ||
* | ||
* TODO(justinfagnani): That requirement is a little fragile. A | ||
* TemplateInstance could instead be more careful about which values it gives | ||
* to Part.update(). | ||
*/ | ||
@@ -127,10 +153,2 @@ export declare type TemplatePart = { | ||
} | ||
/** | ||
* Returns a value ready to be inserted into a Part from a user-provided value. | ||
* | ||
* If the user value is a directive, this invokes the directive with the given | ||
* part. If the value is null, it's converted to undefined to work better | ||
* with certain DOM APIs, like textContent. | ||
*/ | ||
export declare const getValue: (part: Part, value: any) => any; | ||
export interface DirectiveFn<P = Part> { | ||
@@ -141,2 +159,3 @@ (part: P): void; | ||
export declare const directive: <P = Part>(f: DirectiveFn<P>) => DirectiveFn<P>; | ||
export declare const isDirective: (o: any) => boolean; | ||
/** | ||
@@ -158,5 +177,16 @@ * A sentinel value that signals that a value was handled by a directive and | ||
_value: any; | ||
/** | ||
* Sets the current part value, but does not write it to the DOM. | ||
* @param value The value that will be committed. | ||
*/ | ||
setValue(value: any): void; | ||
/** | ||
* Commits the current part value, cause it to actually be written to the DOM. | ||
*/ | ||
commit(): void; | ||
} | ||
/** | ||
* Sets attribute values for AttributeParts, so that the value is only set once | ||
* even if there are multiple parts for an attribute. | ||
*/ | ||
export declare class AttributeCommitter { | ||
@@ -169,2 +199,6 @@ element: Element; | ||
constructor(element: Element, name: string, strings: string[]); | ||
/** | ||
* Creates a single part. Override this to create a differnt type of part. | ||
*/ | ||
protected _createPart(): AttributePart; | ||
protected _getValue(): any; | ||
@@ -185,4 +219,26 @@ commit(): void; | ||
_value: any; | ||
constructor(startNode: Node, endNode: Node, templateFactory: TemplateFactory); | ||
_pendingValue: any; | ||
constructor(templateFactory: TemplateFactory); | ||
/** | ||
* Inserts this part between `ref` and `ref`'s next sibling. Both `ref` and | ||
* its next sibling must be static, unchanging nodes such as those that appear | ||
* in a literal section of a template. | ||
* | ||
* This part must be empty, as its contents are not automatically moved. | ||
*/ | ||
insertAfterNode(ref: Node): void; | ||
/** | ||
* Appends this part into a parent part. | ||
* | ||
* This part must be empty, as its contents are not automatically moved. | ||
*/ | ||
appendIntoPart(part: NodePart): void; | ||
/** | ||
* Appends this part after `ref` | ||
* | ||
* This part must be empty, as its contents are not automatically moved. | ||
*/ | ||
insertAfterPart(ref: NodePart): void; | ||
setValue(value: any): void; | ||
commit(): void; | ||
private _insert; | ||
@@ -195,3 +251,2 @@ private _setNode; | ||
clear(startNode?: Node): void; | ||
commit(): void; | ||
} | ||
@@ -198,0 +253,0 @@ /** |
216
core.js
@@ -32,2 +32,11 @@ /** | ||
export class TemplateProcessor { | ||
/** | ||
* Create parts for an attribute-position binding, given the event, attribute | ||
* name, and string literals. | ||
* | ||
* @param element The element containing the binding | ||
* @param name The attribute name | ||
* @param strings The string literals. There are always at least two strings, | ||
* event for fully-controlled bindings with a single expression. | ||
*/ | ||
handleAttributeExpressions(element, name, strings) { | ||
@@ -37,4 +46,8 @@ const comitter = new AttributeCommitter(element, name, strings); | ||
} | ||
handleTextExpression(node, templateFactory) { | ||
return new NodePart(node, node.nextSibling, templateFactory); | ||
/** | ||
* Create parts for a text-position binding. | ||
* @param templateFactory | ||
*/ | ||
handleTextExpression(templateFactory) { | ||
return new NodePart(templateFactory); | ||
} | ||
@@ -64,7 +77,12 @@ } | ||
html += s; | ||
// We're in a text position if the previous string closed its tags. | ||
// If it doesn't have any tags, then we use the previous text position | ||
// state. | ||
const closing = findTagClose(s); | ||
isTextBinding = closing > -1 ? closing < s.length : isTextBinding; | ||
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; | ||
html += isTextBinding ? nodeMarker : marker; | ||
@@ -149,2 +167,5 @@ } | ||
} | ||
// Allows `document.createComment('')` to be renamed for a | ||
// small manual size-savings. | ||
const createMarker = () => document.createComment(''); | ||
/** | ||
@@ -187,14 +208,2 @@ * An expression marker with embedded unique key to avoid collision with | ||
const lastAttributeNameRegex = /[ \x09\x0a\x0c\x0d]([^\0-\x1F\x7F-\x9F \x09\x0a\x0c\x0d"'>=/]+)[ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*)$/; | ||
/** | ||
* Finds the closing index of the last closed HTML tag. | ||
* This has 3 possible return values: | ||
* - `-1`, meaning there is no tag in str. | ||
* - `string.length`, meaning the last opened tag is unclosed. | ||
* - Some positive number < str.length, meaning the index of the closing '>'. | ||
*/ | ||
function findTagClose(str) { | ||
const close = str.lastIndexOf('>'); | ||
const open = str.indexOf('<', close + 1); | ||
return open > -1 ? str.length : close; | ||
} | ||
export const isTemplatePartActive = (part) => part.index !== -1; | ||
@@ -280,3 +289,3 @@ /** | ||
for (let i = 0; i < lastIndex; i++) { | ||
parent.insertBefore((strings[i] === '') ? document.createComment('') : | ||
parent.insertBefore((strings[i] === '') ? createMarker() : | ||
document.createTextNode(strings[i]), node); | ||
@@ -286,3 +295,3 @@ this.parts.push({ type: 'node', index: index++ }); | ||
parent.insertBefore(strings[lastIndex] === '' ? | ||
document.createComment('') : | ||
createMarker() : | ||
document.createTextNode(strings[lastIndex]), node); | ||
@@ -307,3 +316,3 @@ nodesToRemove.push(node); | ||
previousSibling.nodeType !== Node.TEXT_NODE) { | ||
parent.insertBefore(document.createComment(''), node); | ||
parent.insertBefore(createMarker(), node); | ||
} | ||
@@ -319,3 +328,3 @@ else { | ||
if (node.nextSibling === null) { | ||
parent.insertBefore(document.createComment(''), node); | ||
parent.insertBefore(createMarker(), node); | ||
} | ||
@@ -337,18 +346,2 @@ else { | ||
} | ||
/** | ||
* Returns a value ready to be inserted into a Part from a user-provided value. | ||
* | ||
* If the user value is a directive, this invokes the directive with the given | ||
* part. If the value is null, it's converted to undefined to work better | ||
* with certain DOM APIs, like textContent. | ||
*/ | ||
export const getValue = (part, value) => { | ||
// `null` as the value of a Text node will render the string 'null' | ||
// so we convert it to undefined | ||
if (isDirective(value)) { | ||
value = value(part); | ||
return noChange; | ||
} | ||
return value === null ? undefined : value; | ||
}; | ||
export const directive = (f) => { | ||
@@ -358,3 +351,3 @@ f.__litDirective = true; | ||
}; | ||
const isDirective = (o) => typeof o === 'function' && o.__litDirective === true; | ||
export const isDirective = (o) => typeof o === 'function' && o.__litDirective === true; | ||
/** | ||
@@ -371,2 +364,6 @@ * A sentinel value that signals that a value was handled by a directive and | ||
!(typeof value === 'object' || typeof value === 'function')); | ||
/** | ||
* Sets attribute values for AttributeParts, so that the value is only set once | ||
* even if there are multiple parts for an attribute. | ||
*/ | ||
export class AttributeCommitter { | ||
@@ -380,5 +377,11 @@ constructor(element, name, strings) { | ||
for (let i = 0; i < strings.length - 1; i++) { | ||
this.parts[i] = new AttributePart(this); | ||
this.parts[i] = this._createPart(); | ||
} | ||
} | ||
/** | ||
* Creates a single part. Override this to create a differnt type of part. | ||
*/ | ||
_createPart() { | ||
return new AttributePart(this); | ||
} | ||
_getValue() { | ||
@@ -420,15 +423,21 @@ const strings = this.strings; | ||
setValue(value) { | ||
value = getValue(this, value); | ||
if (value === noChange) { | ||
return; | ||
} | ||
if (_isPrimitiveValue(value)) { | ||
if (value === this._value) { | ||
return; | ||
value = value === null ? undefined : value; | ||
if (value !== noChange || !_isPrimitiveValue(value) || | ||
value !== this._value) { | ||
this._value = value; | ||
if (!isDirective(value)) { | ||
this.committer.dirty = true; | ||
} | ||
} | ||
this._value = value; | ||
this.committer.dirty = true; | ||
} | ||
commit() { | ||
// console.log('commit', this._value); | ||
while (isDirective(this._value)) { | ||
const directive = this._value; | ||
this._value = noChange; | ||
directive(this); | ||
} | ||
if (this._value === noChange) { | ||
return; | ||
} | ||
this.committer.commit(); | ||
@@ -438,10 +447,47 @@ } | ||
export class NodePart { | ||
constructor(startNode, endNode, templateFactory) { | ||
constructor(templateFactory) { | ||
this._value = undefined; | ||
this._pendingValue = undefined; | ||
this.templateFactory = templateFactory; | ||
this.startNode = startNode; | ||
this.endNode = endNode; | ||
} | ||
/** | ||
* Inserts this part between `ref` and `ref`'s next sibling. Both `ref` and | ||
* its next sibling must be static, unchanging nodes such as those that appear | ||
* in a literal section of a template. | ||
* | ||
* This part must be empty, as its contents are not automatically moved. | ||
*/ | ||
insertAfterNode(ref) { | ||
this.startNode = ref; | ||
this.endNode = ref.nextSibling; | ||
} | ||
/** | ||
* Appends this part into a parent part. | ||
* | ||
* This part must be empty, as its contents are not automatically moved. | ||
*/ | ||
appendIntoPart(part) { | ||
part._insert(this.startNode = createMarker()); | ||
part._insert(this.endNode = createMarker()); | ||
} | ||
/** | ||
* Appends this part after `ref` | ||
* | ||
* This part must be empty, as its contents are not automatically moved. | ||
*/ | ||
insertAfterPart(ref) { | ||
ref._insert(this.startNode = createMarker()); | ||
this.endNode = ref.endNode; | ||
ref.endNode = this.startNode; | ||
} | ||
setValue(value) { | ||
value = getValue(this, value); | ||
this._pendingValue = value; | ||
} | ||
commit() { | ||
while (isDirective(this._pendingValue)) { | ||
const directive = this._pendingValue; | ||
this._pendingValue = noChange; | ||
directive(this); | ||
} | ||
const value = this._pendingValue === null ? undefined : this._pendingValue; | ||
if (value === noChange) { | ||
@@ -451,8 +497,5 @@ return; | ||
if (_isPrimitiveValue(value)) { | ||
// Handle primitive values | ||
if (value === this._value) { | ||
// If the value didn't change, do nothing | ||
return; | ||
if (value !== this._value) { | ||
this._setText(value); | ||
} | ||
this._setText(value); | ||
} | ||
@@ -494,3 +537,3 @@ else if (value instanceof TemplateResult) { | ||
// set its value, rather than replacing it. | ||
// TODO(justinfagnani): Can we just check if _previousValue is primitive? | ||
// TODO(justinfagnani): Can we just check if _value is primitive? | ||
node.textContent = value; | ||
@@ -510,2 +553,5 @@ } | ||
else { | ||
// Make sure we propagate the template processor from the TemplateResult | ||
// so that we use it's syntax extension, etc. The template factory comes | ||
// from the render function so that it can control caching. | ||
instance = | ||
@@ -524,9 +570,9 @@ new TemplateInstance(template, value.processor, this.templateFactory); | ||
// array.map((i) => html`${i}`), by reusing existing TemplateInstances. | ||
// If _previousValue is an array, then the previous render was of an | ||
// iterable and _previousValue will contain the NodeParts from the previous | ||
// render. If _previousValue is not an array, clear this part and make a new | ||
// If _value is an array, then the previous render was of an | ||
// iterable and _value will contain the NodeParts from the previous | ||
// render. If _value is not an array, clear this part and make a new | ||
// array for NodeParts. | ||
if (!Array.isArray(this._value)) { | ||
this._value = []; | ||
this.clear(); | ||
this._value = []; | ||
} | ||
@@ -537,33 +583,25 @@ // Lets us keep track of how many items we stamped so we can clear leftover | ||
let partIndex = 0; | ||
let itemPart; | ||
for (const item of value) { | ||
// Try to reuse an existing part | ||
let itemPart = itemParts[partIndex]; | ||
itemPart = itemParts[partIndex]; | ||
// If no existing part, create a new one | ||
if (itemPart === undefined) { | ||
// If we're creating the first item part, it's startNode should be the | ||
// container's startNode | ||
let itemStart = this.startNode; | ||
// If we're not creating the first part, create a new separator marker | ||
// node, and fix up the previous part's endNode to point to it | ||
if (partIndex > 0) { | ||
const previousPart = itemParts[partIndex - 1]; | ||
itemStart = previousPart.endNode = document.createTextNode(''); | ||
this._insert(itemStart); | ||
itemPart = new NodePart(this.templateFactory); | ||
itemParts.push(itemPart); | ||
if (partIndex === 0) { | ||
itemPart.appendIntoPart(this); | ||
} | ||
itemPart = new NodePart(itemStart, this.endNode, this.templateFactory); | ||
itemParts.push(itemPart); | ||
else { | ||
itemPart.insertAfterPart(itemParts[partIndex - 1]); | ||
} | ||
} | ||
itemPart.setValue(item); | ||
itemPart.commit(); | ||
partIndex++; | ||
} | ||
if (partIndex === 0) { | ||
this.clear(); | ||
this._value = undefined; | ||
} | ||
else if (partIndex < itemParts.length) { | ||
const lastPart = itemParts[partIndex - 1]; | ||
// Truncate the parts array so _previousValue reflects the current state | ||
if (partIndex < itemParts.length) { | ||
// Truncate the parts array so _value reflects the current state | ||
itemParts.length = partIndex; | ||
this.clear(lastPart.endNode.previousSibling); | ||
lastPart.endNode = this.endNode; | ||
this.clear(itemPart && itemPart.endNode); | ||
} | ||
@@ -576,2 +614,3 @@ } | ||
this.setValue(v); | ||
this.commit(); | ||
} | ||
@@ -583,5 +622,2 @@ }); | ||
} | ||
commit() { | ||
// nothing for now... | ||
} | ||
} | ||
@@ -642,3 +678,5 @@ /** | ||
if (part.type === 'node') { | ||
this._parts.push(this.processor.handleTextExpression(node, this._getTemplate)); | ||
const part = this.processor.handleTextExpression(this._getTemplate); | ||
part.insertAfterNode(node); | ||
this._parts.push(part); | ||
} | ||
@@ -645,0 +683,0 @@ else { |
@@ -81,3 +81,3 @@ /** | ||
// Create a new node to separate the previous and next Parts | ||
itemStartNode = document.createTextNode(''); | ||
itemStartNode = document.createComment(''); | ||
// itemPart is currently the Part for the previous item. Set | ||
@@ -89,5 +89,6 @@ // it's endNode to the node we'll use for the next Part's | ||
} | ||
itemPart = | ||
new NodePart(itemStartNode, part.endNode, part.templateFactory); | ||
itemPart = new NodePart(part.templateFactory); | ||
itemPart.insertAfterNode(itemStartNode); | ||
itemPart.setValue(v); | ||
itemPart.commit(); | ||
i++; | ||
@@ -94,0 +95,0 @@ } |
@@ -49,3 +49,3 @@ /** | ||
// of the iterable as a value itself. | ||
const itemPart = new NodePart(part.startNode, part.endNode, part.templateFactory); | ||
const itemPart = new NodePart(part.templateFactory); | ||
part._value = itemPart; | ||
@@ -60,2 +60,3 @@ let i = 0; | ||
part.clear(); | ||
itemPart.appendIntoPart(part); | ||
} | ||
@@ -75,2 +76,3 @@ // Check to make sure that value is the still the current value of | ||
itemPart.setValue(v); | ||
itemPart.commit(); | ||
i++; | ||
@@ -77,0 +79,0 @@ } |
@@ -53,7 +53,15 @@ /** | ||
if (itemPart === undefined) { | ||
const marker = document.createTextNode(''); | ||
const endNode = document.createTextNode(''); | ||
// TODO(justinfagnani): We really want to avoid manual marker creation | ||
// here and instead use something like part.insertBeforePart(). This | ||
// requires a little refactoring, like iterating through values and | ||
// existing parts like NodePart#_setIterable does. We can also remove | ||
// keyMapCache and use part._value instead. | ||
// But... repeat() is badly in need of rewriting, so we'll do this for | ||
// now and revisit soon. | ||
const marker = document.createComment(''); | ||
const endNode = document.createComment(''); | ||
container.insertBefore(marker, currentMarker); | ||
container.insertBefore(endNode, currentMarker); | ||
itemPart = new NodePart(marker, endNode, part.templateFactory); | ||
itemPart = new NodePart(part.templateFactory); | ||
itemPart.insertAfterNode(marker); | ||
if (key !== undefined) { | ||
@@ -75,2 +83,3 @@ keyMap.set(key, itemPart); | ||
itemPart.setValue(result); | ||
itemPart.commit(); | ||
} | ||
@@ -77,0 +86,0 @@ // Cleanup |
@@ -15,9 +15,2 @@ /** | ||
import { DirectiveFn, NodePart } from '../core.js'; | ||
/** | ||
* Renders the result as HTML, rather than text. | ||
* | ||
* Note, this is unsafe to use with any user-provided input that hasn't been | ||
* sanitized or escaped, as it may lead to cross-site-scripting | ||
* vulnerabilities. | ||
*/ | ||
export declare const unsafeHTML: (value: any) => DirectiveFn<NodePart>; |
@@ -22,5 +22,7 @@ /** | ||
*/ | ||
const previousValues = new WeakMap(); | ||
export const unsafeHTML = (value) => directive((part) => { | ||
// Dirty check primitive values | ||
if (part._value === value && _isPrimitiveValue(value)) { | ||
const previousValue = previousValues.get(part); | ||
if (previousValue === value && _isPrimitiveValue(value)) { | ||
return; | ||
@@ -32,6 +34,4 @@ } | ||
part.setValue(document.importNode(tmp.content, true)); | ||
// Remember the actual value passed to unsafeHTML rather than the DOM | ||
// Nodes we created | ||
part._value = value; | ||
previousValues.set(part, value); | ||
}); | ||
//# sourceMappingURL=unsafe-html.js.map |
@@ -20,4 +20,5 @@ /** | ||
part.setValue(defaultContent); | ||
part.commit(); | ||
part.setValue(promise); | ||
}); | ||
//# sourceMappingURL=until.js.map |
@@ -14,4 +14,4 @@ /** | ||
*/ | ||
import { AttributeCommitter, Part, SVGTemplateResult, TemplateProcessor, TemplateResult } from './core.js'; | ||
export { render } from './core.js'; | ||
import { AttributeCommitter, AttributePart, Part, SVGTemplateResult, TemplateProcessor, TemplateResult } from './core.js'; | ||
export * from './core.js'; | ||
/** | ||
@@ -69,3 +69,3 @@ * Interprets a template literal as a lit-html HTML template. | ||
_value: any; | ||
private _dirty; | ||
_pendingValue: any; | ||
constructor(element: Element, name: string, strings: string[]); | ||
@@ -75,8 +75,20 @@ setValue(value: any): void; | ||
} | ||
/** | ||
* Sets attribute values for PropertyParts, so that the value is only set once | ||
* even if there are multiple parts for a property. | ||
* | ||
* If an expression controls the whole property value, then the value is simply | ||
* assigned to the property under control. If there are string literals or | ||
* multiple expressions, then the strings are expressions are interpolated into | ||
* a string first. | ||
*/ | ||
export declare class PropertyCommitter extends AttributeCommitter { | ||
single: boolean; | ||
constructor(element: Element, name: string, strings: string[]); | ||
protected _createPart(): PropertyPart; | ||
_getValue(): any; | ||
commit(): void; | ||
} | ||
export declare class PropertyPart extends AttributePart { | ||
} | ||
export declare class EventPart implements Part { | ||
@@ -86,3 +98,3 @@ element: Element; | ||
_value: any; | ||
private _dirty; | ||
_pendingValue: any; | ||
constructor(element: Element, eventName: string); | ||
@@ -89,0 +101,0 @@ setValue(value: any): void; |
@@ -14,4 +14,4 @@ /** | ||
*/ | ||
import { AttributeCommitter, getValue, noChange, SVGTemplateResult, TemplateProcessor, TemplateResult } from './core.js'; | ||
export { render } from './core.js'; | ||
import { AttributeCommitter, AttributePart, isDirective, noChange, SVGTemplateResult, TemplateProcessor, TemplateResult } from './core.js'; | ||
export * from './core.js'; | ||
/** | ||
@@ -80,5 +80,5 @@ * Interprets a template literal as a lit-html HTML template. | ||
this._value = undefined; | ||
this._dirty = true; | ||
this._pendingValue = undefined; | ||
if (strings.length !== 2 || strings[0] !== '' || strings[1] !== '') { | ||
throw new Error('boolean attributes can only contain a single expression'); | ||
throw new Error('Boolean attributes can only contain a single expression'); | ||
} | ||
@@ -90,15 +90,16 @@ this.element = element; | ||
setValue(value) { | ||
value = getValue(this, value); | ||
if (value === noChange) { | ||
this._pendingValue = value; | ||
} | ||
commit() { | ||
while (isDirective(this._pendingValue)) { | ||
const directive = this._pendingValue; | ||
this._pendingValue = noChange; | ||
directive(this); | ||
} | ||
if (this._pendingValue === noChange) { | ||
return; | ||
} | ||
this._value = !!value; | ||
if (this._value !== !!value) { | ||
this._dirty = true; | ||
} | ||
} | ||
commit() { | ||
if (this._dirty) { | ||
this._dirty = false; | ||
if (this._value) { | ||
const value = !!this._pendingValue; | ||
if (this._value !== value) { | ||
if (value) { | ||
this.element.setAttribute(this.name, ''); | ||
@@ -110,4 +111,15 @@ } | ||
} | ||
this._value = value; | ||
this._pendingValue = noChange; | ||
} | ||
} | ||
/** | ||
* Sets attribute values for PropertyParts, so that the value is only set once | ||
* even if there are multiple parts for a property. | ||
* | ||
* If an expression controls the whole property value, then the value is simply | ||
* assigned to the property under control. If there are string literals or | ||
* multiple expressions, then the strings are expressions are interpolated into | ||
* a string first. | ||
*/ | ||
export class PropertyCommitter extends AttributeCommitter { | ||
@@ -119,2 +131,5 @@ constructor(element, name, strings) { | ||
} | ||
_createPart() { | ||
return new PropertyPart(this); | ||
} | ||
_getValue() { | ||
@@ -133,6 +148,8 @@ if (this.single) { | ||
} | ||
export class PropertyPart extends AttributePart { | ||
} | ||
export class EventPart { | ||
constructor(element, eventName) { | ||
this._value = undefined; | ||
this._dirty = true; | ||
this._pendingValue = undefined; | ||
this.element = element; | ||
@@ -142,14 +159,15 @@ this.eventName = eventName; | ||
setValue(value) { | ||
value = getValue(this, value); | ||
if (value !== noChange) { | ||
if ((value == null) !== (this._value == null)) { | ||
this._dirty = true; | ||
} | ||
this._value = value; | ||
} | ||
this._pendingValue = value; | ||
} | ||
commit() { | ||
if (this._dirty) { | ||
this._dirty = false; | ||
if (this._value == null) { | ||
while (isDirective(this._pendingValue)) { | ||
const directive = this._pendingValue; | ||
this._pendingValue = noChange; | ||
directive(this); | ||
} | ||
if (this._pendingValue === noChange) { | ||
return; | ||
} | ||
if ((this._pendingValue == null) !== (this._value == null)) { | ||
if (this._pendingValue == null) { | ||
this.element.removeEventListener(this.eventName, this); | ||
@@ -161,2 +179,4 @@ } | ||
} | ||
this._value = this._pendingValue; | ||
this._pendingValue = noChange; | ||
} | ||
@@ -163,0 +183,0 @@ handleEvent(event) { |
{ | ||
"name": "lit-html", | ||
"version": "0.11.0-dev.1", | ||
"version": "0.11.0-dev.2", | ||
"description": "HTML template literals in JavaScript", | ||
@@ -38,6 +38,3 @@ "license": "BSD-3-Clause", | ||
"@types/mocha": "^5.2.0", | ||
"@webcomponents/shadycss": "^1.2.1", | ||
"@webcomponents/template": "^1.3.1", | ||
"@webcomponents/webcomponentsjs": "^2.0.2", | ||
"babel-polyfill": "^6.26.0", | ||
"@webcomponents/webcomponentsjs": "^2.0.4", | ||
"chai": "^4.1.2", | ||
@@ -44,0 +41,0 @@ "mocha": "^5.2.0", |
253
src/core.ts
@@ -39,2 +39,11 @@ /** | ||
export class TemplateProcessor { | ||
/** | ||
* Create parts for an attribute-position binding, given the event, attribute | ||
* name, and string literals. | ||
* | ||
* @param element The element containing the binding | ||
* @param name The attribute name | ||
* @param strings The string literals. There are always at least two strings, | ||
* event for fully-controlled bindings with a single expression. | ||
*/ | ||
handleAttributeExpressions(element: Element, name: string, strings: string[]): | ||
@@ -46,4 +55,8 @@ Part[] { | ||
handleTextExpression(node: Node, templateFactory: TemplateFactory) { | ||
return new NodePart(node, node.nextSibling!, templateFactory); | ||
/** | ||
* Create parts for a text-position binding. | ||
* @param templateFactory | ||
*/ | ||
handleTextExpression(templateFactory: TemplateFactory) { | ||
return new NodePart(templateFactory); | ||
} | ||
@@ -83,7 +96,12 @@ } | ||
html += s; | ||
// We're in a text position if the previous string closed its tags. | ||
// If it doesn't have any tags, then we use the previous text position | ||
// state. | ||
const closing = findTagClose(s); | ||
isTextBinding = closing > -1 ? closing < s.length : isTextBinding; | ||
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; | ||
html += isTextBinding ? nodeMarker : marker; | ||
@@ -207,2 +225,6 @@ } | ||
// Allows `document.createComment('')` to be renamed for a | ||
// small manual size-savings. | ||
const createMarker = () => document.createComment(''); | ||
/** | ||
@@ -251,17 +273,17 @@ * An expression marker with embedded unique key to avoid collision with | ||
/** | ||
* Finds the closing index of the last closed HTML tag. | ||
* This has 3 possible return values: | ||
* - `-1`, meaning there is no tag in str. | ||
* - `string.length`, meaning the last opened tag is unclosed. | ||
* - Some positive number < str.length, meaning the index of the closing '>'. | ||
* A placeholder for a dynamic expression in an HTML template. | ||
* | ||
* There are two built-in part types: AttributePart and NodePart. NodeParts | ||
* always represent a single dynamic expression, while AttributeParts may | ||
* represent as many expressions are contained in the attribute. | ||
* | ||
* A Template's parts are mutable, so parts can be replaced or modified | ||
* (possibly to implement different template semantics). The contract is that | ||
* parts can only be replaced, not removed, added or reordered, and parts must | ||
* always consume the correct number of values in their `update()` method. | ||
* | ||
* TODO(justinfagnani): That requirement is a little fragile. A | ||
* TemplateInstance could instead be more careful about which values it gives | ||
* to Part.update(). | ||
*/ | ||
function findTagClose(str: string): number { | ||
const close = str.lastIndexOf('>'); | ||
const open = str.indexOf('<', close + 1); | ||
return open > -1 ? str.length : close; | ||
} | ||
/** | ||
* Internal placeholder for a dynamic expression in an HTML template. | ||
*/ | ||
export type TemplatePart = { | ||
@@ -367,3 +389,3 @@ type: 'node', | ||
parent.insertBefore( | ||
(strings[i] === '') ? document.createComment('') : | ||
(strings[i] === '') ? createMarker() : | ||
document.createTextNode(strings[i]), | ||
@@ -375,3 +397,3 @@ node); | ||
strings[lastIndex] === '' ? | ||
document.createComment('') : | ||
createMarker() : | ||
document.createTextNode(strings[lastIndex]), | ||
@@ -397,3 +419,3 @@ node); | ||
previousSibling.nodeType !== Node.TEXT_NODE) { | ||
parent.insertBefore(document.createComment(''), node); | ||
parent.insertBefore(createMarker(), node); | ||
} else { | ||
@@ -408,3 +430,3 @@ index--; | ||
if (node.nextSibling === null) { | ||
parent.insertBefore(document.createComment(''), node); | ||
parent.insertBefore(createMarker(), node); | ||
} else { | ||
@@ -426,18 +448,2 @@ index--; | ||
/** | ||
* Returns a value ready to be inserted into a Part from a user-provided value. | ||
* | ||
* If the user value is a directive, this invokes the directive with the given | ||
* part. If the value is null, it's converted to undefined to work better | ||
* with certain DOM APIs, like textContent. | ||
*/ | ||
export const getValue = (part: Part, value: any) => { | ||
// `null` as the value of a Text node will render the string 'null' | ||
// so we convert it to undefined | ||
if (isDirective(value)) { | ||
value = value(part); | ||
return noChange; | ||
} | ||
return value === null ? undefined : value; | ||
}; | ||
@@ -454,3 +460,3 @@ export interface DirectiveFn<P = Part> { | ||
const isDirective = (o: any) => | ||
export const isDirective = (o: any) => | ||
typeof o === 'function' && o.__litDirective === true; | ||
@@ -480,7 +486,18 @@ | ||
/** | ||
* Sets the current part value, but does not write it to the DOM. | ||
* @param value The value that will be committed. | ||
*/ | ||
setValue(value: any): void; | ||
/** | ||
* Commits the current part value, cause it to actually be written to the DOM. | ||
*/ | ||
commit(): void; | ||
} | ||
/** | ||
* Sets attribute values for AttributeParts, so that the value is only set once | ||
* even if there are multiple parts for an attribute. | ||
*/ | ||
export class AttributeCommitter { | ||
@@ -499,6 +516,13 @@ element: Element; | ||
for (let i = 0; i < strings.length - 1; i++) { | ||
this.parts[i] = new AttributePart(this); | ||
this.parts[i] = this._createPart(); | ||
} | ||
} | ||
/** | ||
* Creates a single part. Override this to create a differnt type of part. | ||
*/ | ||
protected _createPart(): AttributePart { | ||
return new AttributePart(this); | ||
} | ||
protected _getValue(): any { | ||
@@ -546,16 +570,22 @@ const strings = this.strings; | ||
setValue(value: any): void { | ||
value = getValue(this, value); | ||
if (value === noChange) { | ||
return; | ||
} | ||
if (_isPrimitiveValue(value)) { | ||
if (value === this._value) { | ||
return; | ||
value = value === null ? undefined : value; | ||
if (value !== noChange || !_isPrimitiveValue(value) || | ||
value !== this._value) { | ||
this._value = value; | ||
if (!isDirective(value)) { | ||
this.committer.dirty = true; | ||
} | ||
} | ||
this._value = value; | ||
this.committer.dirty = true; | ||
} | ||
commit() { | ||
// console.log('commit', this._value); | ||
while (isDirective(this._value)) { | ||
const directive = this._value; | ||
this._value = noChange; | ||
directive(this); | ||
} | ||
if (this._value === noChange) { | ||
return; | ||
} | ||
this.committer.commit(); | ||
@@ -567,15 +597,55 @@ } | ||
templateFactory: TemplateFactory; | ||
startNode: Node; | ||
endNode: Node; | ||
startNode!: Node; | ||
endNode!: Node; | ||
_value: any = undefined; | ||
_pendingValue: any = undefined; | ||
constructor( | ||
startNode: Node, endNode: Node, templateFactory: TemplateFactory) { | ||
constructor(templateFactory: TemplateFactory) { | ||
this.templateFactory = templateFactory; | ||
this.startNode = startNode; | ||
this.endNode = endNode; | ||
} | ||
/** | ||
* Inserts this part between `ref` and `ref`'s next sibling. Both `ref` and | ||
* its next sibling must be static, unchanging nodes such as those that appear | ||
* in a literal section of a template. | ||
* | ||
* This part must be empty, as its contents are not automatically moved. | ||
*/ | ||
insertAfterNode(ref: Node) { | ||
this.startNode = ref; | ||
this.endNode = ref.nextSibling!; | ||
} | ||
/** | ||
* Appends this part into a parent part. | ||
* | ||
* This part must be empty, as its contents are not automatically moved. | ||
*/ | ||
appendIntoPart(part: NodePart) { | ||
part._insert(this.startNode = createMarker()); | ||
part._insert(this.endNode = createMarker()); | ||
} | ||
/** | ||
* Appends this part after `ref` | ||
* | ||
* This part must be empty, as its contents are not automatically moved. | ||
*/ | ||
insertAfterPart(ref: NodePart) { | ||
ref._insert(this.startNode = createMarker()); | ||
this.endNode = ref.endNode; | ||
ref.endNode = this.startNode; | ||
} | ||
setValue(value: any): void { | ||
value = getValue(this, value); | ||
this._pendingValue = value; | ||
} | ||
commit() { | ||
while (isDirective(this._pendingValue)) { | ||
const directive = this._pendingValue; | ||
this._pendingValue = noChange; | ||
directive(this); | ||
} | ||
const value = this._pendingValue === null ? undefined : this._pendingValue; | ||
if (value === noChange) { | ||
@@ -585,8 +655,5 @@ return; | ||
if (_isPrimitiveValue(value)) { | ||
// Handle primitive values | ||
if (value === this._value) { | ||
// If the value didn't change, do nothing | ||
return; | ||
if (value !== this._value) { | ||
this._setText(value); | ||
} | ||
this._setText(value); | ||
} else if (value instanceof TemplateResult) { | ||
@@ -626,3 +693,3 @@ this._setTemplateResult(value); | ||
// set its value, rather than replacing it. | ||
// TODO(justinfagnani): Can we just check if _previousValue is primitive? | ||
// TODO(justinfagnani): Can we just check if _value is primitive? | ||
node.textContent = value; | ||
@@ -641,2 +708,5 @@ } else { | ||
} else { | ||
// Make sure we propagate the template processor from the TemplateResult | ||
// so that we use it's syntax extension, etc. The template factory comes | ||
// from the render function so that it can control caching. | ||
instance = | ||
@@ -657,9 +727,9 @@ new TemplateInstance(template, value.processor, this.templateFactory); | ||
// If _previousValue is an array, then the previous render was of an | ||
// iterable and _previousValue will contain the NodeParts from the previous | ||
// render. If _previousValue is not an array, clear this part and make a new | ||
// If _value is an array, then the previous render was of an | ||
// iterable and _value will contain the NodeParts from the previous | ||
// render. If _value is not an array, clear this part and make a new | ||
// array for NodeParts. | ||
if (!Array.isArray(this._value)) { | ||
this._value = []; | ||
this.clear(); | ||
this._value = []; | ||
} | ||
@@ -669,38 +739,29 @@ | ||
// items from a previous render | ||
const itemParts = this._value as any[]; | ||
const itemParts = this._value as NodePart[]; | ||
let partIndex = 0; | ||
let itemPart: NodePart|undefined; | ||
for (const item of value) { | ||
// Try to reuse an existing part | ||
let itemPart = itemParts[partIndex]; | ||
itemPart = itemParts[partIndex]; | ||
// If no existing part, create a new one | ||
if (itemPart === undefined) { | ||
// If we're creating the first item part, it's startNode should be the | ||
// container's startNode | ||
let itemStart = this.startNode; | ||
// If we're not creating the first part, create a new separator marker | ||
// node, and fix up the previous part's endNode to point to it | ||
if (partIndex > 0) { | ||
const previousPart = itemParts[partIndex - 1]; | ||
itemStart = previousPart.endNode = document.createTextNode(''); | ||
this._insert(itemStart); | ||
itemPart = new NodePart(this.templateFactory); | ||
itemParts.push(itemPart); | ||
if (partIndex === 0) { | ||
itemPart.appendIntoPart(this); | ||
} else { | ||
itemPart.insertAfterPart(itemParts[partIndex - 1]); | ||
} | ||
itemPart = new NodePart(itemStart, this.endNode, this.templateFactory); | ||
itemParts.push(itemPart); | ||
} | ||
itemPart.setValue(item); | ||
itemPart.commit(); | ||
partIndex++; | ||
} | ||
if (partIndex === 0) { | ||
this.clear(); | ||
this._value = undefined; | ||
} else if (partIndex < itemParts.length) { | ||
const lastPart = itemParts[partIndex - 1]; | ||
// Truncate the parts array so _previousValue reflects the current state | ||
if (partIndex < itemParts.length) { | ||
// Truncate the parts array so _value reflects the current state | ||
itemParts.length = partIndex; | ||
this.clear(lastPart.endNode.previousSibling!); | ||
lastPart.endNode = this.endNode; | ||
this.clear(itemPart && itemPart!.endNode); | ||
} | ||
@@ -714,2 +775,3 @@ } | ||
this.setValue(v); | ||
this.commit(); | ||
} | ||
@@ -723,6 +785,2 @@ }); | ||
} | ||
commit() { | ||
// nothing for now... | ||
} | ||
} | ||
@@ -799,4 +857,5 @@ | ||
if (part.type === 'node') { | ||
this._parts.push( | ||
this.processor.handleTextExpression(node, this._getTemplate)); | ||
const part = this.processor.handleTextExpression(this._getTemplate); | ||
part.insertAfterNode(node); | ||
this._parts.push(part); | ||
} else { | ||
@@ -803,0 +862,0 @@ this._parts.push(...this.processor.handleAttributeExpressions( |
@@ -82,3 +82,3 @@ /** | ||
// Create a new node to separate the previous and next Parts | ||
itemStartNode = document.createTextNode(''); | ||
itemStartNode = document.createComment(''); | ||
// itemPart is currently the Part for the previous item. Set | ||
@@ -90,7 +90,8 @@ // it's endNode to the node we'll use for the next Part's | ||
} | ||
itemPart = | ||
new NodePart(itemStartNode, part.endNode, part.templateFactory); | ||
itemPart = new NodePart(part.templateFactory); | ||
itemPart.insertAfterNode(itemStartNode); | ||
itemPart.setValue(v); | ||
itemPart.commit(); | ||
i++; | ||
} | ||
}); |
@@ -46,5 +46,3 @@ /** | ||
// of the iterable as a value itself. | ||
const itemPart = | ||
new NodePart(part.startNode, part.endNode, part.templateFactory); | ||
const itemPart = new NodePart(part.templateFactory); | ||
part._value = itemPart; | ||
@@ -59,2 +57,3 @@ | ||
part.clear(); | ||
itemPart.appendIntoPart(part); | ||
} | ||
@@ -77,4 +76,5 @@ | ||
itemPart.setValue(v); | ||
itemPart.commit(); | ||
i++; | ||
} | ||
}); |
@@ -70,7 +70,15 @@ /** | ||
if (itemPart === undefined) { | ||
const marker = document.createTextNode(''); | ||
const endNode = document.createTextNode(''); | ||
// TODO(justinfagnani): We really want to avoid manual marker creation | ||
// here and instead use something like part.insertBeforePart(). This | ||
// requires a little refactoring, like iterating through values and | ||
// existing parts like NodePart#_setIterable does. We can also remove | ||
// keyMapCache and use part._value instead. | ||
// But... repeat() is badly in need of rewriting, so we'll do this for | ||
// now and revisit soon. | ||
const marker = document.createComment(''); | ||
const endNode = document.createComment(''); | ||
container.insertBefore(marker, currentMarker); | ||
container.insertBefore(endNode, currentMarker); | ||
itemPart = new NodePart(marker, endNode, part.templateFactory); | ||
itemPart = new NodePart(part.templateFactory); | ||
itemPart.insertAfterNode(marker); | ||
if (key !== undefined) { | ||
@@ -91,2 +99,3 @@ keyMap.set(key, itemPart); | ||
itemPart.setValue(result); | ||
itemPart.commit(); | ||
} | ||
@@ -93,0 +102,0 @@ |
@@ -24,6 +24,10 @@ /** | ||
*/ | ||
const previousValues = new WeakMap<NodePart, string>(); | ||
export const unsafeHTML = (value: any): DirectiveFn<NodePart> => | ||
directive((part: NodePart): void => { | ||
// Dirty check primitive values | ||
if (part._value === value && _isPrimitiveValue(value)) { | ||
const previousValue = previousValues.get(part); | ||
if (previousValue === value && _isPrimitiveValue(value)) { | ||
return; | ||
@@ -36,6 +40,3 @@ } | ||
part.setValue(document.importNode(tmp.content, true)); | ||
// Remember the actual value passed to unsafeHTML rather than the DOM | ||
// Nodes we created | ||
part._value = value; | ||
previousValues.set(part, value); | ||
}); |
@@ -22,5 +22,6 @@ /** | ||
(promise: Promise<any>, defaultContent: any): DirectiveFn<NodePart> => | ||
directive((part: NodePart): void => { | ||
directive((part: NodePart) => { | ||
part.setValue(defaultContent); | ||
part.commit(); | ||
part.setValue(promise); | ||
}); |
@@ -15,5 +15,5 @@ /** | ||
import {AttributeCommitter, getValue, noChange, Part, SVGTemplateResult, TemplateProcessor, TemplateResult} from './core.js'; | ||
import {AttributeCommitter, AttributePart, isDirective, noChange, Part, SVGTemplateResult, TemplateProcessor, TemplateResult} from './core.js'; | ||
export {render} from './core.js'; | ||
export * from './core.js'; | ||
@@ -92,3 +92,3 @@ /** | ||
_value: any = undefined; | ||
private _dirty = true; | ||
_pendingValue: any = undefined; | ||
@@ -98,3 +98,3 @@ constructor(element: Element, name: string, strings: string[]) { | ||
throw new Error( | ||
'boolean attributes can only contain a single expression'); | ||
'Boolean attributes can only contain a single expression'); | ||
} | ||
@@ -107,16 +107,17 @@ this.element = element; | ||
setValue(value: any): void { | ||
value = getValue(this, value); | ||
if (value === noChange) { | ||
return; | ||
} | ||
this._value = !!value; | ||
if (this._value !== !!value) { | ||
this._dirty = true; | ||
} | ||
this._pendingValue = value; | ||
} | ||
commit() { | ||
if (this._dirty) { | ||
this._dirty = false; | ||
if (this._value) { | ||
while (isDirective(this._pendingValue)) { | ||
const directive = this._pendingValue; | ||
this._pendingValue = noChange; | ||
directive(this); | ||
} | ||
if (this._pendingValue === noChange) { | ||
return; | ||
} | ||
const value = !!this._pendingValue; | ||
if (this._value !== value) { | ||
if (value) { | ||
this.element.setAttribute(this.name, ''); | ||
@@ -127,5 +128,16 @@ } else { | ||
} | ||
this._value = value; | ||
this._pendingValue = noChange; | ||
} | ||
} | ||
/** | ||
* Sets attribute values for PropertyParts, so that the value is only set once | ||
* even if there are multiple parts for a property. | ||
* | ||
* If an expression controls the whole property value, then the value is simply | ||
* assigned to the property under control. If there are string literals or | ||
* multiple expressions, then the strings are expressions are interpolated into | ||
* a string first. | ||
*/ | ||
export class PropertyCommitter extends AttributeCommitter { | ||
@@ -140,2 +152,6 @@ single: boolean; | ||
protected _createPart(): PropertyPart { | ||
return new PropertyPart(this); | ||
} | ||
_getValue() { | ||
@@ -156,2 +172,4 @@ if (this.single) { | ||
export class PropertyPart extends AttributePart {} | ||
export class EventPart implements Part { | ||
@@ -161,3 +179,3 @@ element: Element; | ||
_value: any = undefined; | ||
private _dirty = true; | ||
_pendingValue: any = undefined; | ||
@@ -170,15 +188,16 @@ constructor(element: Element, eventName: string) { | ||
setValue(value: any): void { | ||
value = getValue(this, value); | ||
if (value !== noChange) { | ||
if ((value == null) !== (this._value == null)) { | ||
this._dirty = true; | ||
} | ||
this._value = value; | ||
} | ||
this._pendingValue = value; | ||
} | ||
commit() { | ||
if (this._dirty) { | ||
this._dirty = false; | ||
if (this._value == null) { | ||
while (isDirective(this._pendingValue)) { | ||
const directive = this._pendingValue; | ||
this._pendingValue = noChange; | ||
directive(this); | ||
} | ||
if (this._pendingValue === noChange) { | ||
return; | ||
} | ||
if ((this._pendingValue == null) !== (this._value == null)) { | ||
if (this._pendingValue == null) { | ||
this.element.removeEventListener(this.eventName, this); | ||
@@ -189,2 +208,4 @@ } else { | ||
} | ||
this._value = this._pendingValue; | ||
this._pendingValue = noChange; | ||
} | ||
@@ -191,0 +212,0 @@ |
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
260082
11
3821
0