Created
September 29, 2017 19:11
-
-
Save robwormald/7a05787e38d93e2ed4b5114b28be489b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function (global, factory) { | |
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : | |
typeof define === 'function' && define.amd ? define(['exports'], factory) : | |
(factory((global.lit = {}))); | |
}(this, (function (exports) { 'use strict'; | |
/** | |
* @license | |
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved. | |
* This code may only be used under the BSD style license found at | |
* http://polymer.github.io/LICENSE.txt | |
* The complete set of authors may be found at | |
* http://polymer.github.io/AUTHORS.txt | |
* The complete set of contributors may be found at | |
* http://polymer.github.io/CONTRIBUTORS.txt | |
* Code distributed by Google as part of the polymer project is also | |
* subject to an additional IP rights grant found at | |
* http://polymer.github.io/PATENTS.txt | |
*/ | |
// The first argument to JS template tags retain identity across multiple | |
// calls to a tag for the same literal, so we can cache work done per literal | |
// in a Map. | |
const templates = new Map(); | |
/** | |
* Interprets a template literal as an HTML template that can efficiently | |
* render to and update a container. | |
*/ | |
function html(strings, ...values) { | |
let template = templates.get(strings); | |
if (template === undefined) { | |
template = new Template(strings); | |
templates.set(strings, template); | |
} | |
return new TemplateResult(template, values); | |
} | |
/** | |
* The return type of `html`, which holds a Template and the values from | |
* interpolated expressions. | |
*/ | |
class TemplateResult { | |
constructor(template, values) { | |
this.template = template; | |
this.values = values; | |
} | |
} | |
/** | |
* Renders a template to a container. | |
* | |
* To update a container with new values, reevaluate the template literal and | |
* call `render` with the new result. | |
*/ | |
function render(result, container, partCallback = defaultPartCallback) { | |
let instance = container.__templateInstance; | |
// Repeat render, just call update() | |
if (instance !== undefined && instance.template === result.template && | |
instance._partCallback === partCallback) { | |
instance.update(result.values); | |
return; | |
} | |
// First render, create a new TemplateInstance and append it | |
instance = new TemplateInstance(result.template, partCallback); | |
container.__templateInstance = instance; | |
const fragment = instance._clone(); | |
instance.update(result.values); | |
let child; | |
while ((child = container.lastChild)) { | |
container.removeChild(child); | |
} | |
container.appendChild(fragment); | |
} | |
/** | |
* An expression marker with embedded unique key to avoid | |
* https://github.com/PolymerLabs/lit-html/issues/62 | |
*/ | |
const exprMarker = `{{lit-${Math.random()}}}`; | |
/** | |
* A placeholder for a dynamic expression in an HTML template. | |
* | |
* There are two built-in part types: AttributePart and NodePart. NodeParts | |
* always represent a single dynamic expression, while AttributeParts may | |
* represent as many expressions are contained in the attribute. | |
* | |
* A Template's parts are mutable, so parts can be replaced or modified | |
* (possibly to implement different template semantics). The contract is that | |
* parts can only be replaced, not removed, added or reordered, and parts must | |
* always consume the correct number of values in their `update()` method. | |
* | |
* TODO(justinfagnani): That requirement is a little fragile. A | |
* TemplateInstance could instead be more careful about which values it gives | |
* to Part.update(). | |
*/ | |
class TemplatePart { | |
constructor(type, index, name, rawName, strings) { | |
this.type = type; | |
this.index = index; | |
this.name = name; | |
this.rawName = rawName; | |
this.strings = strings; | |
} | |
} | |
class Template { | |
constructor(strings) { | |
this.parts = []; | |
this.element = document.createElement('template'); | |
this.element.innerHTML = strings.join(exprMarker); | |
const walker = document.createTreeWalker(this.element.content, 5 /* elements & text */); | |
let index = -1; | |
let partIndex = 0; | |
const nodesToRemove = []; | |
while (walker.nextNode()) { | |
index++; | |
const node = walker.currentNode; | |
if (node.nodeType === 1 /* ELEMENT_NODE */) { | |
if (!node.hasAttributes()) | |
continue; | |
const attributes = node.attributes; | |
for (let i = 0; i < attributes.length; i++) { | |
const attribute = attributes.item(i); | |
const attributeStrings = attribute.value.split(exprMarker); | |
if (attributeStrings.length > 1) { | |
// Get the template literal section leading up to the first | |
// expression in this attribute attribute | |
const attributeString = strings[partIndex]; | |
// Trim the trailing literal value if this is an interpolation | |
const rawNameString = attributeString.substring(0, attributeString.length - attributeStrings[0].length); | |
// Find the attribute name | |
const rawName = rawNameString.match(/((?:\w|[.\-_$])+)=["']?$/)[1]; | |
this.parts.push(new TemplatePart('attribute', index, attribute.name, rawName, attributeStrings)); | |
node.removeAttribute(attribute.name); | |
partIndex += attributeStrings.length - 1; | |
i--; | |
} | |
} | |
} | |
else if (node.nodeType === 3 /* TEXT_NODE */) { | |
const strings = node.nodeValue.split(exprMarker); | |
if (strings.length > 1) { | |
const parent = node.parentNode; | |
const lastIndex = strings.length - 1; | |
// We have a part for each match found | |
partIndex += 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', index++)); | |
} | |
} | |
else if (!node.nodeValue.trim()) { | |
nodesToRemove.push(node); | |
index--; | |
} | |
} | |
} | |
// Remove text binding nodes after the walk to not disturb the TreeWalker | |
for (const n of nodesToRemove) { | |
n.parentNode.removeChild(n); | |
} | |
} | |
} | |
const getValue = (part, value) => { | |
// `null` as the value of a Text node will render the string 'null' | |
// so we convert it to undefined | |
if (value != null && value.__litDirective === true) { | |
value = value(part); | |
} | |
return value === null ? undefined : value; | |
}; | |
const directive = (f) => { | |
f.__litDirective = true; | |
return f; | |
}; | |
class AttributePart { | |
constructor(instance, element, name, strings) { | |
this.instance = instance; | |
this.element = element; | |
this.name = name; | |
this.strings = strings; | |
this.size = strings.length - 1; | |
} | |
setValue(values, startIndex) { | |
const strings = this.strings; | |
let text = ''; | |
for (let i = 0; i < strings.length; i++) { | |
text += strings[i]; | |
if (i < strings.length - 1) { | |
const v = getValue(this, values[startIndex + i]); | |
if (v && | |
(Array.isArray(v) || typeof v !== 'string' && v[Symbol.iterator])) { | |
for (const t of v) { | |
// TODO: we need to recursively call getValue into iterables... | |
text += t; | |
} | |
} | |
else { | |
text += v; | |
} | |
} | |
} | |
this.element.setAttribute(this.name, text); | |
} | |
} | |
class NodePart { | |
constructor(instance, startNode, endNode) { | |
this.instance = instance; | |
this.startNode = startNode; | |
this.endNode = endNode; | |
} | |
setValue(value) { | |
value = getValue(this, 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._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 { | |
// Fallback, will render the string representation | |
this._setText(value); | |
} | |
} | |
_insert(node) { | |
this.endNode.parentNode.insertBefore(node, this.endNode); | |
} | |
_setNode(value) { | |
this.clear(); | |
this._insert(value); | |
this._previousValue = value; | |
} | |
_setText(value) { | |
const node = this.startNode.nextSibling; | |
if (node === this.endNode.previousSibling && | |
node.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? | |
node.textContent = value; | |
} | |
else { | |
this._setNode(new Text(value)); | |
} | |
this._previousValue = value; | |
} | |
_setTemplateResult(value) { | |
let instance; | |
if (this._previousValue && | |
this._previousValue.template === value.template) { | |
instance = this._previousValue; | |
} | |
else { | |
instance = | |
new TemplateInstance(value.template, this.instance._partCallback); | |
this._setNode(instance._clone()); | |
this._previousValue = instance; | |
} | |
instance.update(value.values); | |
} | |
_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 efficiently update Arrays | |
// of TemplateResults that will be commonly returned from expressions like: | |
// array.map((i) => html`${i}`), by reusing existing TemplateInstances. | |
// If _previousValue is an array, then the previous render was of an | |
// iterable and _previousValue will contain the NodeParts from the previous | |
// render. If _previousValue is not an array, clear this part and make a new | |
// array for NodeParts. | |
if (!Array.isArray(this._previousValue)) { | |
this.clear(); | |
this._previousValue = []; | |
} | |
// 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); | |
} | |
itemPart = new NodePart(this.instance, itemStart, this.endNode); | |
itemParts.push(itemPart); | |
} | |
itemPart.setValue(item); | |
partIndex++; | |
} | |
if (partIndex === 0) { | |
this.clear(); | |
this._previousValue = undefined; | |
} | |
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) { | |
let node; | |
while ((node = startNode.nextSibling) !== this.endNode) { | |
node.parentNode.removeChild(node); | |
} | |
} | |
} | |
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. | |
*/ | |
class TemplateInstance { | |
constructor(template, partCallback = defaultPartCallback) { | |
this._parts = []; | |
this.template = template; | |
this._partCallback = partCallback; | |
} | |
update(values) { | |
let valueIndex = 0; | |
for (const part of this._parts) { | |
if (part.size === undefined) { | |
part.setValue(values[valueIndex]); | |
valueIndex++; | |
} | |
else { | |
part.setValue(values, valueIndex); | |
valueIndex += part.size; | |
} | |
} | |
} | |
_clone() { | |
const fragment = document.importNode(this.template.element.content, true); | |
if (this.template.parts.length > 0) { | |
const walker = document.createTreeWalker(fragment, 5 /* elements & 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._partCallback(this, templatePart, node)); | |
templatePart = parts[++partIndex]; | |
} | |
else { | |
index++; | |
node = walker.nextNode(); | |
} | |
} | |
} | |
return fragment; | |
} | |
} | |
exports.html = html; | |
exports.TemplateResult = TemplateResult; | |
exports.render = render; | |
exports.TemplatePart = TemplatePart; | |
exports.Template = Template; | |
exports.getValue = getValue; | |
exports.directive = directive; | |
exports.AttributePart = AttributePart; | |
exports.NodePart = NodePart; | |
exports.defaultPartCallback = defaultPartCallback; | |
exports.TemplateInstance = TemplateInstance; | |
Object.defineProperty(exports, '__esModule', { value: true }); | |
}))); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment