Skip to content

Instantly share code, notes, and snippets.

@robwormald
Created September 29, 2017 19:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save robwormald/7a05787e38d93e2ed4b5114b28be489b to your computer and use it in GitHub Desktop.
Save robwormald/7a05787e38d93e2ed4b5114b28be489b to your computer and use it in GitHub Desktop.
(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