Comparing version 0.8.0 to 0.9.0
@@ -15,2 +15,16 @@ # Change Log | ||
## [0.9.0] - 2018-02-01 | ||
* Refactored how template tags and `render()` are implemented so that all | ||
specialization of template syntax is done in tags, not `render()`, allowing | ||
for the mixining of templates of different syntaxes, and for hooks in | ||
`render()` to change templates before they're initially processed. | ||
* Added ShadyCSS support in lib/shady-render.js. It's exported render function | ||
will pass templates to ShadyCSS's `prepareTemplate()` function to process style | ||
tags and elements in the template for emulate CSS scoping. | ||
* lit-extended: Attribute bindings with a `?` suffix on the name now act as boolean | ||
attributes. The attribute will be removed for falsey values and set to `''` for | ||
truthy values, matching the HTML specification behavior for boolean attributes. | ||
* Fixed a bug where directives rendered incorrectly on AttributeParts and PropertyParts | ||
## [0.8.0] - 2018-01-12 | ||
@@ -17,0 +31,0 @@ |
@@ -52,4 +52,4 @@ /** | ||
let v = await value_1_1.value; | ||
// When we get the first value, clear the part. This let's the previous | ||
// value display until we can replace it. | ||
// When we get the first value, clear the part. This let's the | ||
// previous value display until we can replace it. | ||
if (i === 0) { | ||
@@ -56,0 +56,0 @@ part.clear(); |
@@ -14,10 +14,16 @@ /** | ||
*/ | ||
import { AttributePart, Part, TemplateInstance, TemplatePart, TemplateResult } from '../lit-html.js'; | ||
export { html } from '../lit-html.js'; | ||
import { AttributePart, Part, SVGTemplateResult, TemplateInstance, TemplatePart, TemplateResult } from '../lit-html.js'; | ||
export { render } from '../lit-html.js'; | ||
/** | ||
* Interprets a template literal as a lit-extended HTML template. | ||
*/ | ||
export declare const html: (strings: TemplateStringsArray, ...values: any[]) => TemplateResult; | ||
/** | ||
* Interprets a template literal as a lit-extended SVG template. | ||
*/ | ||
export declare const svg: (strings: TemplateStringsArray, ...values: any[]) => SVGTemplateResult; | ||
/** | ||
* A PartCallback which allows templates to set properties and declarative | ||
* event handlers. | ||
* | ||
* @param result Renders a `TemplateResult` to a container using the | ||
* `extendedPartCallback` PartCallback, which allows templates to set | ||
* properties and declarative event handlers. | ||
* | ||
* Properties are set by default, instead of attributes. Attribute names in | ||
@@ -44,4 +50,13 @@ * lit-html templates preserve case, so properties are case sensitive. If an | ||
*/ | ||
export declare function render(result: TemplateResult, container: Element | DocumentFragment): void; | ||
export declare const extendedPartCallback: (instance: TemplateInstance, templatePart: TemplatePart, node: Node) => Part; | ||
/** | ||
* Implements a boolean attribute, roughly as defined in the HTML | ||
* specification. | ||
* | ||
* If the value is truthy, then the attribute is present with a value of | ||
* ''. If the value is falsey, the attribute is removed. | ||
*/ | ||
export declare class BooleanAttributePart extends AttributePart { | ||
setValue(values: any[], startIndex: number): void; | ||
} | ||
export declare class PropertyPart extends AttributePart { | ||
@@ -48,0 +63,0 @@ setValue(values: any[], startIndex: number): void; |
@@ -14,10 +14,16 @@ /** | ||
*/ | ||
import { AttributePart, defaultPartCallback, getValue, render as baseRender } from '../lit-html.js'; | ||
export { html } from '../lit-html.js'; | ||
import { AttributePart, defaultPartCallback, directiveValue, getValue, SVGTemplateResult, TemplateResult } from '../lit-html.js'; | ||
export { render } from '../lit-html.js'; | ||
/** | ||
* Interprets a template literal as a lit-extended HTML template. | ||
*/ | ||
export const html = (strings, ...values) => new TemplateResult(strings, values, 'html', extendedPartCallback); | ||
/** | ||
* Interprets a template literal as a lit-extended SVG template. | ||
*/ | ||
export const svg = (strings, ...values) => new SVGTemplateResult(strings, values, 'svg', extendedPartCallback); | ||
/** | ||
* A PartCallback which allows templates to set properties and declarative | ||
* event handlers. | ||
* | ||
* @param result Renders a `TemplateResult` to a container using the | ||
* `extendedPartCallback` PartCallback, which allows templates to set | ||
* properties and declarative event handlers. | ||
* | ||
* Properties are set by default, instead of attributes. Attribute names in | ||
@@ -44,5 +50,2 @@ * lit-html templates preserve case, so properties are case sensitive. If an | ||
*/ | ||
export function render(result, container) { | ||
baseRender(result, container, extendedPartCallback); | ||
} | ||
export const extendedPartCallback = (instance, templatePart, node) => { | ||
@@ -58,2 +61,6 @@ if (templatePart.type === 'attribute') { | ||
} | ||
if (templatePart.name.endsWith('?')) { | ||
const name = templatePart.name.slice(0, -1); | ||
return new BooleanAttributePart(instance, node, name, templatePart.strings); | ||
} | ||
return new PropertyPart(instance, node, templatePart.rawName, templatePart.strings); | ||
@@ -63,2 +70,29 @@ } | ||
}; | ||
/** | ||
* Implements a boolean attribute, roughly as defined in the HTML | ||
* specification. | ||
* | ||
* If the value is truthy, then the attribute is present with a value of | ||
* ''. If the value is falsey, the attribute is removed. | ||
*/ | ||
export class BooleanAttributePart extends AttributePart { | ||
setValue(values, startIndex) { | ||
const s = this.strings; | ||
if (s.length === 2 && s[0] === '' && s[1] === '') { | ||
const value = getValue(this, values[startIndex]); | ||
if (value === directiveValue) { | ||
return; | ||
} | ||
if (value) { | ||
this.element.setAttribute(this.name, ''); | ||
} | ||
else { | ||
this.element.removeAttribute(this.name); | ||
} | ||
} | ||
else { | ||
throw new Error('boolean attributes can only contain a single expression'); | ||
} | ||
} | ||
} | ||
export class PropertyPart extends AttributePart { | ||
@@ -68,2 +102,5 @@ setValue(values, startIndex) { | ||
let value; | ||
if (this._equalToPreviousValues(values, startIndex)) { | ||
return; | ||
} | ||
if (s.length === 2 && s[0] === '' && s[1] === '') { | ||
@@ -78,3 +115,6 @@ // An expression that occupies the whole attribute value will leave | ||
} | ||
this.element[this.name] = value; | ||
if (value !== directiveValue) { | ||
this.element[this.name] = value; | ||
} | ||
this._previousValues = values; | ||
} | ||
@@ -81,0 +121,0 @@ } |
/** | ||
* @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 | ||
*/ | ||
export declare const templateCaches: Map<string, Map<TemplateStringsArray, Template>>; | ||
/** | ||
* Interprets a template literal as an HTML template that can efficiently | ||
@@ -10,3 +24,3 @@ * render to and update a container. | ||
*/ | ||
export declare const svg: (strings: TemplateStringsArray, ...values: any[]) => TemplateResult; | ||
export declare const svg: (strings: TemplateStringsArray, ...values: any[]) => SVGTemplateResult; | ||
/** | ||
@@ -17,7 +31,51 @@ * The return type of `html`, which holds a Template and the values from | ||
export declare class TemplateResult { | ||
template: Template; | ||
strings: TemplateStringsArray; | ||
values: any[]; | ||
constructor(template: Template, values: any[]); | ||
type: string; | ||
partCallback: PartCallback; | ||
constructor(strings: TemplateStringsArray, values: any[], type: string, partCallback?: PartCallback); | ||
/** | ||
* Returns a string of HTML used to create a <template> element. | ||
*/ | ||
getHTML(): string; | ||
getTemplateElement(): HTMLTemplateElement; | ||
} | ||
/** | ||
* A TemplateResult for SVG fragments. | ||
* | ||
* This class wraps HTMl in an <svg> tag in order to parse its contents in the | ||
* SVG namespace, then modifies the template to remove the <svg> tag so that | ||
* clones only container the original fragment. | ||
*/ | ||
export declare class SVGTemplateResult extends TemplateResult { | ||
getHTML(): string; | ||
getTemplateElement(): HTMLTemplateElement; | ||
} | ||
/** | ||
* A function type that creates a Template from a TemplateResult. | ||
* | ||
* This is a hook into the template-creation process for rendering that | ||
* requires some modification of templates before their used, like ShadyCSS, | ||
* which must add classes to elements and remove styles. | ||
* | ||
* Templates should be cached as aggressively as possible, so that many | ||
* TemplateResults produced from the same expression only do the work of | ||
* creating the Template the first time. | ||
* | ||
* Templates are usually cached by TemplateResult.strings and | ||
* TemplateResult.type, but may be cached by other keys if this function | ||
* modifies the template. | ||
* | ||
* Note that currently TemplateFactories must not add, remove, or reorder | ||
* expressions, because there is no way to describe such a modification | ||
* to render() so that values are interpolated to the correct place in the | ||
* template instances. | ||
*/ | ||
export declare type TemplateFactory = (result: TemplateResult) => Template; | ||
/** | ||
* The default TemplateFactory which caches Templates keyed on | ||
* result.type and result.strings. | ||
*/ | ||
export declare function defaultTemplateFactory(result: TemplateResult): Template; | ||
/** | ||
* Renders a template to a container. | ||
@@ -27,4 +85,12 @@ * | ||
* call `render` with the new result. | ||
* | ||
* @param result a TemplateResult created by evaluating a template tag like | ||
* `html` or `svg. | ||
* @param container A DOM parent to render to. The entire contents are either | ||
* replaced, or efficiently updated if the same result type was previous | ||
* rendered there. | ||
* @param templateFactory a function to create a Template or retreive one from | ||
* cache. | ||
*/ | ||
export declare function render(result: TemplateResult, container: Element | DocumentFragment, partCallback?: PartCallback): void; | ||
export declare function render(result: TemplateResult, container: Element | DocumentFragment, templateFactory?: TemplateFactory): void; | ||
/** | ||
@@ -54,10 +120,9 @@ * A placeholder for a dynamic expression in an HTML template. | ||
} | ||
/** | ||
* An updateable Template that tracks the location of dynamic parts. | ||
*/ | ||
export declare class Template { | ||
parts: TemplatePart[]; | ||
element: HTMLTemplateElement; | ||
constructor(strings: TemplateStringsArray, svg?: boolean); | ||
/** | ||
* Returns a string of HTML used to create a <template> element. | ||
*/ | ||
private _getHtml(strings, svg?); | ||
constructor(result: TemplateResult, element: HTMLTemplateElement); | ||
} | ||
@@ -74,2 +139,7 @@ /** | ||
export declare const directive: <P extends Part = Part, F = DirectiveFn<P>>(f: F) => F; | ||
/** | ||
* A sentinel value that signals that a value was handled by a directive and | ||
* should not be written to the DOM. | ||
*/ | ||
export declare const directiveValue: {}; | ||
export interface Part { | ||
@@ -91,4 +161,6 @@ instance: TemplateInstance; | ||
size: number; | ||
_previousValues: any; | ||
constructor(instance: TemplateInstance, element: Element, name: string, strings: string[]); | ||
protected _interpolate(values: any[], startIndex: number): string; | ||
protected _equalToPreviousValues(values: any[], startIndex: number): boolean; | ||
setValue(values: any[], startIndex: number): void; | ||
@@ -120,4 +192,5 @@ } | ||
_partCallback: PartCallback; | ||
_getTemplate: TemplateFactory; | ||
template: Template; | ||
constructor(template: Template, partCallback?: PartCallback); | ||
constructor(template: Template, partCallback: PartCallback, getTemplate: TemplateFactory); | ||
update(values: any[]): void; | ||
@@ -124,0 +197,0 @@ _clone(): DocumentFragment; |
210
lit-html.js
@@ -14,15 +14,6 @@ /** | ||
*/ | ||
/** | ||
* TypeScript has a problem with precompiling templates literals | ||
* https://github.com/Microsoft/TypeScript/issues/17956 | ||
* | ||
* TODO(justinfagnani): Run tests compiled to ES5 with both Babel and | ||
* TypeScript to verify correctness. | ||
*/ | ||
const envCachesTemplates = ((t) => t() === t())(() => ((s) => s) ``); | ||
// 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(); | ||
const svgTemplates = new Map(); | ||
export const templateCaches = new Map(); | ||
/** | ||
@@ -32,3 +23,3 @@ * Interprets a template literal as an HTML template that can efficiently | ||
*/ | ||
export const html = (strings, ...values) => litTag(strings, values, templates, false); | ||
export const html = (strings, ...values) => new TemplateResult(strings, values, 'html'); | ||
/** | ||
@@ -38,14 +29,3 @@ * Interprets a template literal as an SVG template that can efficiently | ||
*/ | ||
export const svg = (strings, ...values) => litTag(strings, values, svgTemplates, true); | ||
function litTag(strings, values, templates, isSvg) { | ||
const key = envCachesTemplates ? | ||
strings : | ||
strings.join('{{--uniqueness-workaround--}}'); | ||
let template = templates.get(key); | ||
if (template === undefined) { | ||
template = new Template(strings, isSvg); | ||
templates.set(key, template); | ||
} | ||
return new TemplateResult(template, values); | ||
} | ||
export const svg = (strings, ...values) => new SVGTemplateResult(strings, values, 'svg'); | ||
/** | ||
@@ -56,8 +36,72 @@ * The return type of `html`, which holds a Template and the values from | ||
export class TemplateResult { | ||
constructor(template, values) { | ||
this.template = template; | ||
constructor(strings, values, type, partCallback = defaultPartCallback) { | ||
this.strings = strings; | ||
this.values = values; | ||
this.type = type; | ||
this.partCallback = partCallback; | ||
} | ||
/** | ||
* Returns a string of HTML used to create a <template> element. | ||
*/ | ||
getHTML() { | ||
const l = this.strings.length - 1; | ||
let html = ''; | ||
let isTextBinding = true; | ||
for (let i = 0; i < l; i++) { | ||
const s = this.strings[i]; | ||
html += s; | ||
// We're in a text position if the previous string closed its tags. | ||
// If it doesn't have any tags, then we use the previous text position | ||
// state. | ||
const closing = findTagClose(s); | ||
isTextBinding = closing > -1 ? closing < s.length : isTextBinding; | ||
html += isTextBinding ? nodeMarker : marker; | ||
} | ||
html += this.strings[l]; | ||
return html; | ||
} | ||
getTemplateElement() { | ||
const template = document.createElement('template'); | ||
template.innerHTML = this.getHTML(); | ||
return template; | ||
} | ||
} | ||
/** | ||
* A TemplateResult for SVG fragments. | ||
* | ||
* This class wraps HTMl in an <svg> tag in order to parse its contents in the | ||
* SVG namespace, then modifies the template to remove the <svg> tag so that | ||
* clones only container the original fragment. | ||
*/ | ||
export class SVGTemplateResult extends TemplateResult { | ||
getHTML() { | ||
return `<svg>${super.getHTML()}</svg>`; | ||
} | ||
getTemplateElement() { | ||
const template = super.getTemplateElement(); | ||
const content = template.content; | ||
const svgElement = content.firstChild; | ||
content.removeChild(svgElement); | ||
reparentNodes(content, svgElement.firstChild); | ||
return template; | ||
} | ||
} | ||
/** | ||
* The default TemplateFactory which caches Templates keyed on | ||
* result.type and result.strings. | ||
*/ | ||
export function defaultTemplateFactory(result) { | ||
let templateCache = templateCaches.get(result.type); | ||
if (templateCache === undefined) { | ||
templateCache = new Map(); | ||
templateCaches.set(result.type, templateCache); | ||
} | ||
let template = templateCache.get(result.strings); | ||
if (template === undefined) { | ||
template = new Template(result, result.getTemplateElement()); | ||
templateCache.set(result.strings, template); | ||
} | ||
return template; | ||
} | ||
/** | ||
* Renders a template to a container. | ||
@@ -67,8 +111,17 @@ * | ||
* call `render` with the new result. | ||
* | ||
* @param result a TemplateResult created by evaluating a template tag like | ||
* `html` or `svg. | ||
* @param container A DOM parent to render to. The entire contents are either | ||
* replaced, or efficiently updated if the same result type was previous | ||
* rendered there. | ||
* @param templateFactory a function to create a Template or retreive one from | ||
* cache. | ||
*/ | ||
export function render(result, container, partCallback = defaultPartCallback) { | ||
export function render(result, container, templateFactory = defaultTemplateFactory) { | ||
const template = templateFactory(result); | ||
let instance = container.__templateInstance; | ||
// Repeat render, just call update() | ||
if (instance !== undefined && instance.template === result.template && | ||
instance._partCallback === partCallback) { | ||
if (instance !== undefined && instance.template === template && | ||
instance._partCallback === result.partCallback) { | ||
instance.update(result.values); | ||
@@ -78,3 +131,4 @@ return; | ||
// First render, create a new TemplateInstance and append it | ||
instance = new TemplateInstance(result.template, partCallback); | ||
instance = | ||
new TemplateInstance(template, result.partCallback, templateFactory); | ||
container.__templateInstance = instance; | ||
@@ -87,6 +141,10 @@ const fragment = instance._clone(); | ||
/** | ||
* An expression marker with embedded unique key to avoid | ||
* https://github.com/PolymerLabs/lit-html/issues/62 | ||
* An expression marker with embedded unique key to avoid collision with | ||
* possible text in templates. | ||
*/ | ||
const marker = `{{lit-${String(Math.random()).slice(2)}}}`; | ||
/** | ||
* An expression marker used text-posisitions, not attribute positions, | ||
* in template. | ||
*/ | ||
const nodeMarker = `<!--${marker}-->`; | ||
@@ -157,13 +215,10 @@ const markerRegex = new RegExp(`${marker}|${nodeMarker}`); | ||
} | ||
/** | ||
* An updateable Template that tracks the location of dynamic parts. | ||
*/ | ||
export class Template { | ||
constructor(strings, svg = false) { | ||
constructor(result, element) { | ||
this.parts = []; | ||
const element = this.element = document.createElement('template'); | ||
element.innerHTML = this._getHtml(strings, svg); | ||
const content = element.content; | ||
if (svg) { | ||
const svgElement = content.firstChild; | ||
content.removeChild(svgElement); | ||
reparentNodes(content, svgElement.firstChild); | ||
} | ||
this.element = element; | ||
const content = this.element.content; | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null | ||
@@ -202,3 +257,3 @@ const walker = document.createTreeWalker(content, 133 /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | | ||
// expression in this attribute attribute | ||
const stringForPart = strings[partIndex]; | ||
const stringForPart = result.strings[partIndex]; | ||
// Find the attribute name | ||
@@ -276,22 +331,2 @@ const attributeNameInPart = lastAttributeNameRegex.exec(stringForPart)[1]; | ||
} | ||
/** | ||
* Returns a string of HTML used to create a <template> element. | ||
*/ | ||
_getHtml(strings, svg) { | ||
const l = strings.length - 1; | ||
let html = ''; | ||
let isTextBinding = true; | ||
for (let i = 0; i < l; i++) { | ||
const s = strings[i]; | ||
html += s; | ||
// We're in a text position if the previous string closed its tags. | ||
// If it doesn't have any tags, then we use the previous text position | ||
// state. | ||
const closing = findTagClose(s); | ||
isTextBinding = closing > -1 ? closing < s.length : isTextBinding; | ||
html += isTextBinding ? nodeMarker : marker; | ||
} | ||
html += strings[l]; | ||
return svg ? `<svg>${html}</svg>` : html; | ||
} | ||
} | ||
@@ -319,3 +354,9 @@ /** | ||
const isDirective = (o) => typeof o === 'function' && o.__litDirective === true; | ||
const directiveValue = {}; | ||
/** | ||
* A sentinel value that signals that a value was handled by a directive and | ||
* should not be written to the DOM. | ||
*/ | ||
export const directiveValue = {}; | ||
const isPrimitiveValue = (value) => value === null || | ||
!(typeof value === 'object' || typeof value === 'function'); | ||
export class AttributePart { | ||
@@ -328,2 +369,3 @@ constructor(instance, element, name, strings) { | ||
this.size = strings.length - 1; | ||
this._previousValues = []; | ||
} | ||
@@ -350,5 +392,32 @@ _interpolate(values, startIndex) { | ||
} | ||
_equalToPreviousValues(values, startIndex) { | ||
for (let i = startIndex; i < startIndex + this.size; i++) { | ||
if (this._previousValues[i] !== values[i] || | ||
!isPrimitiveValue(values[i])) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
setValue(values, startIndex) { | ||
const text = this._interpolate(values, startIndex); | ||
this.element.setAttribute(this.name, text); | ||
if (this._equalToPreviousValues(values, startIndex)) { | ||
return; | ||
} | ||
const s = this.strings; | ||
let value; | ||
if (s.length === 2 && s[0] === '' && s[1] === '') { | ||
// An expression that occupies the whole attribute value will leave | ||
// leading and trailing empty strings. | ||
value = getValue(this, values[startIndex]); | ||
if (Array.isArray(value)) { | ||
value = value.join(''); | ||
} | ||
} | ||
else { | ||
value = this._interpolate(values, startIndex); | ||
} | ||
if (value !== directiveValue) { | ||
this.element.setAttribute(this.name, value); | ||
} | ||
this._previousValues = values; | ||
} | ||
@@ -368,4 +437,3 @@ } | ||
} | ||
if (value === null || | ||
!(typeof value === 'object' || typeof value === 'function')) { | ||
if (isPrimitiveValue(value)) { | ||
// Handle primitive values | ||
@@ -423,10 +491,9 @@ // If the value didn't change, do nothing | ||
_setTemplateResult(value) { | ||
const template = this.instance._getTemplate(value); | ||
let instance; | ||
if (this._previousValue && | ||
this._previousValue.template === value.template) { | ||
if (this._previousValue && this._previousValue.template === template) { | ||
instance = this._previousValue; | ||
} | ||
else { | ||
instance = | ||
new TemplateInstance(value.template, this.instance._partCallback); | ||
instance = new TemplateInstance(template, this.instance._partCallback, this.instance._getTemplate); | ||
this._setNode(instance._clone()); | ||
@@ -514,6 +581,7 @@ this._previousValue = instance; | ||
export class TemplateInstance { | ||
constructor(template, partCallback = defaultPartCallback) { | ||
constructor(template, partCallback, getTemplate) { | ||
this._parts = []; | ||
this.template = template; | ||
this._partCallback = partCallback; | ||
this._getTemplate = getTemplate; | ||
} | ||
@@ -520,0 +588,0 @@ update(values) { |
{ | ||
"name": "lit-html", | ||
"version": "0.8.0", | ||
"version": "0.9.0", | ||
"description": "HTML template literals in JavaScript", | ||
"license": "BSD-3-Clause", | ||
"repository": "PolymerLabs/lit-html", | ||
"repository": "Polymer/lit-html", | ||
"main": "lit-html.js", | ||
@@ -22,3 +22,3 @@ "module": "lit-html.js", | ||
"build": "tsc", | ||
"gen-docs": "typedoc --readme none --mode modules --excludeNotExported --excludePrivate --exclude **/*_test.ts --out ./docs/api .", | ||
"gen-docs": "typedoc --readme none --mode modules --excludeNotExported --excludePrivate --exclude **/*_test.ts --out ./docs/api ./src", | ||
"test": "npm run build && wct --npm && npm run lint", | ||
@@ -35,2 +35,4 @@ "quicktest": "wct -l chrome -p --npm", | ||
"@types/mocha": "^2.2.46", | ||
"@webcomponents/shadycss": "^1.1.0", | ||
"@webcomponents/shadydom": "^1.0.11", | ||
"@webcomponents/template": "^1.2.2", | ||
@@ -37,0 +39,0 @@ "babel-polyfill": "^6.26.0", |
# 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) | ||
[![Build Status](https://travis-ci.org/Polymer/lit-html.svg?branch=master)](https://travis-ci.org/Polymer/lit-html) | ||
@@ -388,6 +388,2 @@ ## Overview | ||
### Async Iterables Support | ||
Async Iterables should be supported natively. | ||
### Higher-Order Templates examples | ||
@@ -394,0 +390,0 @@ |
@@ -54,4 +54,4 @@ /** | ||
for await (let v of value) { | ||
// When we get the first value, clear the part. This let's the previous | ||
// value display until we can replace it. | ||
// When we get the first value, clear the part. This let's the | ||
// previous value display until we can replace it. | ||
if (i === 0) { | ||
@@ -58,0 +58,0 @@ part.clear(); |
@@ -15,12 +15,22 @@ /** | ||
import {AttributePart, defaultPartCallback, getValue, Part, render as baseRender, TemplateInstance, TemplatePart, TemplateResult} from '../lit-html.js'; | ||
import {AttributePart, defaultPartCallback, directiveValue, getValue, Part, SVGTemplateResult, TemplateInstance, TemplatePart, TemplateResult} from '../lit-html.js'; | ||
export {html} from '../lit-html.js'; | ||
export {render} from '../lit-html.js'; | ||
/** | ||
* Interprets a template literal as a lit-extended HTML template. | ||
*/ | ||
export const html = (strings: TemplateStringsArray, ...values: any[]) => | ||
new TemplateResult(strings, values, 'html', extendedPartCallback); | ||
/** | ||
* Interprets a template literal as a lit-extended SVG template. | ||
*/ | ||
export const svg = (strings: TemplateStringsArray, ...values: any[]) => | ||
new SVGTemplateResult(strings, values, 'svg', extendedPartCallback); | ||
/** | ||
* A PartCallback which allows templates to set properties and declarative | ||
* event handlers. | ||
* | ||
* @param result Renders a `TemplateResult` to a container using the | ||
* `extendedPartCallback` PartCallback, which allows templates to set | ||
* properties and declarative event handlers. | ||
* | ||
* Properties are set by default, instead of attributes. Attribute names in | ||
@@ -47,7 +57,2 @@ * lit-html templates preserve case, so properties are case sensitive. If an | ||
*/ | ||
export function render( | ||
result: TemplateResult, container: Element|DocumentFragment) { | ||
baseRender(result, container, extendedPartCallback); | ||
} | ||
export const extendedPartCallback = | ||
@@ -66,2 +71,7 @@ (instance: TemplateInstance, templatePart: TemplatePart, node: Node): | ||
} | ||
if (templatePart.name!.endsWith('?')) { | ||
const name = templatePart.name!.slice(0, -1); | ||
return new BooleanAttributePart( | ||
instance, node as Element, name, templatePart.strings!); | ||
} | ||
return new PropertyPart( | ||
@@ -76,2 +86,29 @@ instance, | ||
/** | ||
* Implements a boolean attribute, roughly as defined in the HTML | ||
* specification. | ||
* | ||
* If the value is truthy, then the attribute is present with a value of | ||
* ''. If the value is falsey, the attribute is removed. | ||
*/ | ||
export class BooleanAttributePart extends AttributePart { | ||
setValue(values: any[], startIndex: number): void { | ||
const s = this.strings; | ||
if (s.length === 2 && s[0] === '' && s[1] === '') { | ||
const value = getValue(this, values[startIndex]); | ||
if (value === directiveValue) { | ||
return; | ||
} | ||
if (value) { | ||
this.element.setAttribute(this.name, ''); | ||
} else { | ||
this.element.removeAttribute(this.name); | ||
} | ||
} else { | ||
throw new Error( | ||
'boolean attributes can only contain a single expression'); | ||
} | ||
} | ||
} | ||
export class PropertyPart extends AttributePart { | ||
@@ -81,2 +118,5 @@ setValue(values: any[], startIndex: number): void { | ||
let value: any; | ||
if (this._equalToPreviousValues(values, startIndex)) { | ||
return; | ||
} | ||
if (s.length === 2 && s[0] === '' && s[1] === '') { | ||
@@ -90,3 +130,7 @@ // An expression that occupies the whole attribute value will leave | ||
} | ||
(this.element as any)[this.name] = value; | ||
if (value !== directiveValue) { | ||
(this.element as any)[this.name] = value; | ||
} | ||
this._previousValues = values; | ||
} | ||
@@ -93,0 +137,0 @@ } |
@@ -15,3 +15,3 @@ /** | ||
import {directive, DirectiveFn, NodePart, removeNodes, reparentNodes, Part} from '../lit-html.js'; | ||
import {directive, DirectiveFn, NodePart, Part, removeNodes, reparentNodes} from '../lit-html.js'; | ||
@@ -85,7 +85,3 @@ export type KeyFn<T> = (item: T) => any; | ||
if (currentMarker !== end) { | ||
reparentNodes( | ||
container, | ||
itemPart.startNode, | ||
end, | ||
currentMarker); | ||
reparentNodes(container, itemPart.startNode, end, currentMarker); | ||
} | ||
@@ -92,0 +88,0 @@ } else { |
@@ -15,17 +15,7 @@ /** | ||
/** | ||
* TypeScript has a problem with precompiling templates literals | ||
* https://github.com/Microsoft/TypeScript/issues/17956 | ||
* | ||
* TODO(justinfagnani): Run tests compiled to ES5 with both Babel and | ||
* TypeScript to verify correctness. | ||
*/ | ||
const envCachesTemplates = | ||
((t: any) => t() === t())(() => ((s: TemplateStringsArray) => s) ``); | ||
// 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<TemplateStringsArray|string, Template>(); | ||
const svgTemplates = new Map<TemplateStringsArray|string, Template>(); | ||
export const templateCaches = | ||
new Map<string, Map<TemplateStringsArray, Template>>(); | ||
@@ -37,3 +27,3 @@ /** | ||
export const html = (strings: TemplateStringsArray, ...values: any[]) => | ||
litTag(strings, values, templates, false); | ||
new TemplateResult(strings, values, 'html'); | ||
@@ -45,20 +35,4 @@ /** | ||
export const svg = (strings: TemplateStringsArray, ...values: any[]) => | ||
litTag(strings, values, svgTemplates, true); | ||
new SVGTemplateResult(strings, values, 'svg'); | ||
function litTag( | ||
strings: TemplateStringsArray, | ||
values: any[], | ||
templates: Map<TemplateStringsArray|string, Template>, | ||
isSvg: boolean): TemplateResult { | ||
const key = envCachesTemplates ? | ||
strings : | ||
strings.join('{{--uniqueness-workaround--}}'); | ||
let template = templates.get(key); | ||
if (template === undefined) { | ||
template = new Template(strings, isSvg); | ||
templates.set(key, template); | ||
} | ||
return new TemplateResult(template, values); | ||
} | ||
/** | ||
@@ -69,12 +43,106 @@ * The return type of `html`, which holds a Template and the values from | ||
export class TemplateResult { | ||
template: Template; | ||
strings: TemplateStringsArray; | ||
values: any[]; | ||
type: string; | ||
partCallback: PartCallback; | ||
constructor(template: Template, values: any[]) { | ||
this.template = template; | ||
constructor( | ||
strings: TemplateStringsArray, values: any[], type: string, | ||
partCallback: PartCallback = defaultPartCallback) { | ||
this.strings = strings; | ||
this.values = values; | ||
this.type = type; | ||
this.partCallback = partCallback; | ||
} | ||
/** | ||
* Returns a string of HTML used to create a <template> element. | ||
*/ | ||
getHTML(): string { | ||
const l = this.strings.length - 1; | ||
let html = ''; | ||
let isTextBinding = true; | ||
for (let i = 0; i < l; i++) { | ||
const s = this.strings[i]; | ||
html += s; | ||
// We're in a text position if the previous string closed its tags. | ||
// If it doesn't have any tags, then we use the previous text position | ||
// state. | ||
const closing = findTagClose(s); | ||
isTextBinding = closing > -1 ? closing < s.length : isTextBinding; | ||
html += isTextBinding ? nodeMarker : marker; | ||
} | ||
html += this.strings[l]; | ||
return html; | ||
} | ||
getTemplateElement(): HTMLTemplateElement { | ||
const template = document.createElement('template'); | ||
template.innerHTML = this.getHTML(); | ||
return template; | ||
} | ||
} | ||
/** | ||
* A TemplateResult for SVG fragments. | ||
* | ||
* This class wraps HTMl in an <svg> tag in order to parse its contents in the | ||
* SVG namespace, then modifies the template to remove the <svg> tag so that | ||
* clones only container the original fragment. | ||
*/ | ||
export class SVGTemplateResult extends TemplateResult { | ||
getHTML(): string { | ||
return `<svg>${super.getHTML()}</svg>`; | ||
} | ||
getTemplateElement(): HTMLTemplateElement { | ||
const template = super.getTemplateElement(); | ||
const content = template.content; | ||
const svgElement = content.firstChild!; | ||
content.removeChild(svgElement); | ||
reparentNodes(content, svgElement.firstChild); | ||
return template; | ||
} | ||
} | ||
/** | ||
* A function type that creates a Template from a TemplateResult. | ||
* | ||
* This is a hook into the template-creation process for rendering that | ||
* requires some modification of templates before their used, like ShadyCSS, | ||
* which must add classes to elements and remove styles. | ||
* | ||
* Templates should be cached as aggressively as possible, so that many | ||
* TemplateResults produced from the same expression only do the work of | ||
* creating the Template the first time. | ||
* | ||
* Templates are usually cached by TemplateResult.strings and | ||
* TemplateResult.type, but may be cached by other keys if this function | ||
* modifies the template. | ||
* | ||
* Note that currently TemplateFactories must not add, remove, or reorder | ||
* expressions, because there is no way to describe such a modification | ||
* to render() so that values are interpolated to the correct place in the | ||
* template instances. | ||
*/ | ||
export type TemplateFactory = (result: TemplateResult) => Template; | ||
/** | ||
* The default TemplateFactory which caches Templates keyed on | ||
* result.type and result.strings. | ||
*/ | ||
export function defaultTemplateFactory(result: TemplateResult) { | ||
let templateCache = templateCaches.get(result.type); | ||
if (templateCache === undefined) { | ||
templateCache = new Map<TemplateStringsArray, Template>(); | ||
templateCaches.set(result.type, templateCache); | ||
} | ||
let template = templateCache.get(result.strings); | ||
if (template === undefined) { | ||
template = new Template(result, result.getTemplateElement()); | ||
templateCache.set(result.strings, template); | ||
} | ||
return template; | ||
} | ||
/** | ||
* Renders a template to a container. | ||
@@ -84,2 +152,10 @@ * | ||
* call `render` with the new result. | ||
* | ||
* @param result a TemplateResult created by evaluating a template tag like | ||
* `html` or `svg. | ||
* @param container A DOM parent to render to. The entire contents are either | ||
* replaced, or efficiently updated if the same result type was previous | ||
* rendered there. | ||
* @param templateFactory a function to create a Template or retreive one from | ||
* cache. | ||
*/ | ||
@@ -89,8 +165,9 @@ export function render( | ||
container: Element|DocumentFragment, | ||
partCallback: PartCallback = defaultPartCallback) { | ||
templateFactory: TemplateFactory = defaultTemplateFactory) { | ||
const template = templateFactory(result); | ||
let instance = (container as any).__templateInstance as any; | ||
// Repeat render, just call update() | ||
if (instance !== undefined && instance.template === result.template && | ||
instance._partCallback === partCallback) { | ||
if (instance !== undefined && instance.template === template && | ||
instance._partCallback === result.partCallback) { | ||
instance.update(result.values); | ||
@@ -101,3 +178,4 @@ return; | ||
// First render, create a new TemplateInstance and append it | ||
instance = new TemplateInstance(result.template, partCallback); | ||
instance = | ||
new TemplateInstance(template, result.partCallback, templateFactory); | ||
(container as any).__templateInstance = instance; | ||
@@ -113,7 +191,13 @@ | ||
/** | ||
* An expression marker with embedded unique key to avoid | ||
* https://github.com/PolymerLabs/lit-html/issues/62 | ||
* An expression marker with embedded unique key to avoid collision with | ||
* possible text in templates. | ||
*/ | ||
const marker = `{{lit-${String(Math.random()).slice(2)}}}`; | ||
/** | ||
* An expression marker used text-posisitions, not attribute positions, | ||
* in template. | ||
*/ | ||
const nodeMarker = `<!--${marker}-->`; | ||
const markerRegex = new RegExp(`${marker}|${nodeMarker}`); | ||
@@ -185,3 +269,5 @@ | ||
/** | ||
* An updateable Template that tracks the location of dynamic parts. | ||
*/ | ||
export class Template { | ||
@@ -191,13 +277,5 @@ parts: TemplatePart[] = []; | ||
constructor(strings: TemplateStringsArray, svg: boolean = false) { | ||
const element = this.element = document.createElement('template'); | ||
element.innerHTML = this._getHtml(strings, svg); | ||
const content = element.content; | ||
if (svg) { | ||
const svgElement = content.firstChild!; | ||
content.removeChild(svgElement); | ||
reparentNodes(content, svgElement.firstChild); | ||
} | ||
constructor(result: TemplateResult, element: HTMLTemplateElement) { | ||
this.element = element; | ||
const content = this.element.content; | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null | ||
@@ -243,3 +321,3 @@ const walker = document.createTreeWalker( | ||
// expression in this attribute attribute | ||
const stringForPart = strings[partIndex]; | ||
const stringForPart = result.strings[partIndex]; | ||
// Find the attribute name | ||
@@ -325,23 +403,2 @@ const attributeNameInPart = | ||
} | ||
/** | ||
* Returns a string of HTML used to create a <template> element. | ||
*/ | ||
private _getHtml(strings: TemplateStringsArray, svg?: boolean): string { | ||
const l = strings.length - 1; | ||
let html = ''; | ||
let isTextBinding = true; | ||
for (let i = 0; i < l; i++) { | ||
const s = strings[i]; | ||
html += s; | ||
// We're in a text position if the previous string closed its tags. | ||
// If it doesn't have any tags, then we use the previous text position | ||
// state. | ||
const closing = findTagClose(s); | ||
isTextBinding = closing > -1 ? closing < s.length : isTextBinding; | ||
html += isTextBinding ? nodeMarker : marker; | ||
} | ||
html += strings[l]; | ||
return svg ? `<svg>${html}</svg>` : html; | ||
} | ||
} | ||
@@ -368,6 +425,7 @@ | ||
export const directive = <P extends Part = Part, F = DirectiveFn<P>>(f: F): F => { | ||
(f as any).__litDirective = true; | ||
return f; | ||
}; | ||
export const directive = | ||
<P extends Part = Part, F = DirectiveFn<P>>(f: F): F => { | ||
(f as any).__litDirective = true; | ||
return f; | ||
}; | ||
@@ -377,4 +435,11 @@ const isDirective = (o: any) => | ||
const directiveValue = {}; | ||
/** | ||
* A sentinel value that signals that a value was handled by a directive and | ||
* should not be written to the DOM. | ||
*/ | ||
export const directiveValue = {}; | ||
const isPrimitiveValue = (value: any) => value === null || | ||
!(typeof value === 'object' || typeof value === 'function'); | ||
export interface Part { | ||
@@ -397,2 +462,3 @@ instance: TemplateInstance; | ||
size: number; | ||
_previousValues: any; | ||
@@ -407,2 +473,4 @@ constructor( | ||
this.size = strings.length - 1; | ||
this._previousValues = []; | ||
} | ||
@@ -431,5 +499,32 @@ | ||
protected _equalToPreviousValues(values: any[], startIndex: number) { | ||
for (let i = startIndex; i < startIndex + this.size; i++) { | ||
if (this._previousValues[i] !== values[i] || | ||
!isPrimitiveValue(values[i])) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
setValue(values: any[], startIndex: number): void { | ||
const text = this._interpolate(values, startIndex); | ||
this.element.setAttribute(this.name, text); | ||
if (this._equalToPreviousValues(values, startIndex)) { | ||
return; | ||
} | ||
const s = this.strings; | ||
let value: any; | ||
if (s.length === 2 && s[0] === '' && s[1] === '') { | ||
// An expression that occupies the whole attribute value will leave | ||
// leading and trailing empty strings. | ||
value = getValue(this, values[startIndex]); | ||
if (Array.isArray(value)) { | ||
value = value.join(''); | ||
} | ||
} else { | ||
value = this._interpolate(values, startIndex); | ||
} | ||
if (value !== directiveValue) { | ||
this.element.setAttribute(this.name, value); | ||
} | ||
this._previousValues = values; | ||
} | ||
@@ -456,4 +551,3 @@ } | ||
} | ||
if (value === null || | ||
!(typeof value === 'object' || typeof value === 'function')) { | ||
if (isPrimitiveValue(value)) { | ||
// Handle primitive values | ||
@@ -509,9 +603,9 @@ // If the value didn't change, do nothing | ||
private _setTemplateResult(value: TemplateResult): void { | ||
const template = this.instance._getTemplate(value); | ||
let instance: TemplateInstance; | ||
if (this._previousValue && | ||
this._previousValue.template === value.template) { | ||
if (this._previousValue && this._previousValue.template === template) { | ||
instance = this._previousValue; | ||
} else { | ||
instance = | ||
new TemplateInstance(value.template, this.instance._partCallback); | ||
instance = new TemplateInstance( | ||
template, this.instance._partCallback, this.instance._getTemplate); | ||
this._setNode(instance._clone()); | ||
@@ -620,8 +714,11 @@ this._previousValue = instance; | ||
_partCallback: PartCallback; | ||
_getTemplate: TemplateFactory; | ||
template: Template; | ||
constructor( | ||
template: Template, partCallback: PartCallback = defaultPartCallback) { | ||
template: Template, partCallback: PartCallback, | ||
getTemplate: TemplateFactory) { | ||
this.template = template; | ||
this._partCallback = partCallback; | ||
this._getTemplate = getTemplate; | ||
} | ||
@@ -628,0 +725,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
204996
39
2721
14
417