Socket
Socket
Sign inDemoInstall

lit-html

Package Overview
Dependencies
Maintainers
1
Versions
102
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

lit-html - npm Package Compare versions

Comparing version 0.5.0 to 0.6.0-pre.1

.travis.yml

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;

60

lib/lit-extended.js

@@ -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 {};

@@ -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>(_) =&gt; 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, '(_) =&gt; 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>(_) =&gt; 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, '(_) =&gt; 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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc