Comparing version 0.10.2 to 0.11.0-dev.1
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { DirectiveFn, NodePart } from '../lit-html.js'; | ||
import { DirectiveFn, NodePart } from '../core.js'; | ||
/** | ||
@@ -17,0 +17,0 @@ * A directive that renders the items of an async iterable[1], appending new |
@@ -21,3 +21,3 @@ /** | ||
}; | ||
import { directive, NodePart } from '../lit-html.js'; | ||
import { directive, NodePart } from '../core.js'; | ||
/** | ||
@@ -44,6 +44,6 @@ * A directive that renders the items of an async iterable[1], appending new | ||
// to do anything. | ||
if (value === part._previousValue) { | ||
if (value === part._value) { | ||
return; | ||
} | ||
part._previousValue = value; | ||
part._value = value; | ||
// We keep track of item Parts across iterations, so that we can | ||
@@ -63,3 +63,3 @@ // share marker nodes between consecutive Parts. | ||
// the part, and if not bail because a new value owns this part | ||
if (part._previousValue !== value) { | ||
if (part._value !== value) { | ||
break; | ||
@@ -91,3 +91,4 @@ } | ||
} | ||
itemPart = new NodePart(part.instance, itemStartNode, part.endNode); | ||
itemPart = | ||
new NodePart(itemStartNode, part.endNode, part.templateFactory); | ||
itemPart.setValue(v); | ||
@@ -94,0 +95,0 @@ i++; |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { DirectiveFn, NodePart } from '../lit-html.js'; | ||
import { DirectiveFn, NodePart } from '../core.js'; | ||
/** | ||
@@ -17,0 +17,0 @@ * A directive that renders the items of an async iterable[1], replacing |
@@ -21,3 +21,3 @@ /** | ||
}; | ||
import { directive, NodePart } from '../lit-html.js'; | ||
import { directive, NodePart } from '../core.js'; | ||
/** | ||
@@ -45,3 +45,3 @@ * A directive that renders the items of an async iterable[1], replacing | ||
// to do anything. | ||
if (value === part._previousValue) { | ||
if (value === part._value) { | ||
return; | ||
@@ -51,4 +51,4 @@ } | ||
// of the iterable as a value itself. | ||
const itemPart = new NodePart(part.instance, part.startNode, part.endNode); | ||
part._previousValue = itemPart; | ||
const itemPart = new NodePart(part.startNode, part.endNode, part.templateFactory); | ||
part._value = itemPart; | ||
let i = 0; | ||
@@ -65,3 +65,3 @@ try { | ||
// the part, and if not bail because a new value owns this part | ||
if (part._previousValue !== itemPart) { | ||
if (part._value !== itemPart) { | ||
break; | ||
@@ -68,0 +68,0 @@ } |
@@ -14,4 +14,5 @@ /** | ||
*/ | ||
import { AttributePart, Part, SVGTemplateResult, TemplateInstance, TemplatePart, TemplateResult } from '../lit-html.js'; | ||
export { render } from '../lit-html.js'; | ||
import { Part, SVGTemplateResult, TemplateProcessor, TemplateResult } from '../core.js'; | ||
export { render } from '../core.js'; | ||
export { BooleanAttributePart, EventPart } from '../lit-html.js'; | ||
/** | ||
@@ -49,25 +50,8 @@ * Interprets a template literal as a lit-extended HTML template. | ||
* | ||
* @deprecated Please use /lit-html.js instead. lit-extended will be removed in | ||
* a future version. | ||
*/ | ||
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 LitExtendedTemplateProcessor extends TemplateProcessor { | ||
handleAttributeExpressions(element: Element, name: string, strings: string[]): Part[]; | ||
} | ||
export declare class PropertyPart extends AttributePart { | ||
setValue(values: any[], startIndex: number): void; | ||
} | ||
export declare class EventPart implements Part { | ||
instance: TemplateInstance; | ||
element: Element; | ||
eventName: string; | ||
private _listener; | ||
constructor(instance: TemplateInstance, element: Element, eventName: string); | ||
setValue(value: any): void; | ||
handleEvent(event: Event): void; | ||
} | ||
export declare const templateProcessor: LitExtendedTemplateProcessor; |
@@ -14,12 +14,14 @@ /** | ||
*/ | ||
import { AttributePart, defaultPartCallback, noChange, getValue, SVGTemplateResult, TemplateResult } from '../lit-html.js'; | ||
export { render } from '../lit-html.js'; | ||
import { AttributeCommitter, SVGTemplateResult, TemplateProcessor, TemplateResult } from '../core.js'; | ||
import { BooleanAttributePart, EventPart, PropertyCommitter } from '../lit-html.js'; | ||
export { render } from '../core.js'; | ||
export { BooleanAttributePart, EventPart } 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); | ||
export const html = (strings, ...values) => new TemplateResult(strings, values, 'html', templateProcessor); | ||
/** | ||
* Interprets a template literal as a lit-extended SVG template. | ||
*/ | ||
export const svg = (strings, ...values) => new SVGTemplateResult(strings, values, 'svg', extendedPartCallback); | ||
export const svg = (strings, ...values) => new SVGTemplateResult(strings, values, 'svg', templateProcessor); | ||
/** | ||
@@ -49,99 +51,24 @@ * A PartCallback which allows templates to set properties and declarative | ||
* | ||
* @deprecated Please use /lit-html.js instead. lit-extended will be removed in | ||
* a future version. | ||
*/ | ||
export const extendedPartCallback = (instance, templatePart, node) => { | ||
if (templatePart.type === 'attribute') { | ||
if (templatePart.rawName.substr(0, 3) === 'on-') { | ||
const eventName = templatePart.rawName.slice(3); | ||
return new EventPart(instance, node, eventName); | ||
export class LitExtendedTemplateProcessor extends TemplateProcessor { | ||
handleAttributeExpressions(element, name, strings) { | ||
if (name.substr(0, 3) === 'on-') { | ||
const eventName = name.slice(3); | ||
return [new EventPart(element, eventName)]; | ||
} | ||
const lastChar = templatePart.name.substr(templatePart.name.length - 1); | ||
const lastChar = name.substr(name.length - 1); | ||
if (lastChar === '$') { | ||
const name = templatePart.name.slice(0, -1); | ||
return new AttributePart(instance, node, name, templatePart.strings); | ||
const comitter = new AttributeCommitter(element, name.slice(0, -1), strings); | ||
return comitter.parts; | ||
} | ||
if (lastChar === '?') { | ||
const name = templatePart.name.slice(0, -1); | ||
return new BooleanAttributePart(instance, node, name, templatePart.strings); | ||
return [new BooleanAttributePart(element, name.slice(0, -1), strings)]; | ||
} | ||
return new PropertyPart(instance, node, templatePart.rawName, templatePart.strings); | ||
const comitter = new PropertyCommitter(element, name, strings); | ||
return comitter.parts; | ||
} | ||
return defaultPartCallback(instance, templatePart, node); | ||
}; | ||
/** | ||
* 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 === noChange) { | ||
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 { | ||
setValue(values, startIndex) { | ||
const s = this.strings; | ||
let value; | ||
if (this._equalToPreviousValues(values, startIndex)) { | ||
return; | ||
} | ||
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]); | ||
} | ||
else { | ||
// Interpolation, so interpolate | ||
value = this._interpolate(values, startIndex); | ||
} | ||
if (value !== noChange) { | ||
this.element[this.name] = value; | ||
} | ||
this._previousValues = values; | ||
} | ||
} | ||
export class EventPart { | ||
constructor(instance, element, eventName) { | ||
this.instance = instance; | ||
this.element = element; | ||
this.eventName = eventName; | ||
} | ||
setValue(value) { | ||
const listener = getValue(this, value); | ||
if (listener === this._listener) { | ||
return; | ||
} | ||
if (listener == null) { | ||
this.element.removeEventListener(this.eventName, this); | ||
} | ||
else if (this._listener == null) { | ||
this.element.addEventListener(this.eventName, this); | ||
} | ||
this._listener = listener; | ||
} | ||
handleEvent(event) { | ||
if (typeof this._listener === 'function') { | ||
this._listener.call(this.element, event); | ||
} | ||
else if (typeof this._listener.handleEvent === 'function') { | ||
this._listener.handleEvent(event); | ||
} | ||
} | ||
} | ||
export const templateProcessor = new LitExtendedTemplateProcessor(); | ||
//# sourceMappingURL=lit-extended.js.map |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { Template } from '../lit-html.js'; | ||
import { Template } from '../core.js'; | ||
/** | ||
@@ -29,3 +29,4 @@ * Removes the list of nodes from a Template safely. In addition to removing | ||
* div | ||
* div <-- stop removing since previous sibling is the removing node (div#1, removed 4 nodes) | ||
* div <-- stop removing since previous sibling is the removing node (div#1, | ||
* removed 4 nodes) | ||
*/ | ||
@@ -32,0 +33,0 @@ export declare function removeNodesFromTemplate(template: Template, nodesToRemove: Set<Node>): void; |
@@ -14,5 +14,4 @@ /** | ||
*/ | ||
import { isTemplatePartActive } from '../lit-html.js'; | ||
const walkerNodeFilter = NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | | ||
NodeFilter.SHOW_TEXT; | ||
import { isTemplatePartActive } from '../core.js'; | ||
const walkerNodeFilter = NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT; | ||
/** | ||
@@ -31,3 +30,4 @@ * Removes the list of nodes from a Template safely. In addition to removing | ||
* div | ||
* div <-- stop removing since previous sibling is the removing node (div#1, removed 4 nodes) | ||
* div <-- stop removing since previous sibling is the removing node (div#1, | ||
* removed 4 nodes) | ||
*/ | ||
@@ -34,0 +34,0 @@ export function removeNodesFromTemplate(template, nodesToRemove) { |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { DirectiveFn, NodePart } from '../lit-html.js'; | ||
import { DirectiveFn, NodePart } from '../core.js'; | ||
export declare type KeyFn<T> = (item: T) => any; | ||
@@ -17,0 +17,0 @@ export declare type ItemTemplate<T> = (item: T, index: number) => any; |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { directive, NodePart, removeNodes, reparentNodes } from '../lit-html.js'; | ||
import { directive, NodePart, removeNodes, reparentNodes } from '../core.js'; | ||
const keyMapCache = new WeakMap(); | ||
@@ -58,3 +58,3 @@ function cleanMap(part, key, map) { | ||
container.insertBefore(endNode, currentMarker); | ||
itemPart = new NodePart(part.instance, marker, endNode); | ||
itemPart = new NodePart(marker, endNode, part.templateFactory); | ||
if (key !== undefined) { | ||
@@ -61,0 +61,0 @@ keyMap.set(key, itemPart); |
@@ -14,4 +14,4 @@ /** | ||
*/ | ||
import { TemplateResult } from '../lit-html.js'; | ||
export { html, svg, TemplateResult } from '../lit-html.js'; | ||
import { TemplateResult } from '../core.js'; | ||
export { html, svg, TemplateResult } from '../core.js'; | ||
declare global { | ||
@@ -18,0 +18,0 @@ interface Window { |
@@ -14,7 +14,19 @@ /** | ||
*/ | ||
import { removeNodes, Template, templateCaches, TemplateInstance } from '../lit-html.js'; | ||
import { removeNodes, Template, templateCaches, TemplateInstance } from '../core.js'; | ||
import { insertNodeIntoTemplate, removeNodesFromTemplate } from './modify-template.js'; | ||
export { html, svg, TemplateResult } from '../lit-html.js'; | ||
export { html, svg, TemplateResult } from '../core.js'; | ||
// Get a key to lookup in `templateCaches`. | ||
const getTemplateCacheKey = (type, scopeName) => `${type}--${scopeName}`; | ||
const verifyShadyCSSVersion = () => { | ||
if (typeof window.ShadyCSS === 'undefined') { | ||
return false; | ||
} | ||
if (typeof window.ShadyCSS.prepareTemplateDom === 'undefined') { | ||
console.warn(`Incompatible ShadyCSS version detected.` + | ||
`Please update to at least @webcomponents/webcomponentsjs@2.0.2 and` + | ||
`@webcomponents/shadycss@1.3.1.`); | ||
return false; | ||
} | ||
return true; | ||
}; | ||
/** | ||
@@ -34,3 +46,3 @@ * Template factory which scopes template DOM using ShadyCSS. | ||
const element = result.getTemplateElement(); | ||
if (typeof window.ShadyCSS === 'object') { | ||
if (verifyShadyCSSVersion()) { | ||
window.ShadyCSS.prepareTemplateDom(element, scopeName); | ||
@@ -53,4 +65,8 @@ } | ||
const { element: { content } } = template; | ||
const styles = content.querySelectorAll('style'); | ||
removeNodesFromTemplate(template, new Set(Array.from(styles))); | ||
// IE 11 doesn't support the iterable param Set constructor | ||
const styles = new Set(); | ||
Array.from(content.querySelectorAll('style')).forEach((s) => { | ||
styles.add(s); | ||
}); | ||
removeNodesFromTemplate(template, styles); | ||
}); | ||
@@ -72,3 +88,4 @@ } | ||
* dynamically rendered to the scope (after the first scope render), they will | ||
* not be scoped and the <style> will be left in the template and rendered output. | ||
* not be scoped and the <style> will be left in the template and rendered | ||
* output. | ||
*/ | ||
@@ -111,3 +128,3 @@ const ensureStylesScoped = (fragment, template, scopeName) => { | ||
if (instance !== undefined && instance.template === template && | ||
instance._partCallback === result.partCallback) { | ||
instance.processor === result.processor) { | ||
instance.update(result.values); | ||
@@ -117,12 +134,9 @@ return; | ||
// First render, create a new TemplateInstance and append it | ||
instance = | ||
new TemplateInstance(template, result.partCallback, templateFactory); | ||
instance = new TemplateInstance(template, result.processor, templateFactory); | ||
container.__templateInstance = instance; | ||
const fragment = instance._clone(); | ||
instance.update(result.values); | ||
const host = container instanceof ShadowRoot ? | ||
container.host : | ||
undefined; | ||
const host = container instanceof ShadowRoot ? container.host : undefined; | ||
// If there's a shadow host, do ShadyCSS scoping... | ||
if (host !== undefined && typeof window.ShadyCSS === 'object') { | ||
if (host !== undefined && verifyShadyCSSVersion()) { | ||
ensureStylesScoped(fragment, template, scopeName); | ||
@@ -129,0 +143,0 @@ window.ShadyCSS.styleElement(host); |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { DirectiveFn, NodePart } from '../lit-html.js'; | ||
import { DirectiveFn, NodePart } from '../core.js'; | ||
/** | ||
@@ -17,0 +17,0 @@ * Renders the result as HTML, rather than text. |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { directive } from '../lit-html.js'; | ||
import { _isPrimitiveValue, directive } from '../core.js'; | ||
/** | ||
@@ -24,6 +24,14 @@ * Renders the result as HTML, rather than text. | ||
export const unsafeHTML = (value) => directive((part) => { | ||
// Dirty check primitive values | ||
if (part._value === value && _isPrimitiveValue(value)) { | ||
return; | ||
} | ||
// Use a <template> to parse HTML into Nodes | ||
const tmp = document.createElement('template'); | ||
tmp.innerHTML = value; | ||
part.setValue(document.importNode(tmp.content, true)); | ||
// Remember the actual value passed to unsafeHTML rather than the DOM | ||
// Nodes we created | ||
part._value = value; | ||
}); | ||
//# sourceMappingURL=unsafe-html.js.map |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { DirectiveFn, NodePart } from '../lit-html.js'; | ||
import { DirectiveFn, NodePart } from '../core.js'; | ||
/** | ||
@@ -17,0 +17,0 @@ * Display `defaultContent` until `promise` resolves. |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { directive } from '../lit-html.js'; | ||
import { directive } from '../core.js'; | ||
/** | ||
@@ -17,0 +17,0 @@ * Display `defaultContent` until `promise` resolves. |
@@ -14,202 +14,76 @@ /** | ||
*/ | ||
export declare const templateCaches: Map<string, Map<TemplateStringsArray, Template>>; | ||
import { AttributeCommitter, Part, SVGTemplateResult, TemplateProcessor, TemplateResult } from './core.js'; | ||
export { render } from './core.js'; | ||
/** | ||
* Interprets a template literal as an HTML template that can efficiently | ||
* render to and update a container. | ||
* Interprets a template literal as a lit-html HTML template. | ||
*/ | ||
export declare const html: (strings: TemplateStringsArray, ...values: any[]) => TemplateResult; | ||
/** | ||
* Interprets a template literal as an SVG template that can efficiently | ||
* render to and update a container. | ||
* Interprets a template literal as a lit-html SVG template. | ||
*/ | ||
export declare const svg: (strings: TemplateStringsArray, ...values: any[]) => SVGTemplateResult; | ||
/** | ||
* The return type of `html`, which holds a Template and the values from | ||
* interpolated expressions. | ||
*/ | ||
export declare class TemplateResult { | ||
strings: TemplateStringsArray; | ||
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. | ||
* A PartCallback which allows templates to set properties, declarative | ||
* event handlers, and boolean attributes. | ||
* | ||
* 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. | ||
* Properties are set by prefixing an attribute name with `.`. | ||
* | ||
* This is a hook into the template-creation process for rendering that | ||
* requires some modification of templates before they're used, like ShadyCSS, | ||
* which must add classes to elements and remove styles. | ||
* Attribute names in lit-html templates preserve case, so properties are case | ||
* sensitive. If an expression takes up an entire attribute value, then the | ||
* property is set to that value. If an expression is interpolated with a string | ||
* or other expressions then the property is set to the string result of the | ||
* interpolation. | ||
* | ||
* 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. | ||
* Example: | ||
* | ||
* Templates are usually cached by TemplateResult.strings and | ||
* TemplateResult.type, but may be cached by other keys if this function | ||
* modifies the template. | ||
* html`<input .value=${value}>` | ||
* | ||
* 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; | ||
export declare type TemplateContainer = (Element | DocumentFragment) & { | ||
__templateInstance?: TemplateInstance; | ||
}; | ||
/** | ||
* Renders a template to a container. | ||
* Event handlers are set by prefixing an attribute name with `@`. | ||
* | ||
* To update a container with new values, reevaluate the template literal and | ||
* call `render` with the new result. | ||
* Example: | ||
* | ||
* @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, templateFactory?: TemplateFactory): void; | ||
/** | ||
* A placeholder for a dynamic expression in an HTML template. | ||
* html`<button @click=${(e)=> this.onClickHandler(e)}>Buy Now</button>` | ||
* | ||
* 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. | ||
* Boolean attributes are set by prepending `?` to an attribute name. | ||
* | ||
* 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. | ||
* Example: | ||
* | ||
* TODO(justinfagnani): That requirement is a little fragile. A | ||
* TemplateInstance could instead be more careful about which values it gives | ||
* to Part.update(). | ||
* html`<input type="checkbox" ?checked=${value}>` | ||
*/ | ||
export declare class TemplatePart { | ||
type: string; | ||
index: number; | ||
name?: string | undefined; | ||
rawName?: string | undefined; | ||
strings?: string[] | undefined; | ||
constructor(type: string, index: number, name?: string | undefined, rawName?: string | undefined, strings?: string[] | undefined); | ||
export declare class LitTemplateProcessor extends TemplateProcessor { | ||
handleAttributeExpressions(element: Element, name: string, strings: string[]): Part[]; | ||
} | ||
export declare const isTemplatePartActive: (part: TemplatePart) => boolean; | ||
export declare const templateProcessor: LitTemplateProcessor; | ||
/** | ||
* An updateable Template that tracks the location of dynamic parts. | ||
*/ | ||
export declare class Template { | ||
parts: TemplatePart[]; | ||
element: HTMLTemplateElement; | ||
constructor(result: TemplateResult, element: HTMLTemplateElement); | ||
} | ||
/** | ||
* Returns a value ready to be inserted into a Part from a user-provided value. | ||
* Implements a boolean attribute, roughly as defined in the HTML | ||
* specification. | ||
* | ||
* If the user value is a directive, this invokes the directive with the given | ||
* part. If the value is null, it's converted to undefined to work better | ||
* with certain DOM APIs, like textContent. | ||
* 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 const getValue: (part: Part, value: any) => any; | ||
export interface DirectiveFn<P = Part> { | ||
(part: P): void; | ||
__litDirective?: true; | ||
} | ||
export declare const directive: <P = Part>(f: DirectiveFn<P>) => DirectiveFn<P>; | ||
/** | ||
* A sentinel value that signals that a value was handled by a directive and | ||
* should not be written to the DOM. | ||
*/ | ||
export declare const noChange: {}; | ||
/** | ||
* @deprecated Use `noChange` instead. | ||
*/ | ||
export { noChange as directiveValue }; | ||
export interface Part { | ||
instance: TemplateInstance; | ||
size?: number; | ||
} | ||
export interface SinglePart extends Part { | ||
export declare class BooleanAttributePart implements Part { | ||
element: Element; | ||
name: string; | ||
strings: string[]; | ||
_value: any; | ||
private _dirty; | ||
constructor(element: Element, name: string, strings: string[]); | ||
setValue(value: any): void; | ||
commit(): void; | ||
} | ||
export interface MultiPart extends Part { | ||
setValue(values: any[], startIndex: number): void; | ||
export declare class PropertyCommitter extends AttributeCommitter { | ||
single: boolean; | ||
constructor(element: Element, name: string, strings: string[]); | ||
_getValue(): any; | ||
commit(): void; | ||
} | ||
export declare class AttributePart implements MultiPart { | ||
instance: TemplateInstance; | ||
export declare class EventPart implements Part { | ||
element: Element; | ||
name: string; | ||
strings: string[]; | ||
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; | ||
} | ||
export declare class NodePart implements SinglePart { | ||
instance: TemplateInstance; | ||
startNode: Node; | ||
endNode: Node; | ||
_previousValue: any; | ||
constructor(instance: TemplateInstance, startNode: Node, endNode: Node); | ||
eventName: string; | ||
_value: any; | ||
private _dirty; | ||
constructor(element: Element, eventName: string); | ||
setValue(value: any): void; | ||
private _insert; | ||
private _setNode; | ||
private _setText; | ||
private _setTemplateResult; | ||
private _setIterable; | ||
private _setPromise; | ||
clear(startNode?: Node): void; | ||
commit(): void; | ||
handleEvent(event: Event): 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 { | ||
_parts: Array<Part | undefined>; | ||
_partCallback: PartCallback; | ||
_getTemplate: TemplateFactory; | ||
template: Template; | ||
constructor(template: Template, partCallback: PartCallback, getTemplate: TemplateFactory); | ||
update(values: any[]): void; | ||
_clone(): DocumentFragment; | ||
} | ||
/** | ||
* Reparents nodes, starting from `startNode` (inclusive) to `endNode` | ||
* (exclusive), into another container (could be the same container), before | ||
* `beforeNode`. If `beforeNode` is null, it appends the nodes to the | ||
* container. | ||
*/ | ||
export declare const reparentNodes: (container: Node, start: Node | null, end?: Node | null, before?: Node | null) => void; | ||
/** | ||
* Removes nodes, starting from `startNode` (inclusive) to `endNode` | ||
* (exclusive), from `container`. | ||
*/ | ||
export declare const removeNodes: (container: Node, startNode: Node | null, endNode?: Node | null) => void; |
682
lit-html.js
@@ -14,415 +14,75 @@ /** | ||
*/ | ||
// 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. | ||
export const templateCaches = new Map(); | ||
import { AttributeCommitter, getValue, noChange, SVGTemplateResult, TemplateProcessor, TemplateResult } from './core.js'; | ||
export { render } from './core.js'; | ||
/** | ||
* Interprets a template literal as an HTML template that can efficiently | ||
* render to and update a container. | ||
* Interprets a template literal as a lit-html HTML template. | ||
*/ | ||
export const html = (strings, ...values) => new TemplateResult(strings, values, 'html'); | ||
export const html = (strings, ...values) => new TemplateResult(strings, values, 'html', templateProcessor); | ||
/** | ||
* Interprets a template literal as an SVG template that can efficiently | ||
* render to and update a container. | ||
* Interprets a template literal as a lit-html SVG template. | ||
*/ | ||
export const svg = (strings, ...values) => new SVGTemplateResult(strings, values, 'svg'); | ||
export const svg = (strings, ...values) => new SVGTemplateResult(strings, values, 'svg', templateProcessor); | ||
/** | ||
* The return type of `html`, which holds a Template and the values from | ||
* interpolated expressions. | ||
*/ | ||
export class TemplateResult { | ||
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. | ||
* A PartCallback which allows templates to set properties, declarative | ||
* event handlers, and boolean attributes. | ||
* | ||
* 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. | ||
* Properties are set by prefixing an attribute name with `.`. | ||
* | ||
* To update a container with new values, reevaluate the template literal and | ||
* call `render` with the new result. | ||
* Attribute names in lit-html templates preserve case, so properties are case | ||
* sensitive. If an expression takes up an entire attribute value, then the | ||
* property is set to that value. If an expression is interpolated with a string | ||
* or other expressions then the property is set to the string result of the | ||
* interpolation. | ||
* | ||
* @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, templateFactory = defaultTemplateFactory) { | ||
const template = templateFactory(result); | ||
let instance = container.__templateInstance; | ||
// Repeat render, just call update() | ||
if (instance !== undefined && instance.template === template && | ||
instance._partCallback === result.partCallback) { | ||
instance.update(result.values); | ||
return; | ||
} | ||
// First render, create a new TemplateInstance and append it | ||
instance = | ||
new TemplateInstance(template, result.partCallback, templateFactory); | ||
container.__templateInstance = instance; | ||
const fragment = instance._clone(); | ||
instance.update(result.values); | ||
removeNodes(container, container.firstChild); | ||
container.appendChild(fragment); | ||
} | ||
/** | ||
* 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-positions, not attribute positions, | ||
* in template. | ||
*/ | ||
const nodeMarker = `<!--${marker}-->`; | ||
const markerRegex = new RegExp(`${marker}|${nodeMarker}`); | ||
/** | ||
* This regex extracts the attribute name preceding an attribute-position | ||
* expression. It does this by matching the syntax allowed for attributes | ||
* against the string literal directly preceding the expression, assuming that | ||
* the expression is in an attribute-value position. | ||
* Example: | ||
* | ||
* See attributes in the HTML spec: | ||
* https://www.w3.org/TR/html5/syntax.html#attributes-0 | ||
* html`<input .value=${value}>` | ||
* | ||
* "\0-\x1F\x7F-\x9F" are Unicode control characters | ||
* Event handlers are set by prefixing an attribute name with `@`. | ||
* | ||
* " \x09\x0a\x0c\x0d" are HTML space characters: | ||
* https://www.w3.org/TR/html5/infrastructure.html#space-character | ||
* Example: | ||
* | ||
* So an attribute is: | ||
* * The name: any character except a control character, space character, ('), | ||
* ("), ">", "=", or "/" | ||
* * Followed by zero or more space characters | ||
* * Followed by "=" | ||
* * Followed by zero or more space characters | ||
* * Followed by: | ||
* * Any character except space, ('), ("), "<", ">", "=", (`), or | ||
* * (") then any non-("), or | ||
* * (') then any non-(') | ||
*/ | ||
const lastAttributeNameRegex = /[ \x09\x0a\x0c\x0d]([^\0-\x1F\x7F-\x9F \x09\x0a\x0c\x0d"'>=/]+)[ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*)$/; | ||
/** | ||
* Finds the closing index of the last closed HTML tag. | ||
* This has 3 possible return values: | ||
* - `-1`, meaning there is no tag in str. | ||
* - `string.length`, meaning the last opened tag is unclosed. | ||
* - Some positive number < str.length, meaning the index of the closing '>'. | ||
*/ | ||
function findTagClose(str) { | ||
const close = str.lastIndexOf('>'); | ||
const open = str.indexOf('<', close + 1); | ||
return open > -1 ? str.length : close; | ||
} | ||
/** | ||
* A placeholder for a dynamic expression in an HTML template. | ||
* html`<button @click=${(e)=> this.onClickHandler(e)}>Buy Now</button>` | ||
* | ||
* 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. | ||
* Boolean attributes are set by prepending `?` to an attribute name. | ||
* | ||
* 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. | ||
* Example: | ||
* | ||
* TODO(justinfagnani): That requirement is a little fragile. A | ||
* TemplateInstance could instead be more careful about which values it gives | ||
* to Part.update(). | ||
* html`<input type="checkbox" ?checked=${value}>` | ||
*/ | ||
export class TemplatePart { | ||
constructor(type, index, name, rawName, strings) { | ||
this.type = type; | ||
this.index = index; | ||
this.name = name; | ||
this.rawName = rawName; | ||
this.strings = strings; | ||
} | ||
} | ||
export const isTemplatePartActive = (part) => part.index !== -1; | ||
/** | ||
* An updateable Template that tracks the location of dynamic parts. | ||
*/ | ||
export class Template { | ||
constructor(result, element) { | ||
this.parts = []; | ||
this.element = element; | ||
const content = this.element.content; | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null | ||
const walker = document.createTreeWalker(content, 133 /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | | ||
NodeFilter.SHOW_TEXT */, null, false); | ||
let index = -1; | ||
let partIndex = 0; | ||
const nodesToRemove = []; | ||
// The actual previous node, accounting for removals: if a node is removed | ||
// it will never be the previousNode. | ||
let previousNode; | ||
// Used to set previousNode at the top of the loop. | ||
let currentNode; | ||
while (walker.nextNode()) { | ||
index++; | ||
previousNode = currentNode; | ||
const node = currentNode = walker.currentNode; | ||
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { | ||
if (!node.hasAttributes()) { | ||
continue; | ||
} | ||
const attributes = node.attributes; | ||
// Per https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap, | ||
// attributes are not guaranteed to be returned in document order. In | ||
// particular, Edge/IE can return them out of order, so we cannot assume | ||
// a correspondance between part index and attribute index. | ||
let count = 0; | ||
for (let i = 0; i < attributes.length; i++) { | ||
if (attributes[i].value.indexOf(marker) >= 0) { | ||
count++; | ||
} | ||
} | ||
while (count-- > 0) { | ||
// Get the template literal section leading up to the first | ||
// expression in this attribute | ||
const stringForPart = result.strings[partIndex]; | ||
// Find the attribute name | ||
const attributeNameInPart = lastAttributeNameRegex.exec(stringForPart)[1]; | ||
// Find the corresponding attribute | ||
// TODO(justinfagnani): remove non-null assertion | ||
const attribute = attributes.getNamedItem(attributeNameInPart); | ||
const stringsForAttributeValue = attribute.value.split(markerRegex); | ||
this.parts.push(new TemplatePart('attribute', index, attribute.name, attributeNameInPart, stringsForAttributeValue)); | ||
node.removeAttribute(attribute.name); | ||
partIndex += stringsForAttributeValue.length - 1; | ||
} | ||
} | ||
else if (node.nodeType === 3 /* Node.TEXT_NODE */) { | ||
const nodeValue = node.nodeValue; | ||
if (nodeValue.indexOf(marker) < 0) { | ||
continue; | ||
} | ||
const parent = node.parentNode; | ||
const strings = nodeValue.split(markerRegex); | ||
const lastIndex = strings.length - 1; | ||
// We have a part for each match found | ||
partIndex += 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((strings[i] === '') | ||
? document.createComment('') | ||
: document.createTextNode(strings[i]), node); | ||
this.parts.push(new TemplatePart('node', index++)); | ||
} | ||
parent.insertBefore(strings[lastIndex] === '' ? | ||
document.createComment('') : | ||
document.createTextNode(strings[lastIndex]), node); | ||
nodesToRemove.push(node); | ||
} | ||
else if (node.nodeType === 8 /* Node.COMMENT_NODE */ && | ||
node.nodeValue === marker) { | ||
const parent = node.parentNode; | ||
// Add a new marker node to be the startNode of the Part if any of the | ||
// following are true: | ||
// * We don't have a previousSibling | ||
// * previousSibling is being removed (thus it's not the | ||
// `previousNode`) | ||
// * previousSibling is not a Text node | ||
// | ||
// TODO(justinfagnani): We should be able to use the previousNode here | ||
// as the marker node and reduce the number of extra nodes we add to a | ||
// template. See https://github.com/PolymerLabs/lit-html/issues/147 | ||
const previousSibling = node.previousSibling; | ||
if (previousSibling === null || previousSibling !== previousNode || | ||
previousSibling.nodeType !== Node.TEXT_NODE) { | ||
parent.insertBefore(document.createComment(''), node); | ||
} | ||
else { | ||
index--; | ||
} | ||
this.parts.push(new TemplatePart('node', index++)); | ||
nodesToRemove.push(node); | ||
// If we don't have a nextSibling add a marker node. | ||
// We don't have to check if the next node is going to be removed, | ||
// because that node will induce a new marker if so. | ||
if (node.nextSibling === null) { | ||
parent.insertBefore(document.createComment(''), node); | ||
} | ||
else { | ||
index--; | ||
} | ||
currentNode = previousNode; | ||
partIndex++; | ||
} | ||
export class LitTemplateProcessor extends TemplateProcessor { | ||
handleAttributeExpressions(element, name, strings) { | ||
const prefix = name[0]; | ||
if (prefix === '.') { | ||
const comitter = new PropertyCommitter(element, name.slice(1), strings); | ||
return comitter.parts; | ||
} | ||
// Remove text binding nodes after the walk to not disturb the TreeWalker | ||
for (const n of nodesToRemove) { | ||
n.parentNode.removeChild(n); | ||
if (prefix === '@') { | ||
return [new EventPart(element, name.slice(1))]; | ||
} | ||
if (prefix === '?') { | ||
return [new BooleanAttributePart(element, name.slice(1), strings)]; | ||
} | ||
return super.handleAttributeExpressions(element, name, strings); | ||
} | ||
} | ||
export const templateProcessor = new LitTemplateProcessor(); | ||
/** | ||
* Returns a value ready to be inserted into a Part from a user-provided value. | ||
* Implements a boolean attribute, roughly as defined in the HTML | ||
* specification. | ||
* | ||
* If the user value is a directive, this invokes the directive with the given | ||
* part. If the value is null, it's converted to undefined to work better | ||
* with certain DOM APIs, like textContent. | ||
* If the value is truthy, then the attribute is present with a value of | ||
* ''. If the value is falsey, the attribute is removed. | ||
*/ | ||
export const getValue = (part, value) => { | ||
// `null` as the value of a Text node will render the string 'null' | ||
// so we convert it to undefined | ||
if (isDirective(value)) { | ||
value = value(part); | ||
return noChange; | ||
} | ||
return value === null ? undefined : value; | ||
}; | ||
export const directive = (f) => { | ||
f.__litDirective = true; | ||
return f; | ||
}; | ||
const isDirective = (o) => typeof o === 'function' && o.__litDirective === true; | ||
/** | ||
* A sentinel value that signals that a value was handled by a directive and | ||
* should not be written to the DOM. | ||
*/ | ||
export const noChange = {}; | ||
/** | ||
* @deprecated Use `noChange` instead. | ||
*/ | ||
export { noChange as directiveValue }; | ||
const isPrimitiveValue = (value) => value === null || | ||
!(typeof value === 'object' || typeof value === 'function'); | ||
export class AttributePart { | ||
constructor(instance, element, name, strings) { | ||
this.instance = instance; | ||
export class BooleanAttributePart { | ||
constructor(element, name, strings) { | ||
this._value = undefined; | ||
this._dirty = true; | ||
if (strings.length !== 2 || strings[0] !== '' || strings[1] !== '') { | ||
throw new Error('boolean attributes can only contain a single expression'); | ||
} | ||
this.element = element; | ||
this.name = name; | ||
this.strings = strings; | ||
this.size = strings.length - 1; | ||
this._previousValues = []; | ||
} | ||
_interpolate(values, startIndex) { | ||
const strings = this.strings; | ||
const l = strings.length - 1; | ||
let text = ''; | ||
for (let i = 0; i < l; i++) { | ||
text += strings[i]; | ||
const v = getValue(this, values[startIndex + i]); | ||
if (v && v !== noChange && | ||
(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; | ||
} | ||
} | ||
return text + strings[l]; | ||
} | ||
_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) { | ||
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 !== noChange) { | ||
this.element.setAttribute(this.name, value); | ||
} | ||
this._previousValues = values; | ||
} | ||
} | ||
export class NodePart { | ||
constructor(instance, startNode, endNode) { | ||
this.instance = instance; | ||
this.startNode = startNode; | ||
this.endNode = endNode; | ||
this._previousValue = undefined; | ||
} | ||
setValue(value) { | ||
@@ -433,220 +93,74 @@ value = getValue(this, value); | ||
} | ||
if (isPrimitiveValue(value)) { | ||
// Handle primitive values | ||
// If the value didn't change, do nothing | ||
if (value === this._previousValue) { | ||
return; | ||
this._value = !!value; | ||
if (this._value !== !!value) { | ||
this._dirty = true; | ||
} | ||
} | ||
commit() { | ||
if (this._dirty) { | ||
this._dirty = false; | ||
if (this._value) { | ||
this.element.setAttribute(this.name, ''); | ||
} | ||
this._setText(value); | ||
else { | ||
this.element.removeAttribute(this.name); | ||
} | ||
} | ||
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); | ||
} | ||
export class PropertyCommitter extends AttributeCommitter { | ||
constructor(element, name, strings) { | ||
super(element, name, strings); | ||
this.single = | ||
(strings.length === 2 && strings[0] === '' && strings[1] === ''); | ||
} | ||
_setNode(value) { | ||
if (this._previousValue === value) { | ||
return; | ||
_getValue() { | ||
if (this.single) { | ||
return this.parts[0]._value; | ||
} | ||
this.clear(); | ||
this._insert(value); | ||
this._previousValue = value; | ||
return super._getValue(); | ||
} | ||
_setText(value) { | ||
const node = this.startNode.nextSibling; | ||
value = value === undefined ? '' : value; | ||
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; | ||
commit() { | ||
if (this.dirty) { | ||
this.dirty = false; | ||
this.element[this.name] = this._getValue(); | ||
} | ||
else { | ||
this._setNode(document.createTextNode(value)); | ||
} | ||
this._previousValue = value; | ||
} | ||
_setTemplateResult(value) { | ||
const template = this.instance._getTemplate(value); | ||
let instance; | ||
if (this._previousValue && this._previousValue.template === template) { | ||
instance = this._previousValue; | ||
} | ||
else { | ||
instance = new TemplateInstance(template, this.instance._partCallback, this.instance._getTemplate); | ||
this._setNode(instance._clone()); | ||
this._previousValue = instance; | ||
} | ||
instance.update(value.values); | ||
} | ||
export class EventPart { | ||
constructor(element, eventName) { | ||
this._value = undefined; | ||
this._dirty = true; | ||
this.element = element; | ||
this.eventName = eventName; | ||
} | ||
_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 us 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 = document.createTextNode(''); | ||
this._insert(itemStart); | ||
} | ||
itemPart = new NodePart(this.instance, itemStart, this.endNode); | ||
itemParts.push(itemPart); | ||
setValue(value) { | ||
value = getValue(this, value); | ||
if (value !== noChange) { | ||
if ((value == null) !== (this._value == null)) { | ||
this._dirty = true; | ||
} | ||
itemPart.setValue(item); | ||
partIndex++; | ||
this._value = value; | ||
} | ||
if (partIndex === 0) { | ||
this.clear(); | ||
this._previousValue = undefined; | ||
} | ||
else if (partIndex < itemParts.length) { | ||
const lastPart = itemParts[partIndex - 1]; | ||
// Truncate the parts array so _previousValue reflects the current state | ||
itemParts.length = partIndex; | ||
this.clear(lastPart.endNode.previousSibling); | ||
lastPart.endNode = this.endNode; | ||
} | ||
} | ||
_setPromise(value) { | ||
this._previousValue = value; | ||
value.then((v) => { | ||
if (this._previousValue === value) { | ||
this.setValue(v); | ||
commit() { | ||
if (this._dirty) { | ||
this._dirty = false; | ||
if (this._value == null) { | ||
this.element.removeEventListener(this.eventName, this); | ||
} | ||
}); | ||
} | ||
clear(startNode = this.startNode) { | ||
removeNodes(this.startNode.parentNode, startNode.nextSibling, this.endNode); | ||
} | ||
} | ||
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, partCallback, getTemplate) { | ||
this._parts = []; | ||
this.template = template; | ||
this._partCallback = partCallback; | ||
this._getTemplate = getTemplate; | ||
} | ||
update(values) { | ||
let valueIndex = 0; | ||
for (const part of this._parts) { | ||
if (!part) { | ||
valueIndex++; | ||
} | ||
else if (part.size === undefined) { | ||
part.setValue(values[valueIndex]); | ||
valueIndex++; | ||
} | ||
else { | ||
part.setValue(values, valueIndex); | ||
valueIndex += part.size; | ||
this.element.addEventListener(this.eventName, this); | ||
} | ||
} | ||
} | ||
_clone() { | ||
// Clone the node, rather than importing it, to keep the fragment in the | ||
// template's document. This leaves the fragment inert so custom elements | ||
// won't upgrade until after the main document adopts the node. | ||
const fragment = this.template.element.content.cloneNode(true); | ||
const parts = this.template.parts; | ||
if (parts.length > 0) { | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be | ||
// null | ||
const walker = document.createTreeWalker(fragment, 133 /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | | ||
NodeFilter.SHOW_TEXT */, null, false); | ||
let index = -1; | ||
for (let i = 0; i < parts.length; i++) { | ||
const part = parts[i]; | ||
const partActive = isTemplatePartActive(part); | ||
// An inactive part has no coresponding Template node. | ||
if (partActive) { | ||
while (index < part.index) { | ||
index++; | ||
walker.nextNode(); | ||
} | ||
} | ||
this._parts.push(partActive ? this._partCallback(this, part, walker.currentNode) : undefined); | ||
} | ||
handleEvent(event) { | ||
if (typeof this._value === 'function') { | ||
this._value.call(this.element, event); | ||
} | ||
return fragment; | ||
else if (typeof this._value.handleEvent === 'function') { | ||
this._value.handleEvent(event); | ||
} | ||
} | ||
} | ||
/** | ||
* Reparents nodes, starting from `startNode` (inclusive) to `endNode` | ||
* (exclusive), into another container (could be the same container), before | ||
* `beforeNode`. If `beforeNode` is null, it appends the nodes to the | ||
* container. | ||
*/ | ||
export const reparentNodes = (container, start, end = null, before = null) => { | ||
let node = start; | ||
while (node !== end) { | ||
const n = node.nextSibling; | ||
container.insertBefore(node, before); | ||
node = n; | ||
} | ||
}; | ||
/** | ||
* Removes nodes, starting from `startNode` (inclusive) to `endNode` | ||
* (exclusive), from `container`. | ||
*/ | ||
export const removeNodes = (container, startNode, endNode = null) => { | ||
let node = startNode; | ||
while (node !== endNode) { | ||
const n = node.nextSibling; | ||
container.removeChild(node); | ||
node = n; | ||
} | ||
}; | ||
//# sourceMappingURL=lit-html.js.map |
{ | ||
"name": "lit-html", | ||
"version": "0.10.2", | ||
"version": "0.11.0-dev.1", | ||
"description": "HTML template literals in JavaScript", | ||
@@ -16,2 +16,5 @@ "license": "BSD-3-Clause", | ||
"/lit-html.d.ts", | ||
"/core.js", | ||
"/core.js.map", | ||
"/core.d.ts", | ||
"/lib/", | ||
@@ -24,5 +27,6 @@ "/src/", | ||
"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", | ||
"test": "npm run build && npm run lint && wct --npm", | ||
"quicktest": "wct -l chrome -p --npm", | ||
"checksize": "uglifyjs lit-html.js -mc --toplevel | gzip -9 | wc -c", | ||
"checksize": "uglifyjs core.js -mc --toplevel | gzip -9 | wc -c", | ||
"min": "uglifyjs core.js -mc --toplevel", | ||
"format": "find src test | grep '\\.js$\\|\\.ts$' | xargs clang-format --style=file -i", | ||
@@ -29,0 +33,0 @@ "lint": "tslint --project ./", |
399
README.md
@@ -5,3 +5,3 @@ > ## 🛠 Status: In Development | ||
# lit-html | ||
HTML templates, via JavaScript template literals | ||
Efficient, Expressive, Extensible HTML templates in JavaScript | ||
@@ -11,7 +11,11 @@ [![Build Status](https://travis-ci.org/Polymer/lit-html.svg?branch=master)](https://travis-ci.org/Polymer/lit-html) | ||
## Documentation | ||
Full documentation is available at [polymer.github.io/lit-html](https://polymer.github.io/lit-html). | ||
## Overview | ||
`lit-html` lets you write [HTML templates](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template) with JavaScript [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals), and efficiently render and _re-render_ those templates to DOM. | ||
`lit-html` lets you write [HTML templates](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template) in JavaScript with [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals). | ||
Visit the [lit-html documentation](https://polymer.github.io/lit-html). | ||
lit-html templates are plain JavaScript and combine the familiarity of writing HTML with the power of JavaScript. lit-html takes care of efficiently rendering templates to DOM, including efficiently updating the DOM with new values. | ||
@@ -24,4 +28,2 @@ ```javascript | ||
// Call the function with some data, and pass the result to render() | ||
// This renders <div>Hello Steve!</div> to the document body | ||
@@ -39,21 +41,2 @@ render(helloTemplate('Steve'), document.body); | ||
### Announcement at Polymer Summit 2017 | ||
<p align="center"> | ||
<a href="https://www.youtube.com/watch?v=ruql541T7gc"> | ||
<img src="https://img.youtube.com/vi/ruql541T7gc/0.jpg" width="256" alt="Efficient, Expressive, and Extensible HTML Templates video"> | ||
<br> | ||
Efficient, Expressive, and Extensible HTML Templates video | ||
</a> | ||
</p> | ||
## Motivation | ||
`lit-html` has four main goals: | ||
1. Efficient updates of previously rendered 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. | ||
4. Very small size. | ||
## Installation | ||
@@ -65,369 +48,9 @@ | ||
## How it Works | ||
`lit-html` utilizes some unique properties of HTML `<template>` elements and JavaScript template literals. So it's helpful to understand them first. | ||
### Tagged Template Literals | ||
A JavaScript template literal is a string literal that can have other JavaScript expressions embedded in it: | ||
```javascript | ||
`My name is ${name}.` | ||
``` | ||
A _tagged_ template literal is prefixed with a special template tag function: | ||
```javascript | ||
let name = 'Monica'; | ||
tag`My name is ${name}.` | ||
``` | ||
Tags are functions of the form: `tag(strings, ...values)`, where `strings` is an immutable array of the literal parts, and values are the results of the embedded expressions. | ||
In the preceding example, `strings` would be `['My name is ', '.']`, and `values` would be `['Monica']`. | ||
### HTML `<template>` Elements | ||
A `<template>` element is an inert tree of DOM (script don't run, images don't load, custom elements aren't upgraded, etc) that can be efficiently cloned. It's usually used to tell the HTML parser that a section of the document must not be instantiated when parsed, but by code at a later time, but it can also be created imperatively with `createElement` and `innerHTML`. | ||
### Template Creation | ||
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. Then it walks the template's DOM and extracts the placeholder and remembers their location. | ||
Every call to `html` returns a `TemplateResult` which contains the template created on the first call, and the expression values for the current call. | ||
### Template Rendering | ||
`render()` takes a `TemplateResult` and renders it to a DOM container. On the initial render it clones the template, then walks it using the remembered placeholder positions, to create `Part`s. | ||
A `Part` is a "hole" in the DOM where values can be injected. `lit-html` includes two type of parts by default: `NodePart` and `AttributePart`, which let you set text content and attribute values respectively. The `Part`s, container, and template they were created from are grouped together in an object called a `TemplateInstance`. | ||
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. | ||
## Performance | ||
`lit-html` is designed to be lightweight and fast (though performance benchmarking is just starting). | ||
* It utilizes the built-in JS and HTML parsers - it doesn't include any expression or markup parser of its own. | ||
* It only updates the dynamic parts of templates - static parts are untouched, not even walked for diffing, after the initial render. | ||
* It uses cloning for initial render. | ||
This should make the approach generally fast and small. Actual science and optimization and still TODOs at this time. | ||
## Features | ||
### Simple expressions and literals | ||
Anything coercible to strings are supported: | ||
```javascript | ||
const render = () => html`foo is ${foo}`; | ||
``` | ||
### Attribute-value Expressions | ||
```javascript | ||
const render = () => html`<div class="${blue}"></div>`; | ||
``` | ||
### SVG Support | ||
To create partial SVG templates (templates that will render inside an `<svg>` tag in the SVG namespace), use the `svg` template tag instead of the `html` template tag: | ||
```javascript | ||
const grid = svg` | ||
<g> | ||
${[0, 10, 20].map((x) => svg`<line x1=${x} y1="0" x2=${x} y2="20" stroke="#000"/>`)} | ||
${[0, 10, 20].map((y) => svg`<line x1="0" y1=${y} x2="20" y2=${y} stroke="#000"/>`)} | ||
</g> | ||
`; | ||
``` | ||
### Safety | ||
Because `lit-html` templates are parsed before values are set, they are safer than generating HTML via string-concatenation. Attributes are set via `setAttribute()` and node text via `textContent`, so the structure of template instances cannot be accidentally changed by expression values, and values are automatically escaped. | ||
_TODO: Add sanitization hooks to disallow inline event handlers, etc._ | ||
### Case-sensitive Attribute Names | ||
Attribute parts store both the HTML-parsed name and the raw name pulled from the string literal. This allows extensions, such as those that might set properties on elements using attribute syntax, to get case-sensitive names. | ||
```javascript | ||
const render = () => html`<div someProp="${blue}"></div>`; | ||
render().template.parts[0].rawName === 'someProp'; | ||
``` | ||
### Arrays/Iterables | ||
```javascript | ||
const items = [1, 2, 3]; | ||
const render = () => html`items = ${items.map((i) => `item: ${i}`)}`; | ||
``` | ||
```javascript | ||
const items = { | ||
a: 1, | ||
b: 23, | ||
c: 456, | ||
}; | ||
const render = () => html`items = ${Object.entries(items)}`; | ||
``` | ||
### Nested Templates | ||
```javascript | ||
const header = html`<h1>${title}</h1>`; | ||
const render = () => html` | ||
${header} | ||
<p>And the body</p> | ||
`; | ||
``` | ||
### Promises | ||
Promises are rendered when they resolve, leaving the previous value in place until they do. Races are handled, so that if an unresolved Promise is overwritten, it won't update the template when it finally resolves. | ||
```javascript | ||
const render = () => html` | ||
The response is ${fetch('sample.txt').then((r) => r.text())}. | ||
`; | ||
``` | ||
### Directives | ||
Directives are functions that can extend lit-html by directly interacting with the Part API. | ||
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: | ||
```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 = ${safe(_=>data.foo)}`; | ||
``` | ||
This example increments a counter on every render: | ||
```javascript | ||
const render = () => html` | ||
<div> | ||
${directive((part) => part.setValue((part.previousValue + 1) || 0))} | ||
</div>`; | ||
``` | ||
lit-html includes a few directives: | ||
#### `repeat(items, keyfn, template)` | ||
A loop that supports efficient re-ordering by using item keys. | ||
Example: | ||
```javascript | ||
const render = () => html` | ||
<ul> | ||
${repeat(items, (i) => i.id, (i, index) => html` | ||
<li>${index}: ${i.name}</li>`)} | ||
</ul> | ||
`; | ||
``` | ||
#### `until(promise, defaultContent)` | ||
Renders `defaultContent` until `promise` resolves, then it renders the resolved value of `promise`. | ||
Example: | ||
```javascript | ||
const render = () => html` | ||
<p> | ||
${until( | ||
fetch('content.txt').then((r) => r.text()), | ||
html`<span>Loading...</span>`)} | ||
</p> | ||
`; | ||
``` | ||
#### `asyncAppend(asyncIterable)` and `asyncReplace(asyncIterable)` | ||
JavaScript asynchronous iterators provide a generic interface for asynchronous sequential access to data. Much like an iterator, a consumer requests the next data item with a a call to `next()`, but with asynchronous iterators `next()` returns a `Promise`, allowing the iterator to provide the item when it's ready. | ||
lit-html offers two directives to consume asynchronous iterators: | ||
* `asyncAppend` renders the values of an [async iterable](https://github.com/tc39/proposal-async-iteration), | ||
appending each new value after the previous. | ||
* `asyncReplace` renders the values of an [async iterable](https://github.com/tc39/proposal-async-iteration), | ||
replacing the previous value with the new value. | ||
Example: | ||
```javascript | ||
const wait = (t) => new Promise((resolve) => setTimeout(resolve, t)); | ||
/** | ||
* Returns an async iterable that yields increasing integers. | ||
*/ | ||
async function* countUp() { | ||
let i = 0; | ||
while (true) { | ||
yield i++; | ||
await wait(1000); | ||
} | ||
} | ||
render(html` | ||
Count: <span>${asyncReplace(countUp())}</span>. | ||
`, document.body); | ||
``` | ||
In the near future, `ReadableStream`s will be async iterables, enabling streaming `fetch()` directly into a template: | ||
```javascript | ||
// Endpoint that returns a billion digits of PI, streamed. | ||
const url = | ||
'https://cors-anywhere.herokuapp.com/http://stuff.mit.edu/afs/sipb/contrib/pi/pi-billion.txt'; | ||
const streamingResponse = (async () => { | ||
const response = await fetch(url); | ||
return response.body.getReader(); | ||
})(); | ||
render(html`π is: ${asyncAppend(streamingResponse)}`, document.body); | ||
``` | ||
### Composability | ||
These features compose so you can render iterables of functions that return arrays of nested templates, etc... | ||
### Extensibility | ||
`lit-html` is designed to be extended by more opinionated flavors of template syntaxes. For instance, `lit-html` doesn't support declarative event handlers or property setting out-of-the-box. A layer on top can add that while exposing the same API, by implementing a custom `render()` function. | ||
Some examples of possible extensions: | ||
* Property setting: Attribute expressions in templates could set properties on node. | ||
* Event handlers: Specially named attributes can install event handlers. | ||
* HTML values: `lit-html` creates `Text` nodes by default. Extensions could allow setting `innerHTML`. | ||
## Status | ||
`lit-html` is still under development. | ||
`lit-html` is under active development and has not yet had a 1.0 release. The | ||
internal API may still change somewhat. The `html` and `render` API is stable. | ||
* It uses JavaScript modules, and there's no build set up yet, so out-of-the-box it only runs in Safari 10.1, Chrome 61, and Firefox 60 and Edge. | ||
* It has a growing test suite, but it has only been run manually on Chrome Canary, Safari 10.1 and Firefox 54. | ||
* Much more test coverage is needed for complex templates, especially template composition and Function and Iterable values. | ||
* It has not been benchmarked thoroughly yet. | ||
* The API may change. | ||
## Contributing | ||
Even without a build configuration, `lit-html` minified with `babili` and gzipped measures in at less than 1.7k. We will strive to keep the size extremely small. | ||
## Benefits over HTML templates | ||
`lit-html` has basically all of the benefits of HTML-in-JS systems like JSX, like: | ||
### Lighter weight | ||
There's no need to load an expression parser and evaluator. | ||
### Seamless access to data | ||
Since template literals are evaluated in JavaScript, their expressions have access to every variable in that scope, including globals, module and block scopes, and `this` inside methods. | ||
If the main use of templates is to inject values into HTML, this breaks down a major barrier between templates and values. | ||
### Faster expression evaluation | ||
They're just JavaScript expressions. | ||
### IDE support by default | ||
In a type-checking environment like TypeScript, expressions are checked because they are just regular script. Hover-over docs and code-completion just work as well. | ||
### Case-sensitive parsing | ||
Template literals preserve case, even though the HTML parser doesn't for attribute names. `lit-html` extracts the raw attribute names, which is useful for template syntaxes that use attribute syntax to set properties on elements. | ||
## Benefits over JSX | ||
### Native syntax | ||
No tooling required. Understood by all JS editors and tools. | ||
### No VDOM overhead | ||
`lit-html` only re-renders the dynamic parts of a template, so it doesn't produce a VDOM tree of the entire template contents, it just produces new values for each expression. By completely skipping static template parts, it saves work. | ||
### Scoped | ||
JSX requires that the compiler be configured with the function to compile tags to. You can't mix two different JSX configurations in the same file. | ||
The `html` template tag is just a variable, probably an imported function. You can have any number of similar functions in the same JS scope, or set `html` to different implementations. | ||
### Templates are values | ||
JSX translates to function calls, and can't be manipulated on a per-template basis at runtime. `lit-html` produces a template object at runtime, which can be further processed by libraries like ShadyCSS. | ||
### CSS-compatible syntax | ||
Because template literals use `${}` as the expression delimiter, CSS's use of `{}` isn't interpreted as an expression. You can include style tags in your templates as you would expect: | ||
```javascript | ||
html` | ||
<style> | ||
:host { | ||
background: burlywood; | ||
} | ||
</style> | ||
` | ||
``` | ||
## Future Work | ||
### Higher-Order Templates examples | ||
#### `when(cond, then, else)` | ||
An if-directive that retains the `then` and `else` _instances_ for fast switching between the two states, like `<dom-if>`. | ||
Example: | ||
```javascript | ||
const render = () => html` | ||
${when(state === 'loading', | ||
html`<div>Loading...</div>`, | ||
html`<p>${message}</p>`)} | ||
`; | ||
``` | ||
#### `guard(guardExpr, template)` | ||
Only re-renders an instance if the guard expression has changed since the last render. | ||
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. | ||
Example: | ||
```javascript | ||
const render = () => html` | ||
<div>Current User: ${guard(user, () => user.getProfile())}</div> | ||
`; | ||
``` | ||
Please see [CONTRIBUTING.md](). |
@@ -15,3 +15,3 @@ /** | ||
import {directive, DirectiveFn, NodePart} from '../lit-html.js'; | ||
import {directive, DirectiveFn, NodePart} from '../core.js'; | ||
@@ -36,10 +36,10 @@ /** | ||
export const asyncAppend = <T>( | ||
value: AsyncIterable<T>, mapper?: (v: T, index?: number) => any): DirectiveFn<NodePart> => | ||
directive(async (part: NodePart) => { | ||
value: AsyncIterable<T>, mapper?: (v: T, index?: number) => any): | ||
DirectiveFn<NodePart> => directive(async (part: NodePart) => { | ||
// If we've already set up this particular iterable, we don't need | ||
// to do anything. | ||
if (value === part._previousValue) { | ||
if (value === part._value) { | ||
return; | ||
} | ||
part._previousValue = value; | ||
part._value = value; | ||
@@ -60,3 +60,3 @@ // We keep track of item Parts across iterations, so that we can | ||
// the part, and if not bail because a new value owns this part | ||
if (part._previousValue !== value) { | ||
if (part._value !== value) { | ||
break; | ||
@@ -92,3 +92,4 @@ } | ||
} | ||
itemPart = new NodePart(part.instance, itemStartNode, part.endNode); | ||
itemPart = | ||
new NodePart(itemStartNode, part.endNode, part.templateFactory); | ||
itemPart.setValue(v); | ||
@@ -95,0 +96,0 @@ i++; |
@@ -15,3 +15,3 @@ /** | ||
import {directive, DirectiveFn, NodePart} from '../lit-html.js'; | ||
import {directive, DirectiveFn, NodePart} from '../core.js'; | ||
@@ -37,7 +37,7 @@ /** | ||
export const asyncReplace = | ||
<T>(value: AsyncIterable<T>, mapper?: (v: T, index?: number) => any): DirectiveFn<NodePart> => | ||
directive(async (part: NodePart) => { | ||
<T>(value: AsyncIterable<T>, mapper?: (v: T, index?: number) => any): | ||
DirectiveFn<NodePart> => directive(async (part: NodePart) => { | ||
// If we've already set up this particular iterable, we don't need | ||
// to do anything. | ||
if (value === part._previousValue) { | ||
if (value === part._value) { | ||
return; | ||
@@ -49,5 +49,5 @@ } | ||
const itemPart = | ||
new NodePart(part.instance, part.startNode, part.endNode); | ||
new NodePart(part.startNode, part.endNode, part.templateFactory); | ||
part._previousValue = itemPart; | ||
part._value = itemPart; | ||
@@ -65,3 +65,3 @@ let i = 0; | ||
// the part, and if not bail because a new value owns this part | ||
if (part._previousValue !== itemPart) { | ||
if (part._value !== itemPart) { | ||
break; | ||
@@ -68,0 +68,0 @@ } |
@@ -15,5 +15,7 @@ /** | ||
import {AttributePart, defaultPartCallback, noChange, getValue, Part, SVGTemplateResult, TemplateInstance, TemplatePart, TemplateResult} from '../lit-html.js'; | ||
import {AttributeCommitter, Part, SVGTemplateResult, TemplateProcessor, TemplateResult} from '../core.js'; | ||
import {BooleanAttributePart, EventPart, PropertyCommitter} from '../lit-html.js'; | ||
export {render} from '../lit-html.js'; | ||
export {render} from '../core.js'; | ||
export {BooleanAttributePart, EventPart} from '../lit-html.js'; | ||
@@ -24,3 +26,3 @@ /** | ||
export const html = (strings: TemplateStringsArray, ...values: any[]) => | ||
new TemplateResult(strings, values, 'html', extendedPartCallback); | ||
new TemplateResult(strings, values, 'html', templateProcessor); | ||
@@ -31,3 +33,3 @@ /** | ||
export const svg = (strings: TemplateStringsArray, ...values: any[]) => | ||
new SVGTemplateResult(strings, values, 'svg', extendedPartCallback); | ||
new SVGTemplateResult(strings, values, 'svg', templateProcessor); | ||
@@ -58,113 +60,25 @@ /** | ||
* | ||
* @deprecated Please use /lit-html.js instead. lit-extended will be removed in | ||
* a future version. | ||
*/ | ||
export const extendedPartCallback = | ||
(instance: TemplateInstance, templatePart: TemplatePart, node: Node): | ||
Part => { | ||
if (templatePart.type === 'attribute') { | ||
if (templatePart.rawName!.substr(0, 3) === 'on-') { | ||
const eventName = templatePart.rawName!.slice(3); | ||
return new EventPart(instance, node as Element, eventName); | ||
} | ||
const lastChar = templatePart.name!.substr(templatePart.name!.length - 1); | ||
if (lastChar === '$') { | ||
const name = templatePart.name!.slice(0, -1); | ||
return new AttributePart( | ||
instance, node as Element, name, templatePart.strings!); | ||
} | ||
if (lastChar === '?') { | ||
const name = templatePart.name!.slice(0, -1); | ||
return new BooleanAttributePart( | ||
instance, node as Element, name, templatePart.strings!); | ||
} | ||
return new PropertyPart( | ||
instance, | ||
node as Element, | ||
templatePart.rawName!, | ||
templatePart.strings!); | ||
} | ||
return defaultPartCallback(instance, templatePart, node); | ||
}; | ||
/** | ||
* 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 === noChange) { | ||
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 LitExtendedTemplateProcessor extends TemplateProcessor { | ||
handleAttributeExpressions(element: Element, name: string, strings: string[]): | ||
Part[] { | ||
if (name.substr(0, 3) === 'on-') { | ||
const eventName = name.slice(3); | ||
return [new EventPart(element, eventName)]; | ||
} | ||
} | ||
} | ||
export class PropertyPart extends AttributePart { | ||
setValue(values: any[], startIndex: number): void { | ||
const s = this.strings; | ||
let value: any; | ||
if (this._equalToPreviousValues(values, startIndex)) { | ||
return; | ||
const lastChar = name.substr(name.length - 1); | ||
if (lastChar === '$') { | ||
const comitter = | ||
new AttributeCommitter(element, name.slice(0, -1), strings); | ||
return comitter.parts; | ||
} | ||
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]); | ||
} else { | ||
// Interpolation, so interpolate | ||
value = this._interpolate(values, startIndex); | ||
if (lastChar === '?') { | ||
return [new BooleanAttributePart(element, name.slice(0, -1), strings)]; | ||
} | ||
if (value !== noChange) { | ||
(this.element as any)[this.name] = value; | ||
} | ||
this._previousValues = values; | ||
const comitter = new PropertyCommitter(element, name, strings); | ||
return comitter.parts; | ||
} | ||
} | ||
export class EventPart implements Part { | ||
instance: TemplateInstance; | ||
element: Element; | ||
eventName: string; | ||
private _listener: any; | ||
constructor(instance: TemplateInstance, element: Element, eventName: string) { | ||
this.instance = instance; | ||
this.element = element; | ||
this.eventName = eventName; | ||
} | ||
setValue(value: any): void { | ||
const listener = getValue(this, value); | ||
if (listener === this._listener) { | ||
return; | ||
} | ||
if (listener == null) { | ||
this.element.removeEventListener(this.eventName, this); | ||
} else if (this._listener == null) { | ||
this.element.addEventListener(this.eventName, this); | ||
} | ||
this._listener = listener; | ||
} | ||
handleEvent(event: Event) { | ||
if (typeof this._listener === 'function') { | ||
this._listener.call(this.element, event); | ||
} else if (typeof this._listener.handleEvent === 'function') { | ||
this._listener.handleEvent(event); | ||
} | ||
} | ||
} | ||
export const templateProcessor = new LitExtendedTemplateProcessor(); |
@@ -15,6 +15,6 @@ /** | ||
import {Template, isTemplatePartActive, TemplatePart} from '../lit-html.js'; | ||
import {isTemplatePartActive, Template, TemplatePart} from '../core.js'; | ||
const walkerNodeFilter = NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | | ||
NodeFilter.SHOW_TEXT; | ||
const walkerNodeFilter = | ||
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT; | ||
@@ -34,11 +34,10 @@ /** | ||
* div | ||
* div <-- stop removing since previous sibling is the removing node (div#1, removed 4 nodes) | ||
* div <-- stop removing since previous sibling is the removing node (div#1, | ||
* removed 4 nodes) | ||
*/ | ||
export function removeNodesFromTemplate(template: Template, nodesToRemove: Set<Node>) { | ||
export function removeNodesFromTemplate( | ||
template: Template, nodesToRemove: Set<Node>) { | ||
const {element: {content}, parts} = template; | ||
const walker = document.createTreeWalker( | ||
content, | ||
walkerNodeFilter, | ||
null as any, | ||
false); | ||
const walker = | ||
document.createTreeWalker(content, walkerNodeFilter, null as any, false); | ||
let partIndex = 0; | ||
@@ -81,7 +80,4 @@ let part = parts[0]; | ||
let count = 1; | ||
const walker = document.createTreeWalker( | ||
node, | ||
walkerNodeFilter, | ||
null as any, | ||
false); | ||
const walker = | ||
document.createTreeWalker(node, walkerNodeFilter, null as any, false); | ||
while (walker.nextNode()) { | ||
@@ -93,11 +89,12 @@ count++; | ||
const nextActiveIndexInTemplateParts = (parts: TemplatePart[], startIndex: number = -1) => { | ||
for (let i = startIndex + 1; i < parts.length; i++) { | ||
const part = parts[i]; | ||
if (isTemplatePartActive(part)) { | ||
return i; | ||
} | ||
} | ||
return -1; | ||
}; | ||
const nextActiveIndexInTemplateParts = | ||
(parts: TemplatePart[], startIndex: number = -1) => { | ||
for (let i = startIndex + 1; i < parts.length; i++) { | ||
const part = parts[i]; | ||
if (isTemplatePartActive(part)) { | ||
return i; | ||
} | ||
} | ||
return -1; | ||
}; | ||
@@ -118,7 +115,4 @@ /** | ||
} | ||
const walker = document.createTreeWalker( | ||
content, | ||
walkerNodeFilter, | ||
null as any, | ||
false); | ||
const walker = | ||
document.createTreeWalker(content, walkerNodeFilter, null as any, false); | ||
let partIndex = nextActiveIndexInTemplateParts(parts); | ||
@@ -125,0 +119,0 @@ let insertCount = 0; |
@@ -15,3 +15,3 @@ /** | ||
import {directive, DirectiveFn, NodePart, removeNodes, reparentNodes} from '../lit-html.js'; | ||
import {directive, DirectiveFn, NodePart, removeNodes, reparentNodes} from '../core.js'; | ||
@@ -30,7 +30,9 @@ export type KeyFn<T> = (item: T) => any; | ||
export function repeat<T>( | ||
items: T[], keyFn: KeyFn<T>, template: ItemTemplate<T>): DirectiveFn<NodePart>; | ||
export function repeat<T>(items: T[], template: ItemTemplate<T>): DirectiveFn<NodePart>; | ||
items: T[], keyFn: KeyFn<T>, template: ItemTemplate<T>): | ||
DirectiveFn<NodePart>; | ||
export function repeat<T>( | ||
items: T[], template: ItemTemplate<T>): DirectiveFn<NodePart>; | ||
export function repeat<T>( | ||
items: Iterable<T>, | ||
keyFnOrTemplate: KeyFn<T>| ItemTemplate<T>, | ||
keyFnOrTemplate: KeyFn<T>|ItemTemplate<T>, | ||
template?: ItemTemplate<T>): DirectiveFn<NodePart> { | ||
@@ -74,3 +76,3 @@ let keyFn: KeyFn<T>; | ||
container.insertBefore(endNode, currentMarker); | ||
itemPart = new NodePart(part.instance, marker, endNode); | ||
itemPart = new NodePart(marker, endNode, part.templateFactory); | ||
if (key !== undefined) { | ||
@@ -77,0 +79,0 @@ keyMap.set(key, itemPart); |
@@ -15,7 +15,7 @@ /** | ||
import {removeNodes, Template, templateCaches, TemplateContainer, TemplateInstance, TemplateResult} from '../lit-html.js'; | ||
import {removeNodes, Template, templateCaches, TemplateContainer, TemplateInstance, TemplateResult} from '../core.js'; | ||
import {insertNodeIntoTemplate, removeNodesFromTemplate} from './modify-template.js'; | ||
export {html, svg, TemplateResult} from '../lit-html.js'; | ||
export {html, svg, TemplateResult} from '../core.js'; | ||
@@ -33,2 +33,16 @@ declare global { | ||
const verifyShadyCSSVersion = () => { | ||
if (typeof window.ShadyCSS === 'undefined') { | ||
return false; | ||
} | ||
if (typeof window.ShadyCSS.prepareTemplateDom === 'undefined') { | ||
console.warn( | ||
`Incompatible ShadyCSS version detected.` + | ||
`Please update to at least @webcomponents/webcomponentsjs@2.0.2 and` + | ||
`@webcomponents/shadycss@1.3.1.`); | ||
return false; | ||
} | ||
return true; | ||
}; | ||
/** | ||
@@ -49,3 +63,3 @@ * Template factory which scopes template DOM using ShadyCSS. | ||
const element = result.getTemplateElement(); | ||
if (typeof window.ShadyCSS === 'object') { | ||
if (verifyShadyCSSVersion()) { | ||
window.ShadyCSS.prepareTemplateDom(element, scopeName); | ||
@@ -71,4 +85,8 @@ } | ||
const {element: {content}} = template; | ||
const styles = content.querySelectorAll('style'); | ||
removeNodesFromTemplate(template, new Set(Array.from(styles))); | ||
// IE 11 doesn't support the iterable param Set constructor | ||
const styles = new Set<Element>(); | ||
Array.from(content.querySelectorAll('style')).forEach((s: Element) => { | ||
styles.add(s); | ||
}); | ||
removeNodesFromTemplate(template, styles); | ||
}); | ||
@@ -92,3 +110,4 @@ } | ||
* dynamically rendered to the scope (after the first scope render), they will | ||
* not be scoped and the <style> will be left in the template and rendered output. | ||
* not be scoped and the <style> will be left in the template and rendered | ||
* output. | ||
*/ | ||
@@ -140,3 +159,3 @@ const ensureStylesScoped = | ||
if (instance !== undefined && instance.template === template && | ||
instance._partCallback === result.partCallback) { | ||
instance.processor === result.processor) { | ||
instance.update(result.values); | ||
@@ -147,4 +166,3 @@ return; | ||
// First render, create a new TemplateInstance and append it | ||
instance = | ||
new TemplateInstance(template, result.partCallback, templateFactory); | ||
instance = new TemplateInstance(template, result.processor, templateFactory); | ||
(container as TemplateContainer).__templateInstance = instance; | ||
@@ -155,8 +173,6 @@ | ||
const host = container instanceof ShadowRoot ? | ||
container.host : | ||
undefined; | ||
const host = container instanceof ShadowRoot ? container.host : undefined; | ||
// If there's a shadow host, do ShadyCSS scoping... | ||
if (host !== undefined && typeof window.ShadyCSS === 'object') { | ||
if (host !== undefined && verifyShadyCSSVersion()) { | ||
ensureStylesScoped(fragment, template, scopeName); | ||
@@ -168,2 +184,2 @@ window.ShadyCSS.styleElement(host); | ||
container.appendChild(fragment); | ||
} | ||
} |
@@ -15,3 +15,3 @@ /** | ||
import {directive, DirectiveFn, NodePart} from '../lit-html.js'; | ||
import {_isPrimitiveValue, directive, DirectiveFn, NodePart} from '../core.js'; | ||
@@ -27,5 +27,15 @@ /** | ||
directive((part: NodePart): void => { | ||
// Dirty check primitive values | ||
if (part._value === value && _isPrimitiveValue(value)) { | ||
return; | ||
} | ||
// Use a <template> to parse HTML into Nodes | ||
const tmp = document.createElement('template'); | ||
tmp.innerHTML = value; | ||
part.setValue(document.importNode(tmp.content, true)); | ||
// Remember the actual value passed to unsafeHTML rather than the DOM | ||
// Nodes we created | ||
part._value = value; | ||
}); |
@@ -15,3 +15,3 @@ /** | ||
import {directive, DirectiveFn, NodePart} from '../lit-html.js'; | ||
import {directive, DirectiveFn, NodePart} from '../core.js'; | ||
@@ -21,6 +21,7 @@ /** | ||
*/ | ||
export const until = (promise: Promise<any>, defaultContent: any): DirectiveFn<NodePart> => | ||
directive((part: NodePart): void => { | ||
part.setValue(defaultContent); | ||
part.setValue(promise); | ||
}); | ||
export const until = | ||
(promise: Promise<any>, defaultContent: any): DirectiveFn<NodePart> => | ||
directive((part: NodePart): void => { | ||
part.setValue(defaultContent); | ||
part.setValue(promise); | ||
}); |
@@ -15,725 +15,165 @@ /** | ||
// 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. | ||
export const templateCaches = | ||
new Map<string, Map<TemplateStringsArray, Template>>(); | ||
import {AttributeCommitter, getValue, noChange, Part, SVGTemplateResult, TemplateProcessor, TemplateResult} from './core.js'; | ||
export {render} from './core.js'; | ||
/** | ||
* Interprets a template literal as an HTML template that can efficiently | ||
* render to and update a container. | ||
* Interprets a template literal as a lit-html HTML template. | ||
*/ | ||
export const html = (strings: TemplateStringsArray, ...values: any[]) => | ||
new TemplateResult(strings, values, 'html'); | ||
new TemplateResult(strings, values, 'html', templateProcessor); | ||
/** | ||
* Interprets a template literal as an SVG template that can efficiently | ||
* render to and update a container. | ||
* Interprets a template literal as a lit-html SVG template. | ||
*/ | ||
export const svg = (strings: TemplateStringsArray, ...values: any[]) => | ||
new SVGTemplateResult(strings, values, 'svg'); | ||
new SVGTemplateResult(strings, values, 'svg', templateProcessor); | ||
/** | ||
* The return type of `html`, which holds a Template and the values from | ||
* interpolated expressions. | ||
*/ | ||
export class TemplateResult { | ||
strings: TemplateStringsArray; | ||
values: any[]; | ||
type: string; | ||
partCallback: PartCallback; | ||
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. | ||
* A PartCallback which allows templates to set properties, declarative | ||
* event handlers, and boolean attributes. | ||
* | ||
* 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. | ||
* Properties are set by prefixing an attribute name with `.`. | ||
* | ||
* This is a hook into the template-creation process for rendering that | ||
* requires some modification of templates before they're used, like ShadyCSS, | ||
* which must add classes to elements and remove styles. | ||
* Attribute names in lit-html templates preserve case, so properties are case | ||
* sensitive. If an expression takes up an entire attribute value, then the | ||
* property is set to that value. If an expression is interpolated with a string | ||
* or other expressions then the property is set to the string result of the | ||
* interpolation. | ||
* | ||
* 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. | ||
* Example: | ||
* | ||
* Templates are usually cached by TemplateResult.strings and | ||
* TemplateResult.type, but may be cached by other keys if this function | ||
* modifies the template. | ||
* html`<input .value=${value}>` | ||
* | ||
* 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; | ||
} | ||
export type TemplateContainer = (Element|DocumentFragment)&{ | ||
__templateInstance?: TemplateInstance; | ||
}; | ||
/** | ||
* Renders a template to a container. | ||
* Event handlers are set by prefixing an attribute name with `@`. | ||
* | ||
* To update a container with new values, reevaluate the template literal and | ||
* call `render` with the new result. | ||
* Example: | ||
* | ||
* @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: TemplateResult, | ||
container: Element|DocumentFragment, | ||
templateFactory: TemplateFactory = defaultTemplateFactory | ||
) { | ||
const template = templateFactory(result); | ||
let instance = (container as TemplateContainer).__templateInstance; | ||
// Repeat render, just call update() | ||
if (instance !== undefined && instance.template === template && | ||
instance._partCallback === result.partCallback) { | ||
instance.update(result.values); | ||
return; | ||
} | ||
// First render, create a new TemplateInstance and append it | ||
instance = | ||
new TemplateInstance(template, result.partCallback, templateFactory); | ||
(container as TemplateContainer).__templateInstance = instance; | ||
const fragment = instance._clone(); | ||
instance.update(result.values); | ||
removeNodes(container, container.firstChild); | ||
container.appendChild(fragment); | ||
} | ||
/** | ||
* 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-positions, not attribute positions, | ||
* in template. | ||
*/ | ||
const nodeMarker = `<!--${marker}-->`; | ||
const markerRegex = new RegExp(`${marker}|${nodeMarker}`); | ||
/** | ||
* This regex extracts the attribute name preceding an attribute-position | ||
* expression. It does this by matching the syntax allowed for attributes | ||
* against the string literal directly preceding the expression, assuming that | ||
* the expression is in an attribute-value position. | ||
* html`<button @click=${(e)=> this.onClickHandler(e)}>Buy Now</button>` | ||
* | ||
* See attributes in the HTML spec: | ||
* https://www.w3.org/TR/html5/syntax.html#attributes-0 | ||
* Boolean attributes are set by prepending `?` to an attribute name. | ||
* | ||
* "\0-\x1F\x7F-\x9F" are Unicode control characters | ||
* Example: | ||
* | ||
* " \x09\x0a\x0c\x0d" are HTML space characters: | ||
* https://www.w3.org/TR/html5/infrastructure.html#space-character | ||
* | ||
* So an attribute is: | ||
* * The name: any character except a control character, space character, ('), | ||
* ("), ">", "=", or "/" | ||
* * Followed by zero or more space characters | ||
* * Followed by "=" | ||
* * Followed by zero or more space characters | ||
* * Followed by: | ||
* * Any character except space, ('), ("), "<", ">", "=", (`), or | ||
* * (") then any non-("), or | ||
* * (') then any non-(') | ||
* html`<input type="checkbox" ?checked=${value}>` | ||
*/ | ||
const lastAttributeNameRegex = | ||
/[ \x09\x0a\x0c\x0d]([^\0-\x1F\x7F-\x9F \x09\x0a\x0c\x0d"'>=/]+)[ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*)$/; | ||
/** | ||
* Finds the closing index of the last closed HTML tag. | ||
* This has 3 possible return values: | ||
* - `-1`, meaning there is no tag in str. | ||
* - `string.length`, meaning the last opened tag is unclosed. | ||
* - Some positive number < str.length, meaning the index of the closing '>'. | ||
*/ | ||
function findTagClose(str: string): number { | ||
const close = str.lastIndexOf('>'); | ||
const open = str.indexOf('<', close + 1); | ||
return open > -1 ? str.length : close; | ||
} | ||
/** | ||
* 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(). | ||
*/ | ||
export class TemplatePart { | ||
constructor( | ||
public type: string, public index: number, public name?: string, | ||
public rawName?: string, public strings?: string[]) { | ||
} | ||
} | ||
export const isTemplatePartActive = (part: TemplatePart) => part.index !== -1; | ||
/** | ||
* An updateable Template that tracks the location of dynamic parts. | ||
*/ | ||
export class Template { | ||
parts: TemplatePart[] = []; | ||
element: HTMLTemplateElement; | ||
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 | ||
const walker = document.createTreeWalker( | ||
content, | ||
133 /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | | ||
NodeFilter.SHOW_TEXT */ | ||
, | ||
null as any, | ||
false); | ||
let index = -1; | ||
let partIndex = 0; | ||
const nodesToRemove: Node[] = []; | ||
// The actual previous node, accounting for removals: if a node is removed | ||
// it will never be the previousNode. | ||
let previousNode: Node|undefined; | ||
// Used to set previousNode at the top of the loop. | ||
let currentNode: Node|undefined; | ||
while (walker.nextNode()) { | ||
index++; | ||
previousNode = currentNode; | ||
const node = currentNode = walker.currentNode as Element; | ||
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { | ||
if (!node.hasAttributes()) { | ||
continue; | ||
} | ||
const attributes = node.attributes; | ||
// Per https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap, | ||
// attributes are not guaranteed to be returned in document order. In | ||
// particular, Edge/IE can return them out of order, so we cannot assume | ||
// a correspondance between part index and attribute index. | ||
let count = 0; | ||
for (let i = 0; i < attributes.length; i++) { | ||
if (attributes[i].value.indexOf(marker) >= 0) { | ||
count++; | ||
} | ||
} | ||
while (count-- > 0) { | ||
// Get the template literal section leading up to the first | ||
// expression in this attribute | ||
const stringForPart = result.strings[partIndex]; | ||
// Find the attribute name | ||
const attributeNameInPart = | ||
lastAttributeNameRegex.exec(stringForPart)![1]; | ||
// Find the corresponding attribute | ||
// TODO(justinfagnani): remove non-null assertion | ||
const attribute = attributes.getNamedItem(attributeNameInPart)!; | ||
const stringsForAttributeValue = attribute.value.split(markerRegex); | ||
this.parts.push(new TemplatePart( | ||
'attribute', | ||
index, | ||
attribute.name, | ||
attributeNameInPart, | ||
stringsForAttributeValue)); | ||
node.removeAttribute(attribute.name); | ||
partIndex += stringsForAttributeValue.length - 1; | ||
} | ||
} else if (node.nodeType === 3 /* Node.TEXT_NODE */) { | ||
const nodeValue = node.nodeValue!; | ||
if (nodeValue.indexOf(marker) < 0) { | ||
continue; | ||
} | ||
const parent = node.parentNode!; | ||
const strings = nodeValue.split(markerRegex); | ||
const lastIndex = strings.length - 1; | ||
// We have a part for each match found | ||
partIndex += 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( | ||
(strings[i] === '') | ||
? document.createComment('') | ||
: document.createTextNode(strings[i]), | ||
node); | ||
this.parts.push(new TemplatePart('node', index++)); | ||
} | ||
parent.insertBefore( | ||
strings[lastIndex] === '' ? | ||
document.createComment('') : | ||
document.createTextNode(strings[lastIndex]), | ||
node); | ||
nodesToRemove.push(node); | ||
} else if ( | ||
node.nodeType === 8 /* Node.COMMENT_NODE */ && | ||
node.nodeValue === marker) { | ||
const parent = node.parentNode!; | ||
// Add a new marker node to be the startNode of the Part if any of the | ||
// following are true: | ||
// * We don't have a previousSibling | ||
// * previousSibling is being removed (thus it's not the | ||
// `previousNode`) | ||
// * previousSibling is not a Text node | ||
// | ||
// TODO(justinfagnani): We should be able to use the previousNode here | ||
// as the marker node and reduce the number of extra nodes we add to a | ||
// template. See https://github.com/PolymerLabs/lit-html/issues/147 | ||
const previousSibling = node.previousSibling; | ||
if (previousSibling === null || previousSibling !== previousNode || | ||
previousSibling.nodeType !== Node.TEXT_NODE) { | ||
parent.insertBefore(document.createComment(''), node); | ||
} else { | ||
index--; | ||
} | ||
this.parts.push(new TemplatePart('node', index++)); | ||
nodesToRemove.push(node); | ||
// If we don't have a nextSibling add a marker node. | ||
// We don't have to check if the next node is going to be removed, | ||
// because that node will induce a new marker if so. | ||
if (node.nextSibling === null) { | ||
parent.insertBefore(document.createComment(''), node); | ||
} else { | ||
index--; | ||
} | ||
currentNode = previousNode; | ||
partIndex++; | ||
} | ||
export class LitTemplateProcessor extends TemplateProcessor { | ||
handleAttributeExpressions(element: Element, name: string, strings: string[]): | ||
Part[] { | ||
const prefix = name[0]; | ||
if (prefix === '.') { | ||
const comitter = new PropertyCommitter(element, name.slice(1), strings); | ||
return comitter.parts; | ||
} | ||
// Remove text binding nodes after the walk to not disturb the TreeWalker | ||
for (const n of nodesToRemove) { | ||
n.parentNode!.removeChild(n); | ||
if (prefix === '@') { | ||
return [new EventPart(element, name.slice(1))]; | ||
} | ||
if (prefix === '?') { | ||
return [new BooleanAttributePart(element, name.slice(1), strings)]; | ||
} | ||
return super.handleAttributeExpressions(element, name, strings); | ||
} | ||
} | ||
export const templateProcessor = new LitTemplateProcessor(); | ||
/** | ||
* Returns a value ready to be inserted into a Part from a user-provided value. | ||
* Implements a boolean attribute, roughly as defined in the HTML | ||
* specification. | ||
* | ||
* If the user value is a directive, this invokes the directive with the given | ||
* part. If the value is null, it's converted to undefined to work better | ||
* with certain DOM APIs, like textContent. | ||
* If the value is truthy, then the attribute is present with a value of | ||
* ''. If the value is falsey, the attribute is removed. | ||
*/ | ||
export const getValue = (part: Part, value: any) => { | ||
// `null` as the value of a Text node will render the string 'null' | ||
// so we convert it to undefined | ||
if (isDirective(value)) { | ||
value = value(part); | ||
return noChange; | ||
} | ||
return value === null ? undefined : value; | ||
}; | ||
export interface DirectiveFn<P= Part> { | ||
(part: P): void; | ||
__litDirective?: true; | ||
} | ||
export const directive = <P= Part>(f: DirectiveFn<P>): DirectiveFn<P> => { | ||
f.__litDirective = true; | ||
return f; | ||
}; | ||
const isDirective = (o: any) => | ||
typeof o === 'function' && o.__litDirective === true; | ||
/** | ||
* A sentinel value that signals that a value was handled by a directive and | ||
* should not be written to the DOM. | ||
*/ | ||
export const noChange = {}; | ||
/** | ||
* @deprecated Use `noChange` instead. | ||
*/ | ||
export { noChange as directiveValue }; | ||
const isPrimitiveValue = (value: any) => value === null || | ||
!(typeof value === 'object' || typeof value === 'function'); | ||
export interface Part { | ||
instance: TemplateInstance; | ||
size?: number; | ||
} | ||
export interface SinglePart extends Part { setValue(value: any): void; } | ||
export interface MultiPart extends Part { | ||
setValue(values: any[], startIndex: number): void; | ||
} | ||
export class AttributePart implements MultiPart { | ||
instance: TemplateInstance; | ||
export class BooleanAttributePart implements Part { | ||
element: Element; | ||
name: string; | ||
strings: string[]; | ||
size: number; | ||
_previousValues: any; | ||
_value: any = undefined; | ||
private _dirty = true; | ||
constructor( | ||
instance: TemplateInstance, element: Element, name: string, | ||
strings: string[]) { | ||
this.instance = instance; | ||
constructor(element: Element, name: string, strings: string[]) { | ||
if (strings.length !== 2 || strings[0] !== '' || strings[1] !== '') { | ||
throw new Error( | ||
'boolean attributes can only contain a single expression'); | ||
} | ||
this.element = element; | ||
this.name = name; | ||
this.strings = strings; | ||
this.size = strings.length - 1; | ||
this._previousValues = []; | ||
} | ||
protected _interpolate(values: any[], startIndex: number) { | ||
const strings = this.strings; | ||
const l = strings.length - 1; | ||
let text = ''; | ||
for (let i = 0; i < l; i++) { | ||
text += strings[i]; | ||
const v = getValue(this, values[startIndex + i]); | ||
if (v && v !== noChange && | ||
(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; | ||
} | ||
setValue(value: any): void { | ||
value = getValue(this, value); | ||
if (value === noChange) { | ||
return; | ||
} | ||
return text + strings[l]; | ||
} | ||
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; | ||
} | ||
this._value = !!value; | ||
if (this._value !== !!value) { | ||
this._dirty = true; | ||
} | ||
return true; | ||
} | ||
setValue(values: any[], startIndex: number): void { | ||
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(''); | ||
commit() { | ||
if (this._dirty) { | ||
this._dirty = false; | ||
if (this._value) { | ||
this.element.setAttribute(this.name, ''); | ||
} else { | ||
this.element.removeAttribute(this.name); | ||
} | ||
} else { | ||
value = this._interpolate(values, startIndex); | ||
} | ||
if (value !== noChange) { | ||
this.element.setAttribute(this.name, value); | ||
} | ||
this._previousValues = values; | ||
} | ||
} | ||
export class NodePart implements SinglePart { | ||
instance: TemplateInstance; | ||
startNode: Node; | ||
endNode: Node; | ||
_previousValue: any; | ||
export class PropertyCommitter extends AttributeCommitter { | ||
single: boolean; | ||
constructor(instance: TemplateInstance, startNode: Node, endNode: Node) { | ||
this.instance = instance; | ||
this.startNode = startNode; | ||
this.endNode = endNode; | ||
this._previousValue = undefined; | ||
constructor(element: Element, name: string, strings: string[]) { | ||
super(element, name, strings); | ||
this.single = | ||
(strings.length === 2 && strings[0] === '' && strings[1] === ''); | ||
} | ||
setValue(value: any): void { | ||
value = getValue(this, value); | ||
if (value === noChange) { | ||
return; | ||
_getValue() { | ||
if (this.single) { | ||
return this.parts[0]._value; | ||
} | ||
if (isPrimitiveValue(value)) { | ||
// 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); | ||
} | ||
return super._getValue(); | ||
} | ||
private _insert(node: Node) { | ||
this.endNode.parentNode!.insertBefore(node, this.endNode); | ||
} | ||
private _setNode(value: Node): void { | ||
if (this._previousValue === value) { | ||
return; | ||
commit(): void { | ||
if (this.dirty) { | ||
this.dirty = false; | ||
(this.element as any)[this.name] = this._getValue(); | ||
} | ||
this.clear(); | ||
this._insert(value); | ||
this._previousValue = value; | ||
} | ||
} | ||
private _setText(value: string): void { | ||
const node = this.startNode.nextSibling!; | ||
value = value === undefined ? '' : value; | ||
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(document.createTextNode(value)); | ||
} | ||
this._previousValue = value; | ||
} | ||
export class EventPart implements Part { | ||
element: Element; | ||
eventName: string; | ||
_value: any = undefined; | ||
private _dirty = true; | ||
private _setTemplateResult(value: TemplateResult): void { | ||
const template = this.instance._getTemplate(value); | ||
let instance: TemplateInstance; | ||
if (this._previousValue && this._previousValue.template === template) { | ||
instance = this._previousValue; | ||
} else { | ||
instance = new TemplateInstance( | ||
template, this.instance._partCallback, this.instance._getTemplate); | ||
this._setNode(instance._clone()); | ||
this._previousValue = instance; | ||
} | ||
instance.update(value.values); | ||
constructor(element: Element, eventName: string) { | ||
this.element = element; | ||
this.eventName = eventName; | ||
} | ||
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 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 us keep track of how many items we stamped so we can clear leftover | ||
// items from a previous render | ||
const itemParts = this._previousValue as any[]; | ||
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 = document.createTextNode(''); | ||
this._insert(itemStart); | ||
} | ||
itemPart = new NodePart(this.instance, itemStart, this.endNode); | ||
itemParts.push(itemPart); | ||
setValue(value: any): void { | ||
value = getValue(this, value); | ||
if (value !== noChange) { | ||
if ((value == null) !== (this._value == null)) { | ||
this._dirty = true; | ||
} | ||
itemPart.setValue(item); | ||
partIndex++; | ||
this._value = value; | ||
} | ||
if (partIndex === 0) { | ||
this.clear(); | ||
this._previousValue = undefined; | ||
} else if (partIndex < itemParts.length) { | ||
const lastPart = itemParts[partIndex - 1]; | ||
// Truncate the parts array so _previousValue reflects the current state | ||
itemParts.length = partIndex; | ||
this.clear(lastPart.endNode.previousSibling!); | ||
lastPart.endNode = this.endNode; | ||
} | ||
} | ||
private _setPromise(value: Promise<any>): void { | ||
this._previousValue = value; | ||
value.then((v: any) => { | ||
if (this._previousValue === value) { | ||
this.setValue(v); | ||
} | ||
}); | ||
} | ||
clear(startNode: Node = this.startNode) { | ||
removeNodes( | ||
this.startNode.parentNode!, startNode.nextSibling!, this.endNode); | ||
} | ||
} | ||
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 { | ||
_parts: Array<Part|undefined> = []; | ||
_partCallback: PartCallback; | ||
_getTemplate: TemplateFactory; | ||
template: Template; | ||
constructor( | ||
template: Template, partCallback: PartCallback, | ||
getTemplate: TemplateFactory) { | ||
this.template = template; | ||
this._partCallback = partCallback; | ||
this._getTemplate = getTemplate; | ||
} | ||
update(values: any[]) { | ||
let valueIndex = 0; | ||
for (const part of this._parts) { | ||
if (!part) { | ||
valueIndex++; | ||
} else if (part.size === undefined) { | ||
(part as SinglePart).setValue(values[valueIndex]); | ||
valueIndex++; | ||
commit() { | ||
if (this._dirty) { | ||
this._dirty = false; | ||
if (this._value == null) { | ||
this.element.removeEventListener(this.eventName, this); | ||
} else { | ||
(part as MultiPart).setValue(values, valueIndex); | ||
valueIndex += part.size; | ||
this.element.addEventListener(this.eventName, this); | ||
} | ||
@@ -743,70 +183,9 @@ } | ||
_clone(): DocumentFragment { | ||
// Clone the node, rather than importing it, to keep the fragment in the | ||
// template's document. This leaves the fragment inert so custom elements | ||
// won't upgrade until after the main document adopts the node. | ||
const fragment = this.template.element.content.cloneNode(true) as DocumentFragment; | ||
const parts = this.template.parts; | ||
if (parts.length > 0) { | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be | ||
// null | ||
const walker = document.createTreeWalker( | ||
fragment, | ||
133 /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | | ||
NodeFilter.SHOW_TEXT */ | ||
, | ||
null as any, | ||
false); | ||
let index = -1; | ||
for (let i = 0; i < parts.length; i++) { | ||
const part = parts[i]; | ||
const partActive = isTemplatePartActive(part); | ||
// An inactive part has no coresponding Template node. | ||
if (partActive) { | ||
while (index < part.index) { | ||
index++; | ||
walker.nextNode(); | ||
} | ||
} | ||
this._parts.push(partActive ? this._partCallback(this, part, walker.currentNode) : undefined); | ||
} | ||
handleEvent(event: Event) { | ||
if (typeof this._value === 'function') { | ||
this._value.call(this.element, event); | ||
} else if (typeof this._value.handleEvent === 'function') { | ||
this._value.handleEvent(event); | ||
} | ||
return fragment; | ||
} | ||
} | ||
/** | ||
* Reparents nodes, starting from `startNode` (inclusive) to `endNode` | ||
* (exclusive), into another container (could be the same container), before | ||
* `beforeNode`. If `beforeNode` is null, it appends the nodes to the | ||
* container. | ||
*/ | ||
export const reparentNodes = | ||
(container: Node, | ||
start: Node | null, | ||
end: Node | null = null, | ||
before: Node | null = null): void => { | ||
let node = start; | ||
while (node !== end) { | ||
const n = node!.nextSibling; | ||
container.insertBefore(node!, before as Node); | ||
node = n; | ||
} | ||
}; | ||
/** | ||
* Removes nodes, starting from `startNode` (inclusive) to `endNode` | ||
* (exclusive), from `container`. | ||
*/ | ||
export const removeNodes = | ||
(container: Node, startNode: Node | null, endNode: Node | null = null): | ||
void => { | ||
let node = startNode; | ||
while (node !== endNode) { | ||
const n = node!.nextSibling; | ||
container.removeChild(node!); | ||
node = n; | ||
} | ||
}; |
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
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a 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
247087
47
3603
52
2