Comparing version 0.5.0 to 0.6.0-pre.1
10
api.md
@@ -47,4 +47,10 @@ ## API | ||
Specially support value types are `Node`, `Function`, and `TemplateResult`. | ||
Parts are either single-valued or multi-valued. If they have a `size` property they are multi-valued and take an array of values along with an index of where to start reading in the array. | ||
* Method `setValue(value: any): void` | ||
Specially supported value types are `Node`, `Function`, and `TemplateResult`. | ||
* Optional Property `size: number` | ||
* Method `setValue(value: any | any[], startIndex?: number): void` | ||
Sets the value of this part. If the part is multi-value, `value` will be an array, and `startIndex` will be a number. |
@@ -14,8 +14,9 @@ /** | ||
*/ | ||
import { TemplateResult, AttributePart, TemplateInstance, TemplatePart, Part, Template } from '../lit-html.js'; | ||
import { TemplateResult, AttributePart, TemplateInstance, TemplatePart, Part } from '../lit-html.js'; | ||
export { html } from '../lit-html.js'; | ||
/** | ||
* | ||
* @param result Renders a `TemplateResult` to a container using an | ||
* `ExtendedTemplateInstance`, which allows templates to set properties and | ||
* event handlers. | ||
* @param result Renders a `TemplateResult` to a container using the | ||
* `extendedPartCallback` PartCallback, which allows templates to set | ||
* properties and declarative event handlers. | ||
* | ||
@@ -36,16 +37,11 @@ * Properties are set by default, instead of attributes. Attribute names in | ||
* | ||
* To set an event handler, prefix the attribute name with `on-` and use a | ||
* function to return the handler, so that the event handler itself is not | ||
* called as a template directive. | ||
* To set an event handler, prefix the attribute name with `on-`: | ||
* | ||
* Example: | ||
* | ||
* html`<button on-click=${_=> this.onClickHandler}>Buy Now</button>` | ||
* html`<button on-click=${(e)=> this.onClickHandler(e)}>Buy Now</button>` | ||
* | ||
*/ | ||
export declare function render(result: TemplateResult, container: Element | DocumentFragment): void; | ||
export declare class ExtendedTemplateInstance extends TemplateInstance { | ||
_createPart(templatePart: TemplatePart, node: Node): Part; | ||
_createInstance(template: Template): ExtendedTemplateInstance; | ||
} | ||
export declare const extendedPartCallback: (instance: TemplateInstance, templatePart: TemplatePart, node: Node) => Part; | ||
export declare class PropertyPart extends AttributePart { | ||
@@ -52,0 +48,0 @@ setValue(values: any[]): void; |
@@ -14,8 +14,9 @@ /** | ||
*/ | ||
import { AttributePart, TemplateInstance, Part } from '../lit-html.js'; | ||
import { render as baseRender, defaultPartCallback, AttributePart, Part } from '../lit-html.js'; | ||
export { html } from '../lit-html.js'; | ||
/** | ||
* | ||
* @param result Renders a `TemplateResult` to a container using an | ||
* `ExtendedTemplateInstance`, which allows templates to set properties and | ||
* event handlers. | ||
* @param result Renders a `TemplateResult` to a container using the | ||
* `extendedPartCallback` PartCallback, which allows templates to set | ||
* properties and declarative event handlers. | ||
* | ||
@@ -36,47 +37,26 @@ * Properties are set by default, instead of attributes. Attribute names in | ||
* | ||
* To set an event handler, prefix the attribute name with `on-` and use a | ||
* function to return the handler, so that the event handler itself is not | ||
* called as a template directive. | ||
* To set an event handler, prefix the attribute name with `on-`: | ||
* | ||
* Example: | ||
* | ||
* html`<button on-click=${_=> this.onClickHandler}>Buy Now</button>` | ||
* html`<button on-click=${(e)=> this.onClickHandler(e)}>Buy Now</button>` | ||
* | ||
*/ | ||
export function render(result, container) { | ||
let instance = container.__templateInstance; | ||
if (instance !== undefined && | ||
instance.template === result.template && | ||
instance instanceof ExtendedTemplateInstance) { | ||
instance.update(result.values); | ||
return; | ||
} | ||
instance = new ExtendedTemplateInstance(result.template); | ||
container.__templateInstance = instance; | ||
const fragment = instance._clone(); | ||
instance.update(result.values); | ||
while (container.firstChild) { | ||
container.removeChild(container.firstChild); | ||
} | ||
container.appendChild(fragment); | ||
baseRender(result, container, extendedPartCallback); | ||
} | ||
export class ExtendedTemplateInstance extends TemplateInstance { | ||
_createPart(templatePart, node) { | ||
if (templatePart.type === 'attribute') { | ||
if (templatePart.rawName.startsWith('on-')) { | ||
const eventName = templatePart.rawName.substring(3); | ||
return new EventPart(this, node, eventName); | ||
} | ||
if (templatePart.name.endsWith('$')) { | ||
const name = templatePart.name.substring(0, templatePart.name.length - 1); | ||
return new AttributePart(this, node, name, templatePart.strings); | ||
} | ||
return new PropertyPart(this, node, templatePart.rawName, templatePart.strings); | ||
export const extendedPartCallback = (instance, templatePart, node) => { | ||
if (templatePart.type === 'attribute') { | ||
if (templatePart.rawName.startsWith('on-')) { | ||
const eventName = templatePart.rawName.substring(3); | ||
return new EventPart(instance, node, eventName); | ||
} | ||
return super._createPart(templatePart, node); | ||
if (templatePart.name.endsWith('$')) { | ||
const name = templatePart.name.substring(0, templatePart.name.length - 1); | ||
return new AttributePart(instance, node, name, templatePart.strings); | ||
} | ||
return new PropertyPart(instance, node, templatePart.rawName, templatePart.strings); | ||
} | ||
_createInstance(template) { | ||
return new ExtendedTemplateInstance(template); | ||
} | ||
} | ||
return defaultPartCallback(instance, templatePart, node); | ||
}; | ||
export class PropertyPart extends AttributePart { | ||
@@ -83,0 +63,0 @@ setValue(values) { |
@@ -14,7 +14,6 @@ /** | ||
*/ | ||
import { NodePart } from "../lit-html.js"; | ||
import { DirectiveFn } from '../lit-html.js'; | ||
export declare type KeyFn<T> = (item: T) => any; | ||
export declare type ItemTemplate<T> = (item: T, index: number) => any; | ||
export declare type RepeatResult = (part: NodePart) => any; | ||
export declare function repeat<T>(items: T[], keyFn: KeyFn<T>, template: ItemTemplate<T>): RepeatResult; | ||
export declare function repeat<T>(items: T[], template: ItemTemplate<T>): RepeatResult; | ||
export declare function repeat<T>(items: T[], keyFn: KeyFn<T>, template: ItemTemplate<T>): DirectiveFn; | ||
export declare function repeat<T>(items: T[], template: ItemTemplate<T>): DirectiveFn; |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { NodePart } from "../lit-html.js"; | ||
import { directive, NodePart } from '../lit-html.js'; | ||
const stateCache = new WeakMap(); | ||
@@ -25,3 +25,3 @@ export function repeat(items, keyFnOrTemplate, template) { | ||
} | ||
return (part) => { | ||
return directive((part) => { | ||
let state = stateCache.get(part); | ||
@@ -133,4 +133,4 @@ if (state === undefined) { | ||
state.parts = itemParts; | ||
}; | ||
}); | ||
} | ||
//# sourceMappingURL=repeat.js.map |
@@ -14,6 +14,6 @@ /** | ||
*/ | ||
import { NodePart } from "../lit-html.js"; | ||
import { NodePart } from '../lit-html.js'; | ||
/** | ||
* Display `defaultContent` until `promise` resolves. | ||
*/ | ||
export declare function until(promise: Promise<any>, defaultContent: any): (part: NodePart) => Promise<void>; | ||
export declare const until: (promise: Promise<any>, defaultContent: any) => (part: NodePart) => Promise<any>; |
@@ -14,11 +14,10 @@ /** | ||
*/ | ||
import { directive } from '../lit-html.js'; | ||
/** | ||
* Display `defaultContent` until `promise` resolves. | ||
*/ | ||
export function until(promise, defaultContent) { | ||
return async function (part) { | ||
part.setValue(defaultContent); | ||
part.setValue(await promise); | ||
}; | ||
} | ||
export const until = (promise, defaultContent) => directive((part) => { | ||
part.setValue(defaultContent); | ||
return promise; | ||
}); | ||
//# sourceMappingURL=until.js.map |
@@ -21,3 +21,3 @@ /** | ||
*/ | ||
export declare function render(result: TemplateResult, container: Element | DocumentFragment): void; | ||
export declare function render(result: TemplateResult, container: Element | DocumentFragment, partCallback?: PartCallback): void; | ||
/** | ||
@@ -41,16 +41,16 @@ * A placeholder for a dynamic expression in an HTML template. | ||
type: string; | ||
index: number; | ||
path: number[]; | ||
name: string | undefined; | ||
rawName: string | undefined; | ||
strings: string[] | undefined; | ||
constructor(type: string, index: number, name?: string | undefined, rawName?: string | undefined, strings?: string[] | undefined); | ||
constructor(type: string, path: number[], name?: string | undefined, rawName?: string | undefined, strings?: string[] | undefined); | ||
} | ||
export declare class Template { | ||
private _strings; | ||
parts: TemplatePart[]; | ||
element: HTMLTemplateElement; | ||
constructor(strings: TemplateStringsArray); | ||
private _parse(); | ||
private _getTemplateHtml(strings); | ||
private _getHtml(strings); | ||
} | ||
export declare type DirectiveFn = (part: Part) => any; | ||
export declare const directive: <F extends DirectiveFn>(f: F) => F; | ||
export declare abstract class Part { | ||
@@ -60,14 +60,19 @@ instance: TemplateInstance; | ||
constructor(instance: TemplateInstance); | ||
abstract setValue(value: any): void; | ||
protected _getValue(value: any): any; | ||
} | ||
export declare class AttributePart extends Part { | ||
export interface SinglePart extends Part { | ||
setValue(value: any): void; | ||
} | ||
export interface MultiPart extends Part { | ||
setValue(values: any[], startIndex: number): void; | ||
} | ||
export declare class AttributePart extends Part implements MultiPart { | ||
element: Element; | ||
name: string; | ||
strings: string[]; | ||
size: number; | ||
constructor(instance: TemplateInstance, element: Element, name: string, strings: string[]); | ||
setValue(values: any[]): void; | ||
readonly size: number; | ||
setValue(values: any[], startIndex: number): void; | ||
} | ||
export declare class NodePart extends Part { | ||
export declare class NodePart extends Part implements SinglePart { | ||
startNode: Node; | ||
@@ -78,26 +83,23 @@ endNode: Node; | ||
setValue(value: any): void; | ||
private _insertNodeBeforeEndNode(node); | ||
private _setNodeValue(value); | ||
private _setTextValue(value); | ||
private _setTemplateResultValue(value); | ||
private _setIterableValue(value); | ||
private _insert(node); | ||
private _setNode(value); | ||
private _setText(value); | ||
private _setTemplateResult(value); | ||
private _setIterable(value); | ||
protected _setPromise(value: Promise<any>): void; | ||
clear(startNode?: Node): void; | ||
} | ||
export declare type PartCallback = (instance: TemplateInstance, templatePart: TemplatePart, node: Node) => Part; | ||
export declare const defaultPartCallback: (instance: TemplateInstance, templatePart: TemplatePart, node: Node) => Part; | ||
/** | ||
* An instance of a `Template` that can be attached to the DOM and updated | ||
* with new values. | ||
*/ | ||
export declare class TemplateInstance { | ||
_template: Template; | ||
_parts: Part[]; | ||
startNode: Node; | ||
endNode: Node; | ||
constructor(template: Template); | ||
readonly template: Template; | ||
_partCallback: PartCallback; | ||
template: Template; | ||
constructor(template: Template, partCallback?: PartCallback); | ||
update(values: any[]): void; | ||
_clone(): DocumentFragment; | ||
_createPart(templatePart: TemplatePart, node: Node): Part; | ||
_createInstance(template: Template): TemplateInstance; | ||
} | ||
declare global { | ||
interface Node { | ||
__templateInstance?: TemplateInstance; | ||
} | ||
} | ||
export {}; |
446
lit-html.js
@@ -46,11 +46,13 @@ /** | ||
*/ | ||
export function render(result, container) { | ||
export function render(result, container, partCallback = defaultPartCallback) { | ||
let instance = container.__templateInstance; | ||
// Repeat render, just call update() | ||
if (instance !== undefined && | ||
instance.template === result.template && | ||
instance instanceof TemplateInstance) { | ||
instance._partCallback === partCallback) { | ||
instance.update(result.values); | ||
return; | ||
} | ||
instance = new TemplateInstance(result.template); | ||
// First render, create a new TemplateInstance and append it | ||
instance = new TemplateInstance(result.template, partCallback); | ||
container.__templateInstance = instance; | ||
@@ -82,5 +84,5 @@ const fragment = instance._clone(); | ||
export class TemplatePart { | ||
constructor(type, index, name, rawName, strings) { | ||
constructor(type, path, name, rawName, strings) { | ||
this.type = type; | ||
this.index = index; | ||
this.path = path; | ||
this.name = name; | ||
@@ -94,62 +96,84 @@ this.rawName = rawName; | ||
this.parts = []; | ||
this._strings = strings; | ||
this._parse(); | ||
} | ||
_parse() { | ||
this.element = document.createElement('template'); | ||
this.element.innerHTML = this._getTemplateHtml(this._strings); | ||
const walker = document.createTreeWalker(this.element.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); | ||
let index = -1; | ||
let partIndex = 0; | ||
this.element.innerHTML = this._getHtml(strings); | ||
const nodesToRemove = []; | ||
const attributesToRemove = []; | ||
while (walker.nextNode()) { | ||
index++; | ||
const node = walker.currentNode; | ||
if (node.nodeType === Node.ELEMENT_NODE) { | ||
const attributes = node.attributes; | ||
for (let i = 0; i < attributes.length; i++) { | ||
const attribute = attributes.item(i); | ||
const value = attribute.value; | ||
const strings = value.split(exprMarker); | ||
if (strings.length > 1) { | ||
const attributeString = this._strings[partIndex]; | ||
// Trim the trailing literal value if this is an interpolation | ||
const rawNameString = attributeString.substring(0, attributeString.length - strings[0].length); | ||
const match = rawNameString.match(/((?:\w|[.\-_$])+)=["']?$/); | ||
const rawName = match[1]; | ||
this.parts.push(new TemplatePart('attribute', index, attribute.name, rawName, strings)); | ||
attributesToRemove.push(attribute); | ||
partIndex += strings.length - 1; | ||
// The current location in the DOM tree, as an array of child indices | ||
const currentPath = []; | ||
// What expression we're currently handling | ||
let expressionIndex = 0; | ||
/* | ||
* This populates the parts array by traversing the template with a | ||
* recursive DFS, and giving each part a path of indices from the root of | ||
* the template to the target node. | ||
*/ | ||
const findParts = (node, index) => { | ||
currentPath.push(index); | ||
let size = 1; | ||
if (node.nodeType === 3 /* TEXT_NODE */) { | ||
const value = node.nodeValue; | ||
const strings = value.split(exprMarker); | ||
if (strings.length > 1) { | ||
const parent = node.parentNode; | ||
size = strings.length; | ||
const lastIndex = size - 1; | ||
// We have a part for each match found | ||
expressionIndex += lastIndex; | ||
// We keep this current node, but reset its content to the last | ||
// literal part. We insert new literal nodes before this so that the | ||
// tree walker keeps its position correctly. | ||
node.textContent = strings[lastIndex]; | ||
// Generate a new text node for each literal section | ||
// These nodes are also used as the markers for node parts | ||
for (let i = 0; i < lastIndex; i++) { | ||
parent.insertBefore(new Text(strings[i]), node); | ||
this.parts.push(new TemplatePart('node', currentPath.slice(1))); | ||
// Increment the last index on the stack because we just created a | ||
// new text node | ||
currentPath[currentPath.length - 1] += 1; | ||
} | ||
} | ||
else if (value.trim() === '') { | ||
nodesToRemove.push(node); | ||
size = 0; | ||
} | ||
} | ||
else if (node.nodeType === Node.TEXT_NODE) { | ||
const strings = node.nodeValue.split(exprMarker); | ||
if (strings.length > 1) { | ||
// Generate a new text node for each literal and two for each part, | ||
// a start and end | ||
partIndex += strings.length - 1; | ||
for (let i = 0; i < strings.length; i++) { | ||
const string = strings[i]; | ||
const literalNode = new Text(string); | ||
node.parentNode.insertBefore(literalNode, node); | ||
index++; | ||
if (i < strings.length - 1) { | ||
node.parentNode.insertBefore(new Text(), node); | ||
node.parentNode.insertBefore(new Text(), node); | ||
this.parts.push(new TemplatePart('node', index)); | ||
index += 2; | ||
else { | ||
if (node.nodeType === 1 && node.hasAttributes()) { | ||
const attributes = node.attributes; | ||
for (let i = 0; i < attributes.length; i++) { | ||
const attribute = attributes.item(i); | ||
const value = attribute.value; | ||
// Look for expression markers | ||
const attributeStrings = value.split(exprMarker); | ||
if (attributeStrings.length > 1) { | ||
// Get the template string that preced this attribute expression | ||
const attributeString = strings[expressionIndex]; | ||
// Trim the trailing literal part of the attribute value if this | ||
// is an interpolation | ||
const rawNameString = attributeString.substring(0, attributeString.length - attributeStrings[0].length); | ||
// Extract the attribute name | ||
const match = rawNameString.match(/((?:\w|[.\-_$])+)=["']?$/); | ||
const rawName = match[1]; | ||
this.parts.push(new TemplatePart('attribute', currentPath.slice(1), attribute.name, rawName, attributeStrings)); | ||
attributesToRemove.push(attribute); | ||
expressionIndex += attributeStrings.length - 1; | ||
} | ||
} | ||
index--; | ||
nodesToRemove.push(node); | ||
} | ||
else if (!node.nodeValue.trim()) { | ||
nodesToRemove.push(node); | ||
index--; | ||
if (node.hasChildNodes()) { | ||
let child = node.firstChild; | ||
let i = 0; | ||
while (child !== null) { | ||
i += findParts(child, i); | ||
child = child.nextSibling; | ||
} | ||
} | ||
} | ||
} | ||
// Remove text binding nodes after the walk to not disturb the TreeWalker | ||
currentPath.pop(); | ||
return size; | ||
}; | ||
findParts(this.element.content, -1); | ||
// Remove empty text nodes and attributes after the walk so as to not | ||
// disturb the traversal | ||
for (const n of nodesToRemove) { | ||
@@ -162,3 +186,3 @@ n.parentNode.removeChild(n); | ||
} | ||
_getTemplateHtml(strings) { | ||
_getHtml(strings) { | ||
const parts = []; | ||
@@ -174,2 +198,6 @@ for (let i = 0; i < strings.length; i++) { | ||
} | ||
export const directive = (f) => { | ||
f.__litDirective = true; | ||
return f; | ||
}; | ||
export class Part { | ||
@@ -180,16 +208,8 @@ constructor(instance) { | ||
_getValue(value) { | ||
if (typeof value === 'function') { | ||
try { | ||
value = value(this); | ||
} | ||
catch (e) { | ||
console.error(e); | ||
return; | ||
} | ||
// `null` as the value of a Text node will render the string 'null' | ||
// so we convert it to undefined | ||
if (typeof value === 'function' && value.__litDirective === true) { | ||
value = value(this); | ||
} | ||
if (value === null) { | ||
// `null` as the value of Text node will render the string 'null' | ||
return undefined; | ||
} | ||
return value; | ||
return value === null ? undefined : value; | ||
} | ||
@@ -200,8 +220,8 @@ } | ||
super(instance); | ||
console.assert(element.nodeType === Node.ELEMENT_NODE); | ||
this.element = element; | ||
this.name = name; | ||
this.strings = strings; | ||
this.size = strings.length - 1; | ||
} | ||
setValue(values) { | ||
setValue(values, startIndex) { | ||
const strings = this.strings; | ||
@@ -212,4 +232,4 @@ let text = ''; | ||
if (i < strings.length - 1) { | ||
const v = this._getValue(values[i]); | ||
if (v && typeof v !== 'string' && v[Symbol.iterator]) { | ||
const v = this._getValue(values[startIndex + i]); | ||
if (v && (Array.isArray(v) || typeof v !== 'string' && v[Symbol.iterator])) { | ||
for (const t of v) { | ||
@@ -227,5 +247,2 @@ // TODO: we need to recursively call getValue into iterables... | ||
} | ||
get size() { | ||
return this.strings.length - 1; | ||
} | ||
} | ||
@@ -240,107 +257,114 @@ export class NodePart extends Part { | ||
value = this._getValue(value); | ||
if (value instanceof Node) { | ||
this._previousValue = this._setNodeValue(value); | ||
if (value === null || | ||
!(typeof value === 'object' || typeof value === 'function')) { | ||
// Handle primitive values | ||
// If the value didn't change, do nothing | ||
if (value === this._previousValue) { | ||
return; | ||
} | ||
this._setText(value); | ||
} | ||
else if (value instanceof TemplateResult) { | ||
this._previousValue = this._setTemplateResultValue(value); | ||
this._setTemplateResult(value); | ||
} | ||
else if (value && value.then !== undefined) { | ||
value.then((v) => { | ||
if (this._previousValue === value) { | ||
this.setValue(v); | ||
} | ||
}); | ||
this._previousValue = value; | ||
else if (Array.isArray(value) || value[Symbol.iterator]) { | ||
this._setIterable(value); | ||
} | ||
else if (value && typeof value !== 'string' && value[Symbol.iterator]) { | ||
this._previousValue = this._setIterableValue(value); | ||
else if (value instanceof Node) { | ||
this._setNode(value); | ||
} | ||
else if (this.startNode.nextSibling === this.endNode.previousSibling && | ||
this.startNode.nextSibling.nodeType === Node.TEXT_NODE) { | ||
this.startNode.nextSibling.textContent = value; | ||
this._previousValue = value; | ||
else if (value.then !== undefined) { | ||
this._setPromise(value); | ||
} | ||
else { | ||
this._previousValue = this._setTextValue(value); | ||
// Fallback, will render the string representation | ||
this._setText(value); | ||
} | ||
} | ||
_insertNodeBeforeEndNode(node) { | ||
_insert(node) { | ||
this.endNode.parentNode.insertBefore(node, this.endNode); | ||
} | ||
_setNodeValue(value) { | ||
_setNode(value) { | ||
this.clear(); | ||
this._insertNodeBeforeEndNode(value); | ||
return value; | ||
this._insert(value); | ||
this._previousValue = value; | ||
} | ||
_setTextValue(value) { | ||
return this._setNodeValue(new Text(value)); | ||
_setText(value) { | ||
if (this.startNode.nextSibling === this.endNode.previousSibling && | ||
this.startNode.nextSibling.nodeType === Node.TEXT_NODE) { | ||
// If we only have a single text node between the markers, we can just | ||
// set its value, rather than replacing it. | ||
// TODO(justinfagnani): Can we just check if _previousValue is | ||
// primitive? | ||
this.startNode.nextSibling.textContent = value; | ||
} | ||
else { | ||
this._setNode(new Text(value)); | ||
} | ||
this._previousValue = value; | ||
} | ||
_setTemplateResultValue(value) { | ||
_setTemplateResult(value) { | ||
let instance; | ||
if (this._previousValue && this._previousValue._template === value.template) { | ||
if (this._previousValue && this._previousValue.template === value.template) { | ||
instance = this._previousValue; | ||
} | ||
else { | ||
instance = this.instance._createInstance(value.template); | ||
this._setNodeValue(instance._clone()); | ||
instance = new TemplateInstance(value.template, this.instance._partCallback); | ||
this._setNode(instance._clone()); | ||
this._previousValue = instance; | ||
} | ||
instance.update(value.values); | ||
return instance; | ||
} | ||
_setIterableValue(value) { | ||
_setIterable(value) { | ||
// For an Iterable, we create a new InstancePart per item, then set its | ||
// value to the item. This is a little bit of overhead for every item in | ||
// an Iterable, but it lets us recurse easily and update Arrays of | ||
// TemplateResults that will be commonly returned from expressions like: | ||
// array.map((i) => html`${i}`) | ||
// We reuse this parts startNode as the first part's startNode, and this | ||
// parts endNode as the last part's endNode. | ||
let itemStart = this.startNode; | ||
let itemEnd; | ||
const values = value[Symbol.iterator](); | ||
const previousParts = Array.isArray(this._previousValue) ? | ||
this._previousValue : undefined; | ||
let previousPartsIndex = 0; | ||
const itemParts = []; | ||
let current = values.next(); | ||
let next = values.next(); | ||
if (current.done) { | ||
// Empty iterable, just clear | ||
// an Iterable, but it lets us recurse easily and efficiently update Arrays | ||
// of TemplateResults that will be commonly returned from expressions like: | ||
// array.map((i) => html`${i}`), by reusing existing TemplateInstances. | ||
if (!Array.isArray(this._previousValue)) { | ||
this.clear(); | ||
this._previousValue = []; | ||
} | ||
while (!current.done) { | ||
// Reuse a previous part if we can, otherwise create a new one | ||
let itemPart; | ||
if (previousParts !== undefined && previousPartsIndex < previousParts.length) { | ||
itemPart = previousParts[previousPartsIndex++]; | ||
if (next.done && itemPart.endNode !== this.endNode) { | ||
// Since this is the last part we'll use, set it's endNode to the | ||
// container's endNode. Setting the value of this part will clean | ||
// up any residual nodes from a previously longer iterable. | ||
// Remove previousSibling, since we want itemPart.endNode to be | ||
// removed as part of the clear operation. | ||
this.clear(itemPart.endNode.previousSibling); | ||
itemPart.endNode = this.endNode; | ||
// Lets of keep track of how many items we stamped so we can clear leftover | ||
// items from a previous render | ||
const itemParts = this._previousValue; | ||
let partIndex = 0; | ||
for (const item of value) { | ||
// Try to reuse an existing part | ||
let 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 = new Text(); | ||
this._insert(itemStart); | ||
} | ||
itemEnd = itemPart.endNode; | ||
itemPart = new NodePart(this.instance, itemStart, this.endNode); | ||
itemParts.push(itemPart); | ||
} | ||
else { | ||
if (next.done) { | ||
// on the last item, reuse this part's endNode | ||
itemEnd = this.endNode; | ||
} | ||
else { | ||
itemEnd = new Text(); | ||
this._insertNodeBeforeEndNode(itemEnd); | ||
} | ||
itemPart = new NodePart(this.instance, itemStart, itemEnd); | ||
} | ||
itemPart.setValue(current.value); | ||
itemParts.push(itemPart); | ||
current = next; | ||
next = values.next(); | ||
itemStart = itemEnd; | ||
itemPart.setValue(item); | ||
partIndex++; | ||
} | ||
return itemParts; | ||
if (partIndex === 0) { | ||
this.clear(); | ||
} | ||
else if (partIndex < itemParts.length) { | ||
const lastPart = itemParts[partIndex - 1]; | ||
this.clear(lastPart.endNode.previousSibling); | ||
lastPart.endNode = this.endNode; | ||
} | ||
} | ||
_setPromise(value) { | ||
value.then((v) => { | ||
if (this._previousValue === value) { | ||
this.setValue(v); | ||
} | ||
}); | ||
this._previousValue = value; | ||
} | ||
clear(startNode = this.startNode) { | ||
@@ -356,10 +380,21 @@ this._previousValue = undefined; | ||
} | ||
export const defaultPartCallback = (instance, templatePart, node) => { | ||
if (templatePart.type === 'attribute') { | ||
return new AttributePart(instance, node, templatePart.name, templatePart.strings); | ||
} | ||
else if (templatePart.type === 'node') { | ||
return new NodePart(instance, node, node.nextSibling); | ||
} | ||
throw new Error(`Unknown part type ${templatePart.type}`); | ||
}; | ||
/** | ||
* An instance of a `Template` that can be attached to the DOM and updated | ||
* with new values. | ||
*/ | ||
export class TemplateInstance { | ||
constructor(template) { | ||
constructor(template, partCallback = defaultPartCallback) { | ||
this._parts = []; | ||
this._template = template; | ||
this.template = template; | ||
this._partCallback = partCallback; | ||
} | ||
get template() { | ||
return this._template; | ||
} | ||
update(values) { | ||
@@ -369,6 +404,7 @@ let valueIndex = 0; | ||
if (part.size === undefined) { | ||
part.setValue(values[valueIndex++]); | ||
part.setValue(values[valueIndex]); | ||
valueIndex++; | ||
} | ||
else { | ||
part.setValue(values.slice(valueIndex, valueIndex + part.size)); | ||
part.setValue(values, valueIndex); | ||
valueIndex += part.size; | ||
@@ -379,38 +415,74 @@ } | ||
_clone() { | ||
const fragment = document.importNode(this._template.element.content, true); | ||
if (this._template.parts.length > 0) { | ||
const walker = document.createTreeWalker(fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); | ||
const parts = this._template.parts; | ||
let index = 0; | ||
let partIndex = 0; | ||
let templatePart = parts[0]; | ||
let node = walker.nextNode(); | ||
while (node != null && partIndex < parts.length) { | ||
if (index === templatePart.index) { | ||
this._parts.push(this._createPart(templatePart, node)); | ||
templatePart = parts[++partIndex]; | ||
const fragment = document.importNode(this.template.element.content, true); | ||
/* | ||
* This implements a search that traverses the minimum number of Nodes in a | ||
* DOM tree while only using Node.firstChild and Node.nextSibling, which | ||
* have been measured to be faster than accessing Node.childNodes. | ||
* | ||
* For any given path of childNode indices starting from the root of the | ||
* template, we recursively find the parent, then call nextSibling until | ||
* we get to the target index. | ||
* | ||
* Once found, we cache the index and node, so that the next lookup can be | ||
* faster by: | ||
* | ||
* 1. Finding the common ancestor of the previously searched path | ||
* 2. Starting a traversal of children from that common ancestor from the | ||
* last node found, rather than firstChild | ||
* | ||
* This means that any node is only ever visited once. | ||
* | ||
* In order for this to work, paths much be searched for in depth-first | ||
* order. | ||
* | ||
* The overhead of these optimizations probably only matters for larger, | ||
* more complex templates, with enough bindings to speard search costs | ||
* across them, and the benefit will not show up on micro-bencharks with | ||
* small templates. However, it doesn't seem like this slows down | ||
* micro-benchmarks. | ||
*/ | ||
let nodeStack = []; | ||
const findNodeAtPath = (path, depth) => { | ||
// Recurse up the tree to find the parent | ||
const parent = (depth === 0) ? fragment : findNodeAtPath(path, depth - 1); | ||
// The target index we're searching for at this depth | ||
const targetIndex = path[depth]; | ||
let currentIndex; | ||
let node; | ||
if (nodeStack.length > depth) { | ||
// If we've cached up to this depth, and the index in the stack at this | ||
// depth equals the targetIndex, then just return from the stack. | ||
if (nodeStack[depth][0] === targetIndex) { | ||
return nodeStack[depth][1]; | ||
} | ||
else { | ||
index++; | ||
node = walker.nextNode(); | ||
// Otherwise, start the search at the last index we used at this level | ||
[currentIndex, node] = nodeStack[depth]; | ||
nodeStack = nodeStack.slice(0, depth); | ||
} | ||
else { | ||
// If the stack didn't have anything at this depth, initialize the search | ||
// to the first child | ||
currentIndex = 0; | ||
node = parent.firstChild; | ||
} | ||
// Perform the traversal | ||
while (node !== null) { | ||
if (currentIndex === targetIndex) { | ||
// When we have a hit, cache it | ||
nodeStack.push([currentIndex, node]); | ||
return node; | ||
} | ||
node = node.nextSibling; | ||
currentIndex++; | ||
} | ||
// This should never happen | ||
return; | ||
}; | ||
for (const p of this.template.parts) { | ||
const node = findNodeAtPath(p.path, p.path.length - 1); | ||
this._parts.push(this._partCallback(this, p, node)); | ||
} | ||
return fragment; | ||
} | ||
_createPart(templatePart, node) { | ||
if (templatePart.type === 'attribute') { | ||
return new AttributePart(this, node, templatePart.name, templatePart.strings); | ||
} | ||
else if (templatePart.type === 'node') { | ||
return new NodePart(this, node, node.nextSibling); | ||
} | ||
else { | ||
throw new Error(`unknown part type: ${templatePart.type}`); | ||
} | ||
} | ||
_createInstance(template) { | ||
return new TemplateInstance(template); | ||
} | ||
} | ||
//# sourceMappingURL=lit-html.js.map |
{ | ||
"name": "lit-html", | ||
"version": "0.5.0", | ||
"version": "0.6.0-pre.1", | ||
"description": "HTML template literals in JavaScript", | ||
@@ -14,3 +14,7 @@ "license": "BSD-3-Clause", | ||
"build": "tsc", | ||
"test": "echo \"run polyserve --npm and open /test/index.html a browser with module support\"" | ||
"gen-docs": "typedoc --readme none --mode modules --excludeNotExported --excludePrivate --exclude **/*_test.ts --out ./docs/api .", | ||
"pretest": "npm run posttest; ln -s node_modules bower_components", | ||
"test": "wct -l chrome", | ||
"posttest": "rm -f bower_components", | ||
"checksize": "uglifyjs lit-html.js -mc --toplevel | gzip -9 | wc -c" | ||
}, | ||
@@ -23,5 +27,9 @@ "author": "The Polymer Authors", | ||
"mocha": "^3.4.2", | ||
"typescript": "^2.4.1" | ||
"typedoc": "^0.8.0", | ||
"typescript": "^2.4.1", | ||
"uglify-es": "^3.0.27", | ||
"web-component-tester": "^6.0.1" | ||
}, | ||
"typings": "lit-html.d.ts" | ||
"typings": "lit-html.d.ts", | ||
"dependencies": {} | ||
} |
# lit-html | ||
HTML templates, via JavaScript template literals | ||
[![Build Status](https://travis-ci.org/PolymerLabs/lit-html.svg?branch=master)](https://travis-ci.org/PolymerLabs/lit-html) | ||
## Overview | ||
@@ -15,3 +17,3 @@ | ||
`lit-html` can be used standalone and directly to help manage some DOM: | ||
`lit-html` can be used standalone and directly to help manage DOM: | ||
```javascript | ||
@@ -27,3 +29,3 @@ const helloTemplate = (name) => html`<div>Hello ${name}!</div>`; | ||
But may also be common to use `lit-html` with a component system that calls `render()` for you, similar to React components: | ||
But it may also be common to use `lit-html` with a component system that calls `render()` for you, similar to React components: | ||
@@ -34,3 +36,3 @@ _(this example uses JS Class Fields, an upcoming specification)_ | ||
static observedProperties = ['message', 'name']; | ||
static observedProperties = ['title', 'body']; | ||
@@ -56,3 +58,3 @@ title = `About lit-html`; | ||
1. Efficient updates of previously rendered DOM. | ||
2. Expressiveness and easy access the JavaScript state that needs to be injected into DOM. | ||
2. Expressiveness and easy access to the JavaScript state that needs to be injected into DOM. | ||
3. Standard JavaScript without required build steps, understandable by standards-compliant tools. | ||
@@ -63,3 +65,3 @@ 4. Very small size. | ||
`lit-html` utilizes some unique properties of HTML `<template>` elements and JavaScript tmplate literals. So it's helpful to understand them first. | ||
`lit-html` utilizes some unique properties of HTML `<template>` elements and JavaScript template literals. So it's helpful to understand them first. | ||
@@ -74,3 +76,3 @@ ### Tagged Template Literals | ||
A _tagged_ template literal is preceded by a special template tag function: | ||
A _tagged_ template literal is prefixed with a special template tag function: | ||
@@ -92,3 +94,3 @@ ```javascript | ||
The first time `html` is called on a particular template literal it does one-time setup work to create the template. It joins all the string parts with a special placeholder, `"{{}}"`, then creates a `<template>` and sets its `innherHTML` to the result. The it walks the template's DOM and extracts the placeholder and remembers their location. | ||
The first time `html` is called on a particular template literal it does one-time setup work to create the template. It joins all the string parts with a special placeholder, `"{{}}"`, then creates a `<template>` and sets its `innerHTML` to the result. The it walks the template's DOM and extracts the placeholder and remembers their location. | ||
@@ -103,3 +105,3 @@ Every call to `html` returns a `TemplateResult` which contains the template created on the first call, and the expression values for the current call. | ||
Rendering can be customized by providing alternate `render()` implementations whcih create different kinds of `TemplateInstances` and `Part`s, like `PropertyPart` and `EventPart` included in `lib/lit-extended.ts` which let templates set properties and event handlers on elements. | ||
Rendering can be customized by providing alternate `render()` implementations which create different kinds of `TemplateInstances` and `Part`s, like `PropertyPart` and `EventPart` included in `lib/lit-extended.ts` which let templates set properties and event handlers on elements. | ||
@@ -110,3 +112,3 @@ ## Performance | ||
* It utilizies the built-in JS and HTML parsers - it doesn't include any expression or markup parser of it's own. | ||
* It utilizes the built-in JS and HTML parsers - it doesn't include any expression or markup parser of it's own. | ||
* It only updates the dynamic parts of templates - static parts are untouched, not even walked for diffing, after the initial render. | ||
@@ -174,21 +176,42 @@ * It uses cloning for initial render. | ||
### Function Values / Directives | ||
### Directives | ||
A function valued expression can be used for error handling and stateful rendering. | ||
Directives are functions that can extend lit-html by directly interacting with the Part API. | ||
If an expression returns a function, the function is called with the `Part` its populating, inside a try/catch block. | ||
Directives will usually be created from factory functions that accept some arguments for values and configuration. Directives are created by passing a function to lit-html's `directive()` function: | ||
This makes it safe from exceptions: | ||
```javascript | ||
html`<div>${directive((part) => { part.setValue('Hello')})}</div>` | ||
``` | ||
The `part` argument is a `Part` object with an API for directly managing the dynamic DOM associated with expressions. See the `Part` API in api.md. | ||
Here's an example of a directive that takes a function, and evaluates it in a try/catch to implement exception safe expressions: | ||
```javascript | ||
const safe = (f) => directive((part) => { | ||
try { | ||
return f(); | ||
} catch (e) { | ||
console.error(e); | ||
} | ||
}); | ||
``` | ||
Now `safe()` can be used to wrap a function: | ||
```javascript | ||
let data; | ||
const render = () => html`foo = ${_=>data.foo}`; | ||
const render = () => html`foo = ${safe(_=>data.foo)}`; | ||
``` | ||
Here, `data.foo` throws because `data` is undefined, but the rest of the template renders. | ||
This example increments a counter on every render: | ||
And is a useful extension point: | ||
```javascript | ||
const render = () => html` | ||
<div> | ||
${directive((part) => part.setValue((part.previousValue + 1) || 0))} | ||
</div>`; | ||
``` | ||
const render = () => html`<div>${(part) => part.setValue((part.previousValue + 1) || 0)}</div>`; | ||
lit-html includes a few directives: | ||
@@ -258,3 +281,3 @@ | ||
* Much more test coverage is needed for complex templates, especially template composition and Function and Iterable values. | ||
* It has not been benchmarked thouroughly yet. | ||
* It has not been benchmarked thoroughly yet. | ||
* The API may change. | ||
@@ -350,3 +373,3 @@ | ||
Since all expressions in a template literal are evaluated when the literal is evaluated, you may want to only evaluate some expensive expressions when certain other values (probably it's dependencies change). `Guard` would memoize function and only call it if the guard expression changed. | ||
Since all expressions in a template literal are evaluated when the literal is evaluated, you may want to only evaluate some expensive expressions when certain other values (probably it's dependencies change). `Guard` would memoize the function and only call it if the guard expression changed. | ||
@@ -353,0 +376,0 @@ Example: |
@@ -15,9 +15,9 @@ /** | ||
import { TemplateResult, AttributePart, TemplateInstance, TemplatePart, Part, Template } from '../lit-html.js'; | ||
import { render as baseRender, defaultPartCallback, TemplateResult, AttributePart, TemplateInstance, TemplatePart, Part } from '../lit-html.js'; | ||
export { html } from '../lit-html.js'; | ||
/** | ||
* | ||
* @param result Renders a `TemplateResult` to a container using an | ||
* `ExtendedTemplateInstance`, which allows templates to set properties and | ||
* event handlers. | ||
* @param result Renders a `TemplateResult` to a container using the | ||
* `extendedPartCallback` PartCallback, which allows templates to set | ||
* properties and declarative event handlers. | ||
* | ||
@@ -38,53 +38,28 @@ * Properties are set by default, instead of attributes. Attribute names in | ||
* | ||
* To set an event handler, prefix the attribute name with `on-` and use a | ||
* function to return the handler, so that the event handler itself is not | ||
* called as a template directive. | ||
* To set an event handler, prefix the attribute name with `on-`: | ||
* | ||
* Example: | ||
* | ||
* html`<button on-click=${_=> this.onClickHandler}>Buy Now</button>` | ||
* html`<button on-click=${(e)=> this.onClickHandler(e)}>Buy Now</button>` | ||
* | ||
*/ | ||
export function render(result: TemplateResult, container: Element|DocumentFragment) { | ||
let instance = container.__templateInstance as any; | ||
if (instance !== undefined && | ||
instance.template === result.template && | ||
instance instanceof ExtendedTemplateInstance) { | ||
instance.update(result.values); | ||
return; | ||
} | ||
instance = new ExtendedTemplateInstance(result.template); | ||
container.__templateInstance = instance; | ||
const fragment = instance._clone(); | ||
instance.update(result.values); | ||
while (container.firstChild) { | ||
container.removeChild(container.firstChild); | ||
} | ||
container.appendChild(fragment); | ||
baseRender(result, container, extendedPartCallback); | ||
} | ||
export class ExtendedTemplateInstance extends TemplateInstance { | ||
_createPart(templatePart: TemplatePart, node: Node): Part { | ||
if (templatePart.type === 'attribute') { | ||
if (templatePart.rawName!.startsWith('on-')) { | ||
const eventName = templatePart.rawName!.substring(3); | ||
return new EventPart(this, node as Element, eventName); | ||
} | ||
if (templatePart.name!.endsWith('$')) { | ||
const name = templatePart.name!.substring(0, templatePart.name!.length - 1); | ||
return new AttributePart(this, node as Element, name, templatePart.strings!); | ||
} | ||
return new PropertyPart(this, node as Element, templatePart.rawName!, templatePart.strings!); | ||
export const extendedPartCallback = (instance: TemplateInstance, templatePart: TemplatePart, node: Node): Part => { | ||
if (templatePart.type === 'attribute') { | ||
if (templatePart.rawName!.startsWith('on-')) { | ||
const eventName = templatePart.rawName!.substring(3); | ||
return new EventPart(instance, node as Element, eventName); | ||
} | ||
return super._createPart(templatePart, node); | ||
if (templatePart.name!.endsWith('$')) { | ||
const name = templatePart.name!.substring(0, templatePart.name!.length - 1); | ||
return new AttributePart(instance, node as Element, name, templatePart.strings!); | ||
} | ||
return new PropertyPart(instance, node as Element, templatePart.rawName!, templatePart.strings!); | ||
} | ||
return defaultPartCallback(instance, templatePart, node); | ||
}; | ||
_createInstance(template: Template) { | ||
return new ExtendedTemplateInstance(template); | ||
} | ||
} | ||
@@ -91,0 +66,0 @@ export class PropertyPart extends AttributePart { |
@@ -15,7 +15,6 @@ /** | ||
import { NodePart } from "../lit-html.js"; | ||
import {directive, DirectiveFn, NodePart} from '../lit-html.js'; | ||
export type KeyFn<T> = (item: T) => any; | ||
export type ItemTemplate<T> = (item: T, index: number) => any; | ||
export type RepeatResult = (part: NodePart) => any; | ||
@@ -29,5 +28,6 @@ interface State { | ||
export function repeat<T>(items: T[], keyFn: KeyFn<T>, template: ItemTemplate<T>): RepeatResult; | ||
export function repeat<T>(items: T[], template: ItemTemplate<T>): RepeatResult; | ||
export function repeat<T>(items: Iterable<T>, keyFnOrTemplate: KeyFn<T>|ItemTemplate<T>, template?: ItemTemplate<T>): RepeatResult { | ||
export function repeat<T>(items: T[], keyFn: KeyFn<T>, template: ItemTemplate<T>): DirectiveFn; | ||
export function repeat<T>(items: T[], template: ItemTemplate<T>): DirectiveFn; | ||
export function repeat<T>(items: Iterable<T>, keyFnOrTemplate: KeyFn<T>|ItemTemplate<T>, template?: ItemTemplate<T>): DirectiveFn { | ||
let keyFn: KeyFn<T>; | ||
@@ -40,3 +40,4 @@ if (arguments.length === 2) { | ||
return (part: NodePart): any => { | ||
return directive((part: NodePart): any => { | ||
let state = stateCache.get(part); | ||
@@ -152,3 +153,3 @@ if (state === undefined) { | ||
state.parts = itemParts; | ||
}; | ||
}); | ||
} |
@@ -15,3 +15,3 @@ /** | ||
import { NodePart } from "../lit-html.js"; | ||
import { directive, NodePart } from '../lit-html.js'; | ||
@@ -21,7 +21,6 @@ /** | ||
*/ | ||
export function until(promise: Promise<any>, defaultContent: any) { | ||
return async function(part: NodePart) { | ||
export const until = (promise: Promise<any>, defaultContent: any) => | ||
directive((part: NodePart) => { | ||
part.setValue(defaultContent); | ||
part.setValue(await promise); | ||
} | ||
} | ||
return promise; | ||
}); |
@@ -53,7 +53,10 @@ /** | ||
*/ | ||
export function render(result: TemplateResult, container: Element|DocumentFragment) { | ||
let instance = container.__templateInstance as any; | ||
export function render(result: TemplateResult, container: Element|DocumentFragment, | ||
partCallback: PartCallback = defaultPartCallback) { | ||
let instance = (container as any).__templateInstance as any; | ||
// Repeat render, just call update() | ||
if (instance !== undefined && | ||
instance.template === result.template && | ||
instance instanceof TemplateInstance) { | ||
instance._partCallback === partCallback) { | ||
instance.update(result.values); | ||
@@ -63,4 +66,5 @@ return; | ||
instance = new TemplateInstance(result.template); | ||
container.__templateInstance = instance; | ||
// First render, create a new TemplateInstance and append it | ||
instance = new TemplateInstance(result.template, partCallback); | ||
(container as any).__templateInstance = instance; | ||
@@ -97,3 +101,3 @@ const fragment = instance._clone(); | ||
public type: string, | ||
public index: number, | ||
public path: number[], | ||
public name?: string, | ||
@@ -106,3 +110,2 @@ public rawName?: string, | ||
export class Template { | ||
private _strings: TemplateStringsArray; | ||
parts: TemplatePart[] = []; | ||
@@ -112,63 +115,91 @@ element: HTMLTemplateElement; | ||
constructor(strings: TemplateStringsArray) { | ||
this._strings = strings; | ||
this._parse(); | ||
} | ||
this.element = document.createElement('template'); | ||
this.element.innerHTML = this._getHtml(strings); | ||
private _parse() { | ||
this.element = document.createElement('template'); | ||
this.element.innerHTML = this._getTemplateHtml(this._strings); | ||
const walker = document.createTreeWalker(this.element.content, | ||
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); | ||
let index = -1; | ||
let partIndex = 0; | ||
const nodesToRemove = []; | ||
const attributesToRemove = []; | ||
while (walker.nextNode()) { | ||
index++; | ||
const node = walker.currentNode; | ||
if (node.nodeType === Node.ELEMENT_NODE) { | ||
const attributes = node.attributes; | ||
for (let i = 0; i < attributes.length; i++) { | ||
const attribute = attributes.item(i); | ||
const value = attribute.value; | ||
const strings = value.split(exprMarker); | ||
if (strings.length > 1) { | ||
const attributeString = this._strings[partIndex]; | ||
// Trim the trailing literal value if this is an interpolation | ||
const rawNameString = attributeString.substring(0, attributeString.length - strings[0].length); | ||
const match = rawNameString.match(/((?:\w|[.\-_$])+)=["']?$/); | ||
const rawName = match![1]; | ||
this.parts.push(new TemplatePart('attribute', index, attribute.name, rawName, strings)); | ||
attributesToRemove.push(attribute); | ||
partIndex += strings.length - 1; | ||
const nodesToRemove: Node[] = []; | ||
const attributesToRemove: Attr[] = []; | ||
// The current location in the DOM tree, as an array of child indices | ||
const currentPath: number[] = []; | ||
// What expression we're currently handling | ||
let expressionIndex = 0; | ||
/* | ||
* This populates the parts array by traversing the template with a | ||
* recursive DFS, and giving each part a path of indices from the root of | ||
* the template to the target node. | ||
*/ | ||
const findParts = (node: Node, index: number) => { | ||
currentPath.push(index); | ||
let size = 1; | ||
if (node.nodeType === 3 /* TEXT_NODE */) { | ||
const value = node.nodeValue!; | ||
const strings = value!.split(exprMarker); | ||
if (strings.length > 1) { | ||
const parent = node.parentNode!; | ||
size = strings.length; | ||
const lastIndex = size - 1; | ||
// We have a part for each match found | ||
expressionIndex += lastIndex; | ||
// We keep this current node, but reset its content to the last | ||
// literal part. We insert new literal nodes before this so that the | ||
// tree walker keeps its position correctly. | ||
node.textContent = strings[lastIndex]; | ||
// Generate a new text node for each literal section | ||
// These nodes are also used as the markers for node parts | ||
for (let i = 0; i < lastIndex; i++) { | ||
parent.insertBefore(new Text(strings[i]), node); | ||
this.parts.push(new TemplatePart('node', currentPath.slice(1))); | ||
// Increment the last index on the stack because we just created a | ||
// new text node | ||
currentPath[currentPath.length - 1] += 1; | ||
} | ||
} else if (value.trim() === '') { | ||
nodesToRemove.push(node); | ||
size = 0; | ||
} | ||
} else if (node.nodeType === Node.TEXT_NODE) { | ||
const strings = node.nodeValue!.split(exprMarker); | ||
if (strings.length > 1) { | ||
// Generate a new text node for each literal and two for each part, | ||
// a start and end | ||
partIndex += strings.length - 1; | ||
for (let i = 0; i < strings.length; i++) { | ||
const string = strings[i]; | ||
const literalNode = new Text(string); | ||
node.parentNode!.insertBefore(literalNode, node); | ||
index++; | ||
if (i < strings.length - 1) { | ||
node.parentNode!.insertBefore(new Text(), node); | ||
node.parentNode!.insertBefore(new Text(), node); | ||
this.parts.push(new TemplatePart('node', index)); | ||
index += 2; | ||
} else { /* ELEMENT_NODE or DOCUMENT_FRAGMENT */ | ||
if (node.nodeType === 1 && node.hasAttributes()) { | ||
const attributes = node.attributes; | ||
for (let i = 0; i < attributes.length; i++) { | ||
const attribute = attributes.item(i); | ||
const value = attribute.value; | ||
// Look for expression markers | ||
const attributeStrings = value.split(exprMarker); | ||
if (attributeStrings.length > 1) { | ||
// Get the template string that preced this attribute expression | ||
const attributeString = strings[expressionIndex]; | ||
// Trim the trailing literal part of the attribute value if this | ||
// is an interpolation | ||
const rawNameString = attributeString.substring(0, attributeString.length - attributeStrings[0].length); | ||
// Extract the attribute name | ||
const match = rawNameString.match(/((?:\w|[.\-_$])+)=["']?$/); | ||
const rawName = match![1]; | ||
this.parts.push(new TemplatePart('attribute', currentPath.slice(1), attribute.name, rawName, attributeStrings)); | ||
attributesToRemove.push(attribute); | ||
expressionIndex += attributeStrings.length - 1; | ||
} | ||
} | ||
index--; | ||
nodesToRemove.push(node); | ||
} else if (!node.nodeValue!.trim()) { | ||
nodesToRemove.push(node); | ||
index--; | ||
} | ||
if (node.hasChildNodes()) { | ||
let child = node.firstChild; | ||
let i = 0; | ||
while (child !== null) { | ||
i += findParts(child, i); | ||
child = child.nextSibling; | ||
} | ||
} | ||
} | ||
currentPath.pop(); | ||
return size; | ||
} | ||
findParts(this.element.content, -1); | ||
// Remove text binding nodes after the walk to not disturb the TreeWalker | ||
// Remove empty text nodes and attributes after the walk so as to not | ||
// disturb the traversal | ||
for (const n of nodesToRemove) { | ||
@@ -182,3 +213,3 @@ n.parentNode!.removeChild(n); | ||
private _getTemplateHtml(strings: TemplateStringsArray): string { | ||
private _getHtml(strings: TemplateStringsArray): string { | ||
const parts = []; | ||
@@ -196,2 +227,9 @@ for (let i = 0; i < strings.length; i++) { | ||
export type DirectiveFn = (part: Part) => any; | ||
export const directive = <F extends DirectiveFn>(f: F): F => { | ||
(f as any).__litDirective = true; | ||
return f; | ||
}; | ||
export abstract class Part { | ||
@@ -205,35 +243,35 @@ instance: TemplateInstance | ||
abstract setValue(value: any): void; | ||
protected _getValue(value: any) { | ||
if (typeof value === 'function') { | ||
try { | ||
value = value(this); | ||
} catch (e) { | ||
console.error(e); | ||
return; | ||
} | ||
// `null` as the value of a Text node will render the string 'null' | ||
// so we convert it to undefined | ||
if (typeof value === 'function' && value.__litDirective === true) { | ||
value = value(this); | ||
} | ||
if (value === null) { | ||
// `null` as the value of Text node will render the string 'null' | ||
return undefined; | ||
} | ||
return value; | ||
return value === null ? undefined : value; | ||
} | ||
} | ||
export class AttributePart extends Part { | ||
export interface SinglePart extends Part { | ||
setValue(value: any): void; | ||
} | ||
export interface MultiPart extends Part { | ||
setValue(values: any[], startIndex: number): void; | ||
} | ||
export class AttributePart extends Part implements MultiPart { | ||
element: Element; | ||
name: string; | ||
strings: string[]; | ||
size: number; | ||
constructor(instance: TemplateInstance, element: Element, name: string, strings: string[]) { | ||
super(instance); | ||
console.assert(element.nodeType === Node.ELEMENT_NODE); | ||
this.element = element; | ||
this.name = name; | ||
this.strings = strings; | ||
this.size = strings.length - 1; | ||
} | ||
setValue(values: any[]): void { | ||
setValue(values: any[], startIndex: number): void { | ||
const strings = this.strings; | ||
@@ -245,4 +283,4 @@ let text = ''; | ||
if (i < strings.length - 1) { | ||
const v = this._getValue(values[i]); | ||
if (v && typeof v !== 'string' && v[Symbol.iterator]) { | ||
const v = this._getValue(values[startIndex + i]); | ||
if (v && (Array.isArray(v) || typeof v !== 'string' && v[Symbol.iterator])) { | ||
for (const t of v) { | ||
@@ -260,9 +298,5 @@ // TODO: we need to recursively call getValue into iterables... | ||
get size(): number { | ||
return this.strings.length - 1; | ||
} | ||
} | ||
export class NodePart extends Part { | ||
export class NodePart extends Part implements SinglePart { | ||
startNode: Node; | ||
@@ -281,113 +315,119 @@ endNode: Node; | ||
if (value instanceof Node) { | ||
this._previousValue = this._setNodeValue(value); | ||
if (value === null || | ||
!(typeof value === 'object' || typeof value === 'function')) { | ||
// Handle primitive values | ||
// If the value didn't change, do nothing | ||
if (value === this._previousValue) { | ||
return; | ||
} | ||
this._setText(value); | ||
} else if (value instanceof TemplateResult) { | ||
this._previousValue = this._setTemplateResultValue(value); | ||
} else if (value && value.then !== undefined) { | ||
value.then((v: any) => { | ||
if (this._previousValue === value) { | ||
this.setValue(v); | ||
} | ||
}); | ||
this._previousValue = value; | ||
} else if (value && typeof value !== 'string' && value[Symbol.iterator]) { | ||
this._previousValue = this._setIterableValue(value); | ||
} else if (this.startNode.nextSibling! === this.endNode.previousSibling! && | ||
this.startNode.nextSibling!.nodeType === Node.TEXT_NODE) { | ||
this.startNode.nextSibling!.textContent = value; | ||
this._previousValue = value; | ||
this._setTemplateResult(value); | ||
} else if (Array.isArray(value) || value[Symbol.iterator]) { | ||
this._setIterable(value); | ||
} else if (value instanceof Node) { | ||
this._setNode(value); | ||
} else if (value.then !== undefined) { | ||
this._setPromise(value); | ||
} else { | ||
this._previousValue = this._setTextValue(value); | ||
// Fallback, will render the string representation | ||
this._setText(value); | ||
} | ||
} | ||
private _insertNodeBeforeEndNode(node: Node) { | ||
private _insert(node: Node) { | ||
this.endNode.parentNode!.insertBefore(node, this.endNode); | ||
} | ||
private _setNodeValue(value: Node): Node { | ||
private _setNode(value: Node): void { | ||
this.clear(); | ||
this._insertNodeBeforeEndNode(value); | ||
return value; | ||
this._insert(value); | ||
this._previousValue = value; | ||
} | ||
private _setTextValue(value: string): Node { | ||
return this._setNodeValue(new Text(value)); | ||
private _setText(value: string): void { | ||
if (this.startNode.nextSibling! === this.endNode.previousSibling! && | ||
this.startNode.nextSibling!.nodeType === Node.TEXT_NODE) { | ||
// If we only have a single text node between the markers, we can just | ||
// set its value, rather than replacing it. | ||
// TODO(justinfagnani): Can we just check if _previousValue is | ||
// primitive? | ||
this.startNode.nextSibling!.textContent = value; | ||
} else { | ||
this._setNode(new Text(value)); | ||
} | ||
this._previousValue = value; | ||
} | ||
private _setTemplateResultValue(value: TemplateResult): TemplateInstance { | ||
private _setTemplateResult(value: TemplateResult): void { | ||
let instance: TemplateInstance; | ||
if (this._previousValue && this._previousValue._template === value.template) { | ||
if (this._previousValue && this._previousValue.template === value.template) { | ||
instance = this._previousValue; | ||
} else { | ||
instance = this.instance._createInstance(value.template); | ||
this._setNodeValue(instance._clone()); | ||
instance = new TemplateInstance(value.template, this.instance._partCallback); | ||
this._setNode(instance._clone()); | ||
this._previousValue = instance; | ||
} | ||
instance.update(value.values); | ||
return instance; | ||
} | ||
private _setIterableValue(value: any): NodePart[] { | ||
private _setIterable(value: any): void { | ||
// For an Iterable, we create a new InstancePart per item, then set its | ||
// value to the item. This is a little bit of overhead for every item in | ||
// an Iterable, but it lets us recurse easily and update Arrays of | ||
// TemplateResults that will be commonly returned from expressions like: | ||
// array.map((i) => html`${i}`) | ||
// an Iterable, but it lets us recurse easily and efficiently update Arrays | ||
// of TemplateResults that will be commonly returned from expressions like: | ||
// array.map((i) => html`${i}`), by reusing existing TemplateInstances. | ||
// We reuse this parts startNode as the first part's startNode, and this | ||
// parts endNode as the last part's endNode. | ||
if (!Array.isArray(this._previousValue)) { | ||
this.clear(); | ||
this._previousValue = []; | ||
} | ||
let itemStart = this.startNode; | ||
let itemEnd; | ||
const values = value[Symbol.iterator]() as Iterator<any>; | ||
// Lets of keep track of how many items we stamped so we can clear leftover | ||
// items from a previous render | ||
const itemParts = this._previousValue; | ||
let partIndex = 0; | ||
const previousParts: NodePart[]|undefined = Array.isArray(this._previousValue) ? | ||
this._previousValue : undefined; | ||
let previousPartsIndex = 0; | ||
const itemParts = []; | ||
let current = values.next(); | ||
let next = values.next(); | ||
for (const item of value) { | ||
// Try to reuse an existing part | ||
let itemPart = itemParts[partIndex]; | ||
if (current.done) { | ||
// Empty iterable, just clear | ||
this.clear(); | ||
} | ||
while (!current.done) { | ||
// Reuse a previous part if we can, otherwise create a new one | ||
let itemPart: NodePart; | ||
if (previousParts !== undefined && previousPartsIndex < previousParts.length) { | ||
itemPart = previousParts[previousPartsIndex++]; | ||
if (next.done && itemPart.endNode !== this.endNode) { | ||
// Since this is the last part we'll use, set it's endNode to the | ||
// container's endNode. Setting the value of this part will clean | ||
// up any residual nodes from a previously longer iterable. | ||
// 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; | ||
// Remove previousSibling, since we want itemPart.endNode to be | ||
// removed as part of the clear operation. | ||
this.clear(itemPart.endNode.previousSibling!); | ||
itemPart.endNode = this.endNode; | ||
// 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 = new Text(); | ||
this._insert(itemStart); | ||
} | ||
itemEnd = itemPart.endNode; | ||
} else { | ||
if (next.done) { | ||
// on the last item, reuse this part's endNode | ||
itemEnd = this.endNode; | ||
} else { | ||
itemEnd = new Text(); | ||
this._insertNodeBeforeEndNode(itemEnd); | ||
} | ||
itemPart = new NodePart(this.instance, itemStart, itemEnd); | ||
itemPart = new NodePart(this.instance, itemStart, this.endNode); | ||
itemParts.push(itemPart); | ||
} | ||
itemPart.setValue(current.value); | ||
itemParts.push(itemPart); | ||
current = next; | ||
next = values.next(); | ||
itemStart = itemEnd; | ||
itemPart.setValue(item); | ||
partIndex++; | ||
} | ||
return itemParts; | ||
if (partIndex === 0) { | ||
this.clear(); | ||
} else if (partIndex < itemParts.length) { | ||
const lastPart = itemParts[partIndex - 1]; | ||
this.clear(lastPart.endNode.previousSibling!); | ||
lastPart.endNode = this.endNode; | ||
} | ||
} | ||
protected _setPromise(value: Promise<any>): void { | ||
value.then((v: any) => { | ||
if (this._previousValue === value) { | ||
this.setValue(v); | ||
} | ||
}); | ||
this._previousValue = value; | ||
} | ||
clear(startNode: Node = this.startNode) { | ||
@@ -406,16 +446,28 @@ this._previousValue = undefined; | ||
export type PartCallback = (instance: TemplateInstance, templatePart: TemplatePart, node: Node) => Part; | ||
export const defaultPartCallback = (instance: TemplateInstance, templatePart: TemplatePart, node: Node): Part => { | ||
if (templatePart.type === 'attribute') { | ||
return new AttributePart(instance, node as Element, templatePart.name!, templatePart.strings!); | ||
} else if (templatePart.type === 'node') { | ||
return new NodePart(instance, node, node.nextSibling!); | ||
} | ||
throw new Error(`Unknown part type ${templatePart.type}`); | ||
} | ||
/** | ||
* An instance of a `Template` that can be attached to the DOM and updated | ||
* with new values. | ||
*/ | ||
export class TemplateInstance { | ||
_template: Template; | ||
_parts: Part[] = []; | ||
startNode: Node; | ||
endNode: Node; | ||
_partCallback: PartCallback; | ||
template: Template; | ||
constructor(template: Template) { | ||
this._template = template; | ||
constructor(template: Template, partCallback: PartCallback = defaultPartCallback) { | ||
this.template = template; | ||
this._partCallback = partCallback; | ||
} | ||
get template() { | ||
return this._template; | ||
} | ||
update(values: any[]) { | ||
@@ -425,5 +477,6 @@ let valueIndex = 0; | ||
if (part.size === undefined) { | ||
part.setValue(values[valueIndex++]); | ||
(part as SinglePart).setValue(values[valueIndex]); | ||
valueIndex++; | ||
} else { | ||
part.setValue(values.slice(valueIndex, valueIndex + part.size)); | ||
(part as MultiPart).setValue(values, valueIndex); | ||
valueIndex += part.size; | ||
@@ -435,46 +488,80 @@ } | ||
_clone(): DocumentFragment { | ||
const fragment = document.importNode(this._template.element.content, true); | ||
const fragment = document.importNode(this.template.element.content, true); | ||
if (this._template.parts.length > 0) { | ||
const walker = document.createTreeWalker(fragment, | ||
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); | ||
/* | ||
* This implements a search that traverses the minimum number of Nodes in a | ||
* DOM tree while only using Node.firstChild and Node.nextSibling, which | ||
* have been measured to be faster than accessing Node.childNodes. | ||
* | ||
* For any given path of childNode indices starting from the root of the | ||
* template, we recursively find the parent, then call nextSibling until | ||
* we get to the target index. | ||
* | ||
* Once found, we cache the index and node, so that the next lookup can be | ||
* faster by: | ||
* | ||
* 1. Finding the common ancestor of the previously searched path | ||
* 2. Starting a traversal of children from that common ancestor from the | ||
* last node found, rather than firstChild | ||
* | ||
* This means that any node is only ever visited once. | ||
* | ||
* In order for this to work, paths much be searched for in depth-first | ||
* order. | ||
* | ||
* The overhead of these optimizations probably only matters for larger, | ||
* more complex templates, with enough bindings to speard search costs | ||
* across them, and the benefit will not show up on micro-bencharks with | ||
* small templates. However, it doesn't seem like this slows down | ||
* micro-benchmarks. | ||
*/ | ||
let nodeStack: [number, Node][] = []; | ||
const findNodeAtPath = (path: number[], depth: number): Node|undefined => { | ||
// Recurse up the tree to find the parent | ||
const parent = (depth === 0) ? fragment : findNodeAtPath(path, depth - 1)!; | ||
const parts = this._template.parts; | ||
let index = 0; | ||
let partIndex = 0; | ||
let templatePart = parts[0]; | ||
let node = walker.nextNode(); | ||
while (node != null && partIndex < parts.length) { | ||
if (index === templatePart.index) { | ||
this._parts.push(this._createPart(templatePart, node)); | ||
templatePart = parts[++partIndex]; | ||
} else { | ||
index++; | ||
node = walker.nextNode(); | ||
// The target index we're searching for at this depth | ||
const targetIndex = path[depth]; | ||
let currentIndex; | ||
let node; | ||
if (nodeStack.length > depth) { | ||
// If we've cached up to this depth, and the index in the stack at this | ||
// depth equals the targetIndex, then just return from the stack. | ||
if (nodeStack[depth][0] === targetIndex) { | ||
return nodeStack[depth][1]; | ||
} | ||
// Otherwise, start the search at the last index we used at this level | ||
[currentIndex, node] = nodeStack[depth] | ||
nodeStack = nodeStack.slice(0, depth); | ||
} else { | ||
// If the stack didn't have anything at this depth, initialize the search | ||
// to the first child | ||
currentIndex = 0; | ||
node = parent.firstChild; | ||
} | ||
// Perform the traversal | ||
while (node !== null) { | ||
if (currentIndex === targetIndex) { | ||
// When we have a hit, cache it | ||
nodeStack.push([currentIndex, node]); | ||
return node; | ||
} | ||
node = node.nextSibling; | ||
currentIndex++; | ||
} | ||
// This should never happen | ||
return; | ||
} | ||
return fragment; | ||
} | ||
_createPart(templatePart: TemplatePart, node: Node): Part { | ||
if (templatePart.type === 'attribute') { | ||
return new AttributePart(this, node as Element, templatePart.name!, templatePart.strings!); | ||
} else if (templatePart.type === 'node') { | ||
return new NodePart(this, node, node.nextSibling!); | ||
} else { | ||
throw new Error(`unknown part type: ${templatePart.type}`); | ||
for (const p of this.template.parts) { | ||
const node = findNodeAtPath(p.path, p.path.length - 1)!; | ||
this._parts.push(this._partCallback(this, p, node)); | ||
} | ||
} | ||
_createInstance(template: Template) { | ||
return new TemplateInstance(template); | ||
return fragment; | ||
} | ||
} | ||
declare global { | ||
interface Node { | ||
__templateInstance?: TemplateInstance; | ||
} | ||
} |
@@ -18,3 +18,3 @@ /** | ||
import {html, NodePart, TemplateInstance} from '../../lit-html.js'; | ||
import {html, render} from '../../lit-html.js'; | ||
import {repeat} from '../../lib/repeat.js'; | ||
@@ -27,14 +27,5 @@ | ||
let container: HTMLElement; | ||
let startNode: Node; | ||
let endNode: Node; | ||
let part: NodePart; | ||
setup(() => { | ||
container = document.createElement('div'); | ||
startNode = new Text(); | ||
endNode = new Text(); | ||
container.appendChild(startNode); | ||
container.appendChild(endNode); | ||
const instance = new TemplateInstance(html``.template); | ||
part = new NodePart(instance, startNode, endNode); | ||
}); | ||
@@ -45,5 +36,5 @@ | ||
test('renders a list', () => { | ||
const r = repeat([1, 2, 3], (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`); | ||
r(part); | ||
const r = html`${repeat([1, 2, 3], (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`)}`; | ||
render(r, container); | ||
assert.equal(container.innerHTML, `<li>item: 1</li><li>item: 2</li><li>item: 3</li>`); | ||
@@ -54,5 +45,5 @@ }); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html`${repeat(items, (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 1</li><li>item: 2</li><li>item: 3</li>`); | ||
@@ -62,3 +53,3 @@ const children1 = Array.from(container.querySelectorAll('li')); | ||
items = [3, 2, 1]; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 3</li><li>item: 2</li><li>item: 1</li>`); | ||
@@ -73,8 +64,8 @@ const children2 = Array.from(container.querySelectorAll('li')); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html`${repeat(items, (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
items = [0, 1, 2, 3]; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 0</li><li>item: 1</li><li>item: 2</li><li>item: 3</li>`); | ||
@@ -85,8 +76,8 @@ }); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html`${repeat(items, (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
items = [1, 2, 3, 4]; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 1</li><li>item: 2</li><li>item: 3</li><li>item: 4</li>`); | ||
@@ -97,8 +88,8 @@ }); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html`${repeat(items, (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
items = []; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, ``); | ||
@@ -109,9 +100,10 @@ }); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html`${repeat(items, (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
const children1 = Array.from(container.querySelectorAll('li')); | ||
items = [2, 3]; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 2</li><li>item: 3</li>`); | ||
@@ -125,9 +117,10 @@ const children2 = Array.from(container.querySelectorAll('li')); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html`${repeat(items, (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
const children1 = Array.from(container.querySelectorAll('li')); | ||
items = [1, 2]; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 1</li><li>item: 2</li>`); | ||
@@ -141,9 +134,10 @@ const children2 = Array.from(container.querySelectorAll('li')); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html`${repeat(items, (i) => i, (i: number) => html` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
const children1 = Array.from(container.querySelectorAll('li')); | ||
items = [1, 3]; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 1</li><li>item: 3</li>`); | ||
@@ -160,5 +154,5 @@ const children2 = Array.from(container.querySelectorAll('li')); | ||
test('renderes an list', () => { | ||
const r = repeat([1, 2, 3], (i: number) => html` | ||
<li>item: ${i}</li>`); | ||
r(part); | ||
const r = html`${repeat([1, 2, 3], (i: number) => html` | ||
<li>item: ${i}</li>`)}`; | ||
render(r, container); | ||
assert.equal(container.innerHTML, `<li>item: 1</li><li>item: 2</li><li>item: 3</li>`); | ||
@@ -169,9 +163,9 @@ }); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i: number) => html` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html`${repeat(items, (i: number) => html` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 1</li><li>item: 2</li><li>item: 3</li>`); | ||
items = [3, 2, 1]; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 3</li><li>item: 2</li><li>item: 1</li>`); | ||
@@ -182,8 +176,8 @@ }); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i: number) => html` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html`${repeat(items, (i: number) => html` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
items = []; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, ``); | ||
@@ -190,0 +184,0 @@ }); |
@@ -33,2 +33,3 @@ /** | ||
await promise; | ||
await new Promise((r) => setTimeout(() => r())); | ||
assert.equal(container.innerHTML, '<div>foo</div>'); | ||
@@ -35,0 +36,0 @@ }); |
@@ -18,3 +18,3 @@ /** | ||
import {html, render, TemplateResult, TemplatePart, TemplateInstance, NodePart, Part, AttributePart, Template} from '../lit-html.js'; | ||
import {html, render, defaultPartCallback, TemplateResult, TemplatePart, TemplateInstance, NodePart, Part, AttributePart } from '../lit-html.js'; | ||
@@ -41,2 +41,17 @@ const assert = chai.assert; | ||
test('does not create extra empty text nodes', () => { | ||
const countNodes = (result: TemplateResult, getNodes: (f: DocumentFragment) => NodeList) => | ||
getNodes(result.template.element.content).length; | ||
assert.equal(countNodes(html`<div>${0}</div>`, (c) => c.childNodes[0].childNodes), 2); | ||
assert.equal(countNodes(html`${0}`, (c) => c.childNodes), 2); | ||
assert.equal(countNodes(html`a${0}`, (c) => c.childNodes), 2); | ||
assert.equal(countNodes(html`${0}a`, (c) => c.childNodes), 2); | ||
assert.equal(countNodes(html`${0}${0}`, (c) => c.childNodes), 3); | ||
assert.equal(countNodes(html`a${0}${0}`, (c) => c.childNodes), 3); | ||
assert.equal(countNodes(html`${0}b${0}`, (c) => c.childNodes), 3); | ||
assert.equal(countNodes(html`${0}${0}c`, (c) => c.childNodes), 3); | ||
assert.equal(countNodes(html`a${0}b${0}c`, (c) => c.childNodes), 3); | ||
}); | ||
test('parses parts for multiple expressions', () => { | ||
@@ -46,7 +61,7 @@ const result = html` | ||
<p>${2}</p> | ||
${3} | ||
<span a="${4}">${5}</span> | ||
${3}x${4} | ||
<span a="${5}x${6}">${7}x${8}</span> | ||
</div>`; | ||
const parts = result.template.parts; | ||
assert.equal(parts.length, 5); | ||
assert.equal(parts.length, 7); | ||
}); | ||
@@ -91,2 +106,12 @@ | ||
test('resists XSS attempt in node values', () => { | ||
const result = html`<div>${'<script>alert("boo");</script>'}</div>`; | ||
assert(result.template.element.innerHTML, '<div></div>'); | ||
}) | ||
test('resists XSS attempt in attribute values', () => { | ||
const result = html`<div foo="${'"><script>alert("boo");</script><div foo="'}"></div>`; | ||
assert(result.template.element.innerHTML, '<div></div>'); | ||
}) | ||
}); | ||
@@ -122,14 +147,9 @@ | ||
test('renders a thunk', () => { | ||
test('renders a function', () => { | ||
// This test just checks that we don't call the function | ||
const container = document.createElement('div'); | ||
render(html`<div>${(_:any)=>123}</div>`, container); | ||
assert.equal(container.innerHTML, '<div>123</div>'); | ||
assert.equal(container.innerHTML, '<div>(_) => 123</div>'); | ||
}); | ||
test('renders thunks that throw as empty text', () => { | ||
const container = document.createElement('div'); | ||
render(html`<div>${(_:any)=>{throw new Error('e')}}</div>`, container); | ||
assert.equal(container.innerHTML, '<div></div>'); | ||
}); | ||
test('renders arrays', () => { | ||
@@ -204,6 +224,7 @@ const container = document.createElement('div'); | ||
test('renders a thunk to an attribute', () => { | ||
test('renders a function to an attribute', () => { | ||
// This test just checks that we don't call the function | ||
const container = document.createElement('div'); | ||
render(html`<div foo=${(_:any)=>123}></div>`, container); | ||
assert.equal(container.innerHTML, '<div foo="123"></div>'); | ||
assert.equal(container.innerHTML, '<div foo="(_) => 123"></div>'); | ||
}); | ||
@@ -287,2 +308,29 @@ | ||
test('dirty checks simple values', () => { | ||
const container = document.createElement('div'); | ||
let foo = 'aaa'; | ||
const t = () => html`<div>${foo}</div>`; | ||
render(t(), container); | ||
assert.equal(container.innerHTML, '<div>aaa</div>'); | ||
const text = container.firstChild!.childNodes[1] as Text; | ||
assert.equal(text.textContent, 'aaa'); | ||
// Set textContent manually. Since lit-html doesn't dirty checks against | ||
// actual DOM, but again previous part values, this modification should | ||
// persist through the next render with the same value. | ||
text.textContent = 'bbb'; | ||
assert.equal(text.textContent, 'bbb'); | ||
assert.equal(container.innerHTML, '<div>bbb</div>'); | ||
// Re-render with the same content, should be a no-op | ||
render(t(), container); | ||
assert.equal(container.innerHTML, '<div>bbb</div>'); | ||
const text2 = container.firstChild!.childNodes[1] as Text; | ||
// The next node should be the same too | ||
assert.strictEqual(text, text2); | ||
}); | ||
test('renders to and updates a container', () => { | ||
@@ -445,13 +493,8 @@ const container = document.createElement('div'); | ||
class PropertySettingTemplateInstance extends TemplateInstance { | ||
_createPart(templatePart: TemplatePart, node: Node): Part { | ||
const partCallback = (instance: TemplateInstance, templatePart: TemplatePart, node: Node): Part => { | ||
if (templatePart.type === 'attribute') { | ||
return new PropertyPart(this, node as Element, templatePart.rawName!, templatePart.strings!); | ||
return new PropertyPart(instance, node as Element, templatePart.rawName!, templatePart.strings!); | ||
} | ||
return super._createPart(templatePart, node); | ||
} | ||
_createInstance(template: Template) { | ||
return new PropertySettingTemplateInstance(template); | ||
} | ||
} | ||
return defaultPartCallback(instance, templatePart, node); | ||
}; | ||
@@ -483,6 +526,3 @@ class PropertyPart extends AttributePart { | ||
const t = html`<div someProp="${123}"></div>`; | ||
const instance = new PropertySettingTemplateInstance(t.template); | ||
const fragment = instance._clone(); | ||
instance.update(t.values); | ||
container.appendChild(fragment); | ||
render(t, container, partCallback); | ||
assert.equal(container.innerHTML, '<div></div>'); | ||
@@ -492,9 +532,6 @@ assert.strictEqual((container.firstElementChild as any).someProp, 123); | ||
test('works with nested tempaltes', () => { | ||
test('works with nested templates', () => { | ||
const container = document.createElement('div'); | ||
const t = html`${html`<div someProp="${123}"></div>`}`; | ||
const instance = new PropertySettingTemplateInstance(t.template); | ||
const fragment = instance._clone(); | ||
instance.update(t.values); | ||
container.appendChild(fragment); | ||
render(t, container, partCallback); | ||
assert.equal(container.innerHTML, '<div></div>'); | ||
@@ -547,12 +584,7 @@ assert.strictEqual((container.firstElementChild as any).someProp, 123); | ||
test('accepts a thunk', () => { | ||
test('accepts a function', () => { | ||
part.setValue((_:any)=>123); | ||
assert.equal(container.innerHTML, '123'); | ||
assert.equal(container.innerHTML, '(_) => 123'); | ||
}); | ||
test('accepts thunks that throw as empty text', () => { | ||
part.setValue((_:any)=>{throw new Error('e')}); | ||
assert.equal(container.innerHTML, ''); | ||
}); | ||
test('accepts an element', () => { | ||
@@ -688,7 +720,18 @@ part.setValue(document.createElement('p')); | ||
test('updates arrays with siblings', () => { | ||
let items = [1, 2, 3]; | ||
const t = () => html`<p></p>${items}<a></a>`; | ||
render(t(), container); | ||
assert.equal(container.innerHTML, '<p></p>123<a></a>'); | ||
items = [1, 2, 3, 4]; | ||
render(t(), container); | ||
assert.equal(container.innerHTML, '<p></p>1234<a></a>'); | ||
}); | ||
test('updates are stable when called multiple times with templates', () => { | ||
let value = 'foo'; | ||
const r = () => html`<h1>${value}</h1>`; | ||
part.setValue(r); | ||
part.setValue(r()); | ||
assert.equal(container.innerHTML, '<h1>foo</h1>'); | ||
@@ -698,6 +741,6 @@ const originalH1 = container.querySelector('h1'); | ||
value = 'bar'; | ||
part.setValue(r); | ||
part.setValue(r()); | ||
assert.equal(container.innerHTML, '<h1>bar</h1>'); | ||
const newH1 = container.querySelector('h1'); | ||
assert.isTrue(newH1 === originalH1); | ||
assert.strictEqual(newH1, originalH1); | ||
}); | ||
@@ -708,3 +751,3 @@ | ||
const r = () => items.map((i)=>html`<li>${i}</li>`); | ||
part.setValue(r); | ||
part.setValue(r()); | ||
assert.equal(container.innerHTML, '<li>1</li><li>2</li><li>3</li>'); | ||
@@ -714,3 +757,3 @@ const originalLIs = Array.from(container.querySelectorAll('li')); | ||
items = [3, 2, 1]; | ||
part.setValue(r); | ||
part.setValue(r()); | ||
assert.equal(container.innerHTML, '<li>3</li><li>2</li><li>1</li>'); | ||
@@ -717,0 +760,0 @@ const newLIs = Array.from(container.querySelectorAll('li')); |
@@ -16,3 +16,3 @@ /** | ||
/// <reference path="../../../node_modules/@types/chai/index.d.ts" /> | ||
import { html, NodePart, TemplateInstance } from '../../lit-html.js'; | ||
import { html, render } from '../../lit-html.js'; | ||
import { repeat } from '../../lib/repeat.js'; | ||
@@ -22,19 +22,10 @@ const assert = chai.assert; | ||
let container; | ||
let startNode; | ||
let endNode; | ||
let part; | ||
setup(() => { | ||
container = document.createElement('div'); | ||
startNode = new Text(); | ||
endNode = new Text(); | ||
container.appendChild(startNode); | ||
container.appendChild(endNode); | ||
const instance = new TemplateInstance(html ``.template); | ||
part = new NodePart(instance, startNode, endNode); | ||
}); | ||
suite('keyed', () => { | ||
test('renders a list', () => { | ||
const r = repeat([1, 2, 3], (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`); | ||
r(part); | ||
const r = html `${repeat([1, 2, 3], (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`)}`; | ||
render(r, container); | ||
assert.equal(container.innerHTML, `<li>item: 1</li><li>item: 2</li><li>item: 3</li>`); | ||
@@ -44,9 +35,9 @@ }); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html `${repeat(items, (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 1</li><li>item: 2</li><li>item: 3</li>`); | ||
const children1 = Array.from(container.querySelectorAll('li')); | ||
items = [3, 2, 1]; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 3</li><li>item: 2</li><li>item: 1</li>`); | ||
@@ -60,7 +51,7 @@ const children2 = Array.from(container.querySelectorAll('li')); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html `${repeat(items, (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
items = [0, 1, 2, 3]; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 0</li><li>item: 1</li><li>item: 2</li><li>item: 3</li>`); | ||
@@ -70,7 +61,7 @@ }); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html `${repeat(items, (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
items = [1, 2, 3, 4]; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 1</li><li>item: 2</li><li>item: 3</li><li>item: 4</li>`); | ||
@@ -80,7 +71,7 @@ }); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html `${repeat(items, (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
items = []; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, ``); | ||
@@ -90,8 +81,8 @@ }); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html `${repeat(items, (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
const children1 = Array.from(container.querySelectorAll('li')); | ||
items = [2, 3]; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 2</li><li>item: 3</li>`); | ||
@@ -104,8 +95,8 @@ const children2 = Array.from(container.querySelectorAll('li')); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html `${repeat(items, (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
const children1 = Array.from(container.querySelectorAll('li')); | ||
items = [1, 2]; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 1</li><li>item: 2</li>`); | ||
@@ -118,8 +109,8 @@ const children2 = Array.from(container.querySelectorAll('li')); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html `${repeat(items, (i) => i, (i) => html ` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
const children1 = Array.from(container.querySelectorAll('li')); | ||
items = [1, 3]; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 1</li><li>item: 3</li>`); | ||
@@ -133,5 +124,5 @@ const children2 = Array.from(container.querySelectorAll('li')); | ||
test('renderes an list', () => { | ||
const r = repeat([1, 2, 3], (i) => html ` | ||
<li>item: ${i}</li>`); | ||
r(part); | ||
const r = html `${repeat([1, 2, 3], (i) => html ` | ||
<li>item: ${i}</li>`)}`; | ||
render(r, container); | ||
assert.equal(container.innerHTML, `<li>item: 1</li><li>item: 2</li><li>item: 3</li>`); | ||
@@ -141,8 +132,8 @@ }); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => html ` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html `${repeat(items, (i) => html ` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 1</li><li>item: 2</li><li>item: 3</li>`); | ||
items = [3, 2, 1]; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, `<li>item: 3</li><li>item: 2</li><li>item: 1</li>`); | ||
@@ -152,7 +143,7 @@ }); | ||
let items = [1, 2, 3]; | ||
const t = () => repeat(items, (i) => html ` | ||
<li>item: ${i}</li>`); | ||
t()(part); | ||
const t = () => html `${repeat(items, (i) => html ` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
items = []; | ||
t()(part); | ||
render(t(), container); | ||
assert.equal(container.innerHTML, ``); | ||
@@ -159,0 +150,0 @@ }); |
@@ -28,2 +28,3 @@ /** | ||
await promise; | ||
await new Promise((r) => setTimeout(() => r())); | ||
assert.equal(container.innerHTML, '<div>foo</div>'); | ||
@@ -30,0 +31,0 @@ }); |
@@ -16,3 +16,3 @@ /** | ||
/// <reference path="../../node_modules/@types/chai/index.d.ts" /> | ||
import { html, render, TemplateResult, TemplateInstance, NodePart, AttributePart } from '../lit-html.js'; | ||
import { html, render, defaultPartCallback, TemplateResult, TemplateInstance, NodePart, AttributePart } from '../lit-html.js'; | ||
const assert = chai.assert; | ||
@@ -32,2 +32,14 @@ suite('lit-html', () => { | ||
}); | ||
test('does not create extra empty text nodes', () => { | ||
const countNodes = (result, getNodes) => getNodes(result.template.element.content).length; | ||
assert.equal(countNodes(html `<div>${0}</div>`, (c) => c.childNodes[0].childNodes), 2); | ||
assert.equal(countNodes(html `${0}`, (c) => c.childNodes), 2); | ||
assert.equal(countNodes(html `a${0}`, (c) => c.childNodes), 2); | ||
assert.equal(countNodes(html `${0}a`, (c) => c.childNodes), 2); | ||
assert.equal(countNodes(html `${0}${0}`, (c) => c.childNodes), 3); | ||
assert.equal(countNodes(html `a${0}${0}`, (c) => c.childNodes), 3); | ||
assert.equal(countNodes(html `${0}b${0}`, (c) => c.childNodes), 3); | ||
assert.equal(countNodes(html `${0}${0}c`, (c) => c.childNodes), 3); | ||
assert.equal(countNodes(html `a${0}b${0}c`, (c) => c.childNodes), 3); | ||
}); | ||
test('parses parts for multiple expressions', () => { | ||
@@ -37,7 +49,7 @@ const result = html ` | ||
<p>${2}</p> | ||
${3} | ||
<span a="${4}">${5}</span> | ||
${3}x${4} | ||
<span a="${5}x${6}">${7}x${8}</span> | ||
</div>`; | ||
const parts = result.template.parts; | ||
assert.equal(parts.length, 5); | ||
assert.equal(parts.length, 7); | ||
}); | ||
@@ -78,2 +90,10 @@ test('stores raw names of attributes', () => { | ||
}); | ||
test('resists XSS attempt in node values', () => { | ||
const result = html `<div>${'<script>alert("boo");</script>'}</div>`; | ||
assert(result.template.element.innerHTML, '<div></div>'); | ||
}); | ||
test('resists XSS attempt in attribute values', () => { | ||
const result = html `<div foo="${'"><script>alert("boo");</script><div foo="'}"></div>`; | ||
assert(result.template.element.innerHTML, '<div></div>'); | ||
}); | ||
}); | ||
@@ -102,12 +122,8 @@ suite('TemplateResult', () => { | ||
}); | ||
test('renders a thunk', () => { | ||
test('renders a function', () => { | ||
// This test just checks that we don't call the function | ||
const container = document.createElement('div'); | ||
render(html `<div>${(_) => 123}</div>`, container); | ||
assert.equal(container.innerHTML, '<div>123</div>'); | ||
assert.equal(container.innerHTML, '<div>(_) => 123</div>'); | ||
}); | ||
test('renders thunks that throw as empty text', () => { | ||
const container = document.createElement('div'); | ||
render(html `<div>${(_) => { throw new Error('e'); }}</div>`, container); | ||
assert.equal(container.innerHTML, '<div></div>'); | ||
}); | ||
test('renders arrays', () => { | ||
@@ -172,6 +188,7 @@ const container = document.createElement('div'); | ||
}); | ||
test('renders a thunk to an attribute', () => { | ||
test('renders a function to an attribute', () => { | ||
// This test just checks that we don't call the function | ||
const container = document.createElement('div'); | ||
render(html `<div foo=${(_) => 123}></div>`, container); | ||
assert.equal(container.innerHTML, '<div foo="123"></div>'); | ||
assert.equal(container.innerHTML, '<div foo="(_) => 123"></div>'); | ||
}); | ||
@@ -240,2 +257,23 @@ test('renders an array to an attribute', () => { | ||
suite('update', () => { | ||
test('dirty checks simple values', () => { | ||
const container = document.createElement('div'); | ||
let foo = 'aaa'; | ||
const t = () => html `<div>${foo}</div>`; | ||
render(t(), container); | ||
assert.equal(container.innerHTML, '<div>aaa</div>'); | ||
const text = container.firstChild.childNodes[1]; | ||
assert.equal(text.textContent, 'aaa'); | ||
// Set textContent manually. Since lit-html doesn't dirty checks against | ||
// actual DOM, but again previous part values, this modification should | ||
// persist through the next render with the same value. | ||
text.textContent = 'bbb'; | ||
assert.equal(text.textContent, 'bbb'); | ||
assert.equal(container.innerHTML, '<div>bbb</div>'); | ||
// Re-render with the same content, should be a no-op | ||
render(t(), container); | ||
assert.equal(container.innerHTML, '<div>bbb</div>'); | ||
const text2 = container.firstChild.childNodes[1]; | ||
// The next node should be the same too | ||
assert.strictEqual(text, text2); | ||
}); | ||
test('renders to and updates a container', () => { | ||
@@ -362,13 +400,8 @@ const container = document.createElement('div'); | ||
// case of the names! | ||
class PropertySettingTemplateInstance extends TemplateInstance { | ||
_createPart(templatePart, node) { | ||
if (templatePart.type === 'attribute') { | ||
return new PropertyPart(this, node, templatePart.rawName, templatePart.strings); | ||
} | ||
return super._createPart(templatePart, node); | ||
const partCallback = (instance, templatePart, node) => { | ||
if (templatePart.type === 'attribute') { | ||
return new PropertyPart(instance, node, templatePart.rawName, templatePart.strings); | ||
} | ||
_createInstance(template) { | ||
return new PropertySettingTemplateInstance(template); | ||
} | ||
} | ||
return defaultPartCallback(instance, templatePart, node); | ||
}; | ||
class PropertyPart extends AttributePart { | ||
@@ -398,16 +431,10 @@ setValue(values) { | ||
const t = html `<div someProp="${123}"></div>`; | ||
const instance = new PropertySettingTemplateInstance(t.template); | ||
const fragment = instance._clone(); | ||
instance.update(t.values); | ||
container.appendChild(fragment); | ||
render(t, container, partCallback); | ||
assert.equal(container.innerHTML, '<div></div>'); | ||
assert.strictEqual(container.firstElementChild.someProp, 123); | ||
}); | ||
test('works with nested tempaltes', () => { | ||
test('works with nested templates', () => { | ||
const container = document.createElement('div'); | ||
const t = html `${html `<div someProp="${123}"></div>`}`; | ||
const instance = new PropertySettingTemplateInstance(t.template); | ||
const fragment = instance._clone(); | ||
instance.update(t.values); | ||
container.appendChild(fragment); | ||
render(t, container, partCallback); | ||
assert.equal(container.innerHTML, '<div></div>'); | ||
@@ -449,10 +476,6 @@ assert.strictEqual(container.firstElementChild.someProp, 123); | ||
}); | ||
test('accepts a thunk', () => { | ||
test('accepts a function', () => { | ||
part.setValue((_) => 123); | ||
assert.equal(container.innerHTML, '123'); | ||
assert.equal(container.innerHTML, '(_) => 123'); | ||
}); | ||
test('accepts thunks that throw as empty text', () => { | ||
part.setValue((_) => { throw new Error('e'); }); | ||
assert.equal(container.innerHTML, ''); | ||
}); | ||
test('accepts an element', () => { | ||
@@ -568,13 +591,22 @@ part.setValue(document.createElement('p')); | ||
}); | ||
test('updates arrays with siblings', () => { | ||
let items = [1, 2, 3]; | ||
const t = () => html `<p></p>${items}<a></a>`; | ||
render(t(), container); | ||
assert.equal(container.innerHTML, '<p></p>123<a></a>'); | ||
items = [1, 2, 3, 4]; | ||
render(t(), container); | ||
assert.equal(container.innerHTML, '<p></p>1234<a></a>'); | ||
}); | ||
test('updates are stable when called multiple times with templates', () => { | ||
let value = 'foo'; | ||
const r = () => html `<h1>${value}</h1>`; | ||
part.setValue(r); | ||
part.setValue(r()); | ||
assert.equal(container.innerHTML, '<h1>foo</h1>'); | ||
const originalH1 = container.querySelector('h1'); | ||
value = 'bar'; | ||
part.setValue(r); | ||
part.setValue(r()); | ||
assert.equal(container.innerHTML, '<h1>bar</h1>'); | ||
const newH1 = container.querySelector('h1'); | ||
assert.isTrue(newH1 === originalH1); | ||
assert.strictEqual(newH1, originalH1); | ||
}); | ||
@@ -584,7 +616,7 @@ test('updates are stable when called multiple times with arrays of templates', () => { | ||
const r = () => items.map((i) => html `<li>${i}</li>`); | ||
part.setValue(r); | ||
part.setValue(r()); | ||
assert.equal(container.innerHTML, '<li>1</li><li>2</li><li>3</li>'); | ||
const originalLIs = Array.from(container.querySelectorAll('li')); | ||
items = [3, 2, 1]; | ||
part.setValue(r); | ||
part.setValue(r()); | ||
assert.equal(container.innerHTML, '<li>3</li><li>2</li><li>1</li>'); | ||
@@ -591,0 +623,0 @@ const newLIs = Array.from(container.querySelectorAll('li')); |
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
Uses eval
Supply chain riskPackage uses eval() which is a dangerous function. This prevents the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 2 instances in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
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
856041
73
4768
371
8
3
3