Comparing version 0.6.0 to 0.7.0
@@ -13,4 +13,12 @@ # Change Log | ||
## Unreleased | ||
<!-- ## Unreleased --> | ||
## [0.7.0] - 2017-10-06 | ||
* Added the `svg` template tag for creating partial SVG content | ||
* Support for expressions inside tables and other elements with limited permitted content | ||
* Only remove whitespace between elements, or at the start or end of elements | ||
* Fixed bugs with rendering iterables | ||
* A few IE/Edge fixes. Closer to full support. | ||
## [0.6.0] - 2017-09-01 | ||
@@ -17,0 +25,0 @@ |
155
lit-html.js
@@ -14,2 +14,10 @@ /** | ||
*/ | ||
/** | ||
* TypeScript has a problem with precompiling templates literals | ||
* https://github.com/Microsoft/TypeScript/issues/17956 | ||
* | ||
* TODO(justinfagnani): Run tests compiled to ES5 with both Babel and | ||
* TypeScript to verify correctness. | ||
*/ | ||
const envCachesTemplates = ((t) => t() === t())(() => ((s) => s) ``); | ||
// The first argument to JS template tags retain identity across multiple | ||
@@ -19,2 +27,3 @@ // calls to a tag for the same literal, so we can cache work done per literal | ||
const templates = new Map(); | ||
const svgTemplates = new Map(); | ||
/** | ||
@@ -24,7 +33,16 @@ * Interprets a template literal as an HTML template that can efficiently | ||
*/ | ||
export function html(strings, ...values) { | ||
let template = templates.get(strings); | ||
export const html = (strings, ...values) => litTag(strings, values, templates, false); | ||
/** | ||
* Interprets a template literal as an SVG template that can efficiently | ||
* render to and update a container. | ||
*/ | ||
export const svg = (strings, ...values) => litTag(strings, values, svgTemplates, true); | ||
function litTag(strings, values, templates, isSvg) { | ||
const key = envCachesTemplates ? | ||
strings : | ||
strings.join('{{--uniqueness-workaround--}}'); | ||
let template = templates.get(key); | ||
if (template === undefined) { | ||
template = new Template(strings); | ||
templates.set(strings, template); | ||
template = new Template(strings, isSvg); | ||
templates.set(key, template); | ||
} | ||
@@ -72,4 +90,16 @@ return new TemplateResult(template, values); | ||
*/ | ||
const exprMarker = `{{lit-${Math.random()}}}`; | ||
const attributeMarker = `{{lit-${Math.random()}}}`; | ||
/** | ||
* Regex to scan the string preceding an expression to see if we're in a text | ||
* context, and not an attribute context. | ||
* | ||
* This works by seeing if we have a `>` not followed by a `<`. If there is a | ||
* `<` closer to the end of the strings, then we're inside a tag. | ||
*/ | ||
const textRegex = />[^<]*$/; | ||
const hasTagsRegex = /[^<]*/; | ||
const textMarkerContent = '_-lit-html-_'; | ||
const textMarker = `<!--${textMarkerContent}-->`; | ||
const attrOrTextRegex = new RegExp(`${attributeMarker}|${textMarker}`); | ||
/** | ||
* A placeholder for a dynamic expression in an HTML template. | ||
@@ -100,20 +130,30 @@ * | ||
export class Template { | ||
constructor(strings) { | ||
constructor(strings, svg = false) { | ||
this.parts = []; | ||
this.svg = svg; | ||
this.element = document.createElement('template'); | ||
this.element.innerHTML = strings.join(exprMarker); | ||
const walker = document.createTreeWalker(this.element.content, 5 /* elements & text */); | ||
this.element.innerHTML = this._getHtml(strings, svg); | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null | ||
const walker = document.createTreeWalker(this.element.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++; | ||
const node = walker.currentNode; | ||
if (node.nodeType === 1 /* ELEMENT_NODE */) { | ||
if (!node.hasAttributes()) | ||
previousNode = currentNode; | ||
const node = currentNode = walker.currentNode; | ||
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { | ||
if (!node.hasAttributes()) { | ||
continue; | ||
} | ||
const attributes = node.attributes; | ||
for (let i = 0; i < attributes.length; i++) { | ||
const attribute = attributes.item(i); | ||
const attributeStrings = attribute.value.split(exprMarker); | ||
const attributeStrings = attribute.value.split(attrOrTextRegex); | ||
if (attributeStrings.length > 1) { | ||
@@ -134,4 +174,5 @@ // Get the template literal section leading up to the first | ||
} | ||
else if (node.nodeType === 3 /* TEXT_NODE */) { | ||
const strings = node.nodeValue.split(exprMarker); | ||
else if (node.nodeType === 3 /* Node.TEXT_NODE */) { | ||
const nodeValue = node.nodeValue; | ||
const strings = nodeValue.split(attributeMarker); | ||
if (strings.length > 1) { | ||
@@ -149,10 +190,48 @@ const parent = node.parentNode; | ||
for (let i = 0; i < lastIndex; i++) { | ||
parent.insertBefore(new Text(strings[i]), node); | ||
parent.insertBefore(document.createTextNode(strings[i]), node); | ||
this.parts.push(new TemplatePart('node', index++)); | ||
} | ||
} | ||
else if (!node.nodeValue.trim()) { | ||
nodesToRemove.push(node); | ||
else { | ||
// Strip whitespace-only nodes, only between elements, or at the | ||
// beginning or end of elements. | ||
const previousSibling = node.previousSibling; | ||
const nextSibling = node.nextSibling; | ||
if ((previousSibling === null || | ||
previousSibling.nodeType === 1 /* Node.ELEMENT_NODE */) && | ||
(nextSibling === null || | ||
nextSibling.nodeType === 1 /* Node.ELEMENT_NODE */) && | ||
nodeValue.trim() === '') { | ||
nodesToRemove.push(node); | ||
currentNode = previousNode; | ||
index--; | ||
} | ||
} | ||
} | ||
else if (node.nodeType === 8 /* Node.COMMENT_NODE */ && | ||
node.nodeValue === textMarkerContent) { | ||
const parent = node.parentNode; | ||
// If we don't have a previous node add a marker node. | ||
// If the previousSibling is removed, because it's another part | ||
// placholder, or empty text, add a marker node. | ||
if (node.previousSibling === null || | ||
node.previousSibling !== previousNode) { | ||
parent.insertBefore(new Text(), node); | ||
} | ||
else { | ||
index--; | ||
} | ||
this.parts.push(new TemplatePart('node', index++)); | ||
nodesToRemove.push(node); | ||
// If we don't have a next node 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 marker if so. | ||
if (node.nextSibling === null) { | ||
parent.insertBefore(new Text(), node); | ||
} | ||
else { | ||
index--; | ||
} | ||
currentNode = previousNode; | ||
partIndex++; | ||
} | ||
@@ -165,2 +244,23 @@ } | ||
} | ||
/** | ||
* Returns a string of HTML used to create a <template> element. | ||
*/ | ||
_getHtml(strings, svg) { | ||
const l = strings.length; | ||
const a = []; | ||
let isTextBinding = false; | ||
for (let i = 0; i < l - 1; i++) { | ||
const s = strings[i]; | ||
a.push(s); | ||
// We're in a text position if the previous string matches the | ||
// textRegex. If it doesn't and the previous string has no tags, then | ||
// we use the previous text position state. | ||
isTextBinding = s.match(textRegex) !== null || | ||
(s.match(hasTagsRegex) !== null && isTextBinding); | ||
a.push(isTextBinding ? textMarker : attributeMarker); | ||
} | ||
a.push(strings[l - 1]); | ||
const html = a.join(''); | ||
return svg ? `<svg>${html}</svg>` : html; | ||
} | ||
} | ||
@@ -214,2 +314,3 @@ export const getValue = (part, value) => { | ||
this.endNode = endNode; | ||
this._previousValue = undefined; | ||
} | ||
@@ -263,3 +364,3 @@ setValue(value) { | ||
else { | ||
this._setNode(new Text(value)); | ||
this._setNode(document.createTextNode(value === undefined ? '' : value)); | ||
} | ||
@@ -296,3 +397,3 @@ this._previousValue = value; | ||
} | ||
// Lets of keep track of how many items we stamped so we can clear leftover | ||
// Lets us keep track of how many items we stamped so we can clear leftover | ||
// items from a previous render | ||
@@ -313,3 +414,3 @@ const itemParts = this._previousValue; | ||
const previousPart = itemParts[partIndex - 1]; | ||
itemStart = previousPart.endNode = new Text(); | ||
itemStart = previousPart.endNode = document.createTextNode(''); | ||
this._insert(itemStart); | ||
@@ -329,2 +430,4 @@ } | ||
const lastPart = itemParts[partIndex - 1]; | ||
// Truncate the parts array so _previousValue reflects the current state | ||
itemParts.length = partIndex; | ||
this.clear(lastPart.endNode.previousSibling); | ||
@@ -384,3 +487,5 @@ lastPart.endNode = this.endNode; | ||
if (this.template.parts.length > 0) { | ||
const walker = document.createTreeWalker(fragment, 5 /* elements & text */); | ||
// 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); | ||
const parts = this.template.parts; | ||
@@ -402,2 +507,10 @@ let index = 0; | ||
} | ||
if (this.template.svg) { | ||
const svgElement = fragment.firstChild; | ||
fragment.removeChild(svgElement); | ||
const nodes = svgElement.childNodes; | ||
for (let i = 0; i < nodes.length; i++) { | ||
fragment.appendChild(nodes.item(i)); | ||
} | ||
} | ||
return fragment; | ||
@@ -404,0 +517,0 @@ } |
{ | ||
"name": "lit-html", | ||
"version": "0.6.0", | ||
"version": "0.7.0", | ||
"description": "HTML template literals in JavaScript", | ||
@@ -15,5 +15,4 @@ "license": "BSD-3-Clause", | ||
"gen-docs": "typedoc --readme none --mode modules --excludeNotExported --excludePrivate --exclude **/*_test.ts --out ./docs/api .", | ||
"pretest": "npm run posttest; ln -s node_modules bower_components", | ||
"test": "npm run build && wct -l chrome && npm run lint", | ||
"posttest": "rm -f bower_components", | ||
"test": "npm run build && wct -l chrome --npm && npm run lint", | ||
"quicktest": "wct -l chrome -p --npm", | ||
"checksize": "uglifyjs lit-html.js -mc --toplevel | gzip -9 | wc -c", | ||
@@ -33,3 +32,4 @@ "format": "find src test | grep '\\.js$\\|\\.ts$' | xargs clang-format --style=file -i", | ||
"uglify-es": "^3.0.27", | ||
"web-component-tester": "^6.0.1" | ||
"wct-browser-legacy": "^0.0.1-pre.10", | ||
"web-component-tester": "^6.3.0" | ||
}, | ||
@@ -36,0 +36,0 @@ "typings": "lit-html.d.ts", |
@@ -10,42 +10,32 @@ # lit-html | ||
`lit-html` provides two main exports: | ||
```javascript | ||
import {html, render} from 'lit-html'; | ||
* `html`: A JavaScript [template tag](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_template_literals) used to produce a `TemplateResult`, which is a container for a template, and the values that should populate the template. | ||
* `render()`: A function that renders a `TemplateResult` to a DOM container, such as an element or shadow root. | ||
// This is a lit-html template function. It returns a lit-html template. | ||
const helloTemplate = (name) => html`<div>Hello ${name}!</div>`; | ||
### Examples | ||
// Call the function with some data, and pass the result to render() | ||
`lit-html` can be used standalone and directly to help manage DOM: | ||
```javascript | ||
const helloTemplate = (name) => html`<div>Hello ${name}!</div>`; | ||
// renders <div>Hello Steve!</div> to the document body | ||
// This renders <div>Hello Steve!</div> to the document body | ||
render(helloTemplate('Steve'), document.body); | ||
// updates to <div>Hello Kevin!</div>, but only updates the ${name} part | ||
// This updates to <div>Hello Kevin!</div>, but only updates the ${name} part | ||
render(helloTemplate('Kevin'), document.body); | ||
``` | ||
But it may also be common to use `lit-html` with a component system that calls `render()` for you, similar to React components: | ||
`lit-html` provides two main exports: | ||
_(this example uses JS Class Fields, an upcoming specification)_ | ||
```javascript | ||
class MyElement extends CoolLitMixin(HTMLElement) { | ||
* `html`: A JavaScript [template tag](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_template_literals) used to produce a `TemplateResult`, which is a container for a template, and the values that should populate the template. | ||
* `render()`: A function that renders a `TemplateResult` to a DOM container, such as an element or shadow root. | ||
static observedProperties = ['title', 'body']; | ||
### Announcement at Polymer Summit 2017 | ||
title = `About lit-html`; | ||
body = `It's got potential.`; | ||
<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> | ||
// Called by the base-class when properties change, result is passed | ||
// to lit-html's render() function. | ||
render() { | ||
return html` | ||
<h1>${this.title}</h1> | ||
<p>${this.body}</p> | ||
`; | ||
} | ||
} | ||
``` | ||
## Motivation | ||
@@ -70,3 +60,3 @@ | ||
`My name is ${name}.` | ||
``` | ||
``` | ||
@@ -90,3 +80,3 @@ A _tagged_ template literal is prefixed with a special template tag function: | ||
The first time `html` is called on a particular template literal it does one-time setup work to create the template. It joins all the string parts with a special placeholder, `"{{}}"`, then creates a `<template>` and sets its `innerHTML` to the result. The it walks the template's DOM and extracts the placeholder and remembers their location. | ||
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. | ||
@@ -129,2 +119,15 @@ Every call to `html` returns a `TemplateResult` which contains the template created on the first call, and the expression values for the current call. | ||
### SVG Support | ||
To create partial SVG templates - template that will rendering inside and `<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"/>`)} | ||
${[0, 10, 20].map((y) => svg`<line x1="0" y1=${y} x2="0" y2=${y}/>`)} | ||
</g> | ||
`; | ||
``` | ||
### Safety | ||
@@ -149,3 +152,3 @@ | ||
const items = [1, 2, 3]; | ||
const render = () => html`items = ${items.map((i) => `item: ${i})}`; | ||
const render = () => html`items = ${items.map((i) => `item: ${i}`)}`; | ||
``` | ||
@@ -273,3 +276,3 @@ | ||
* 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 Canary (coming in 61), and Firefox 54 (behind a flag). | ||
* 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 54 (behind a flag). | ||
* It has a growing test suite, but it has only been run manually on Chrome Canary, Safari 10.1 and Firefox 54. | ||
@@ -276,0 +279,0 @@ * Much more test coverage is needed for complex templates, especially template composition and Function and Iterable values. |
@@ -60,3 +60,2 @@ /** | ||
let index = 0; | ||
let oldPartsIndex = 0; | ||
let currentMarker: Node|undefined; | ||
@@ -77,4 +76,3 @@ | ||
// if there's no keyMap | ||
let itemPart = | ||
keyMap === undefined ? oldParts[oldPartsIndex++] : keyMap.get(key); | ||
let itemPart = keyMap === undefined ? oldParts[0] : keyMap.get(key); | ||
@@ -81,0 +79,0 @@ if (itemPart === undefined) { |
@@ -15,6 +15,17 @@ /** | ||
/** | ||
* TypeScript has a problem with precompiling templates literals | ||
* https://github.com/Microsoft/TypeScript/issues/17956 | ||
* | ||
* TODO(justinfagnani): Run tests compiled to ES5 with both Babel and | ||
* TypeScript to verify correctness. | ||
*/ | ||
const envCachesTemplates = | ||
((t: any) => t() === t())(() => ((s: TemplateStringsArray) => s) ``); | ||
// The first argument to JS template tags retain identity across multiple | ||
// calls to a tag for the same literal, so we can cache work done per literal | ||
// in a Map. | ||
const templates = new Map<TemplateStringsArray, Template>(); | ||
const templates = new Map<TemplateStringsArray|string, Template>(); | ||
const svgTemplates = new Map<TemplateStringsArray|string, Template>(); | ||
@@ -25,8 +36,24 @@ /** | ||
*/ | ||
export function html( | ||
strings: TemplateStringsArray, ...values: any[]): TemplateResult { | ||
let template = templates.get(strings); | ||
export const html = (strings: TemplateStringsArray, ...values: any[]) => | ||
litTag(strings, values, templates, false); | ||
/** | ||
* Interprets a template literal as an SVG template that can efficiently | ||
* render to and update a container. | ||
*/ | ||
export const svg = (strings: TemplateStringsArray, ...values: any[]) => | ||
litTag(strings, values, svgTemplates, true); | ||
function litTag( | ||
strings: TemplateStringsArray, | ||
values: any[], | ||
templates: Map<TemplateStringsArray|string, Template>, | ||
isSvg: boolean): TemplateResult { | ||
const key = envCachesTemplates ? | ||
strings : | ||
strings.join('{{--uniqueness-workaround--}}'); | ||
let template = templates.get(key); | ||
if (template === undefined) { | ||
template = new Template(strings); | ||
templates.set(strings, template); | ||
template = new Template(strings, isSvg); | ||
templates.set(key, template); | ||
} | ||
@@ -87,5 +114,18 @@ return new TemplateResult(template, values); | ||
*/ | ||
const exprMarker = `{{lit-${Math.random()}}}`; | ||
const attributeMarker = `{{lit-${Math.random()}}}`; | ||
/** | ||
* Regex to scan the string preceding an expression to see if we're in a text | ||
* context, and not an attribute context. | ||
* | ||
* This works by seeing if we have a `>` not followed by a `<`. If there is a | ||
* `<` closer to the end of the strings, then we're inside a tag. | ||
*/ | ||
const textRegex = />[^<]*$/; | ||
const hasTagsRegex = /[^<]*/; | ||
const textMarkerContent = '_-lit-html-_'; | ||
const textMarker = `<!--${textMarkerContent}-->`; | ||
const attrOrTextRegex = new RegExp(`${attributeMarker}|${textMarker}`); | ||
/** | ||
* A placeholder for a dynamic expression in an HTML template. | ||
@@ -113,25 +153,42 @@ * | ||
export class Template { | ||
parts: TemplatePart[] = []; | ||
element: HTMLTemplateElement; | ||
svg: boolean; | ||
constructor(strings: TemplateStringsArray) { | ||
constructor(strings: TemplateStringsArray, svg: boolean = false) { | ||
this.svg = svg; | ||
this.element = document.createElement('template'); | ||
this.element.innerHTML = strings.join(exprMarker); | ||
this.element.innerHTML = this._getHtml(strings, svg); | ||
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null | ||
const walker = document.createTreeWalker( | ||
this.element.content, 5 /* elements & text */); | ||
this.element.content, | ||
133 /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | | ||
NodeFilter.SHOW_TEXT */ | ||
, | ||
null as any, | ||
false); | ||
let index = -1; | ||
let partIndex = 0; | ||
const nodesToRemove = []; | ||
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++; | ||
const node = walker.currentNode as Element; | ||
if (node.nodeType === 1 /* ELEMENT_NODE */) { | ||
if (!node.hasAttributes()) | ||
previousNode = currentNode; | ||
const node = currentNode = walker.currentNode as Element; | ||
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { | ||
if (!node.hasAttributes()) { | ||
continue; | ||
} | ||
const attributes = node.attributes; | ||
for (let i = 0; i < attributes.length; i++) { | ||
const attribute = attributes.item(i); | ||
const attributeStrings = attribute.value.split(exprMarker); | ||
const attributeStrings = attribute.value.split(attrOrTextRegex); | ||
if (attributeStrings.length > 1) { | ||
@@ -153,4 +210,5 @@ // Get the template literal section leading up to the first | ||
} | ||
} else if (node.nodeType === 3 /* TEXT_NODE */) { | ||
const strings = node.nodeValue!.split(exprMarker); | ||
} else if (node.nodeType === 3 /* Node.TEXT_NODE */) { | ||
const nodeValue = node.nodeValue!; | ||
const strings = nodeValue.split(attributeMarker); | ||
if (strings.length > 1) { | ||
@@ -171,9 +229,45 @@ const parent = node.parentNode!; | ||
for (let i = 0; i < lastIndex; i++) { | ||
parent.insertBefore(new Text(strings[i]), node); | ||
parent.insertBefore(document.createTextNode(strings[i]), node); | ||
this.parts.push(new TemplatePart('node', index++)); | ||
} | ||
} else if (!node.nodeValue!.trim()) { | ||
nodesToRemove.push(node); | ||
} else { | ||
// Strip whitespace-only nodes, only between elements, or at the | ||
// beginning or end of elements. | ||
const previousSibling = node.previousSibling; | ||
const nextSibling = node.nextSibling; | ||
if ((previousSibling === null || | ||
previousSibling.nodeType === 1 /* Node.ELEMENT_NODE */) && | ||
(nextSibling === null || | ||
nextSibling.nodeType === 1 /* Node.ELEMENT_NODE */) && | ||
nodeValue.trim() === '') { | ||
nodesToRemove.push(node); | ||
currentNode = previousNode; | ||
index--; | ||
} | ||
} | ||
} else if ( | ||
node.nodeType === 8 /* Node.COMMENT_NODE */ && | ||
node.nodeValue === textMarkerContent) { | ||
const parent = node.parentNode!; | ||
// If we don't have a previous node add a marker node. | ||
// If the previousSibling is removed, because it's another part | ||
// placholder, or empty text, add a marker node. | ||
if (node.previousSibling === null || | ||
node.previousSibling !== previousNode) { | ||
parent.insertBefore(new Text(), node); | ||
} else { | ||
index--; | ||
} | ||
this.parts.push(new TemplatePart('node', index++)); | ||
nodesToRemove.push(node); | ||
// If we don't have a next node 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 marker if so. | ||
if (node.nextSibling === null) { | ||
parent.insertBefore(new Text(), node); | ||
} else { | ||
index--; | ||
} | ||
currentNode = previousNode; | ||
partIndex++; | ||
} | ||
@@ -187,2 +281,24 @@ } | ||
} | ||
/** | ||
* Returns a string of HTML used to create a <template> element. | ||
*/ | ||
private _getHtml(strings: TemplateStringsArray, svg?: boolean): string { | ||
const l = strings.length; | ||
const a = []; | ||
let isTextBinding = false; | ||
for (let i = 0; i < l - 1; i++) { | ||
const s = strings[i]; | ||
a.push(s); | ||
// We're in a text position if the previous string matches the | ||
// textRegex. If it doesn't and the previous string has no tags, then | ||
// we use the previous text position state. | ||
isTextBinding = s.match(textRegex) !== null || | ||
(s.match(hasTagsRegex) !== null && isTextBinding); | ||
a.push(isTextBinding ? textMarker : attributeMarker); | ||
} | ||
a.push(strings[l - 1]); | ||
const html = a.join(''); | ||
return svg ? `<svg>${html}</svg>` : html; | ||
} | ||
} | ||
@@ -271,2 +387,3 @@ | ||
this.endNode = endNode; | ||
this._previousValue = undefined; | ||
} | ||
@@ -319,3 +436,3 @@ | ||
} else { | ||
this._setNode(new Text(value)); | ||
this._setNode(document.createTextNode(value === undefined ? '' : value)); | ||
} | ||
@@ -355,5 +472,5 @@ this._previousValue = value; | ||
// Lets of keep track of how many items we stamped so we can clear leftover | ||
// Lets us keep track of how many items we stamped so we can clear leftover | ||
// items from a previous render | ||
const itemParts = this._previousValue; | ||
const itemParts = this._previousValue as any[]; | ||
let partIndex = 0; | ||
@@ -375,3 +492,3 @@ | ||
const previousPart = itemParts[partIndex - 1]; | ||
itemStart = previousPart.endNode = new Text(); | ||
itemStart = previousPart.endNode = document.createTextNode(''); | ||
this._insert(itemStart); | ||
@@ -391,2 +508,4 @@ } | ||
const lastPart = itemParts[partIndex - 1]; | ||
// Truncate the parts array so _previousValue reflects the current state | ||
itemParts.length = partIndex; | ||
this.clear(lastPart.endNode.previousSibling!); | ||
@@ -464,4 +583,10 @@ lastPart.endNode = this.endNode; | ||
if (this.template.parts.length > 0) { | ||
const walker = | ||
document.createTreeWalker(fragment, 5 /* elements & text */); | ||
// 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); | ||
@@ -483,4 +608,12 @@ const parts = this.template.parts; | ||
} | ||
if (this.template.svg) { | ||
const svgElement = fragment.firstChild!; | ||
fragment.removeChild(svgElement); | ||
const nodes = svgElement.childNodes; | ||
for (let i = 0; i < nodes.length; i++) { | ||
fragment.appendChild(nodes.item(i)); | ||
} | ||
} | ||
return fragment; | ||
} | ||
} |
@@ -160,3 +160,3 @@ /** | ||
test('renderes an list', () => { | ||
test('renders a list', () => { | ||
const r = html`${repeat([1, 2, 3], (i: number) => html` | ||
@@ -197,4 +197,14 @@ <li>item: ${i}</li>`)}`; | ||
test('re-renders a list', () => { | ||
const items = [1, 2, 3, 4, 5]; | ||
const t = () => html`${repeat(items, (i: number) => html` | ||
<li>item: ${i}</li>`)}`; | ||
render(t(), container); | ||
render(t(), container); | ||
assert.equal( | ||
container.innerHTML, | ||
`<li>item: 1</li><li>item: 2</li><li>item: 3</li><li>item: 4</li><li>item: 5</li>`); | ||
}); | ||
}); | ||
}); |
@@ -18,3 +18,3 @@ /** | ||
import {AttributePart, defaultPartCallback, html, NodePart, Part, render, TemplateInstance, TemplatePart, TemplateResult} from '../lit-html.js'; | ||
import {AttributePart, defaultPartCallback, html, NodePart, Part, render, svg, TemplateInstance, TemplatePart, TemplateResult} from '../lit-html.js'; | ||
@@ -65,3 +65,2 @@ const assert = chai.assert; | ||
render(result, container); | ||
console.log(container.innerHTML); | ||
assert.equal(container.innerHTML, '{{}}'); | ||
@@ -139,4 +138,14 @@ }); | ||
let container: HTMLElement; | ||
setup(() => { | ||
container = document.createElement('div'); | ||
}); | ||
test('removes whitespace-only nodes', () => { | ||
render(html`<div> </div>`, container); | ||
assert.equal(container.innerHTML, '<div></div>'); | ||
}); | ||
test('renders a string', () => { | ||
const container = document.createElement('div'); | ||
render(html`<div>${'foo'}</div>`, container); | ||
@@ -147,3 +156,2 @@ assert.equal(container.innerHTML, '<div>foo</div>'); | ||
test('renders a number', () => { | ||
const container = document.createElement('div'); | ||
render(html`<div>${123}</div>`, container); | ||
@@ -154,3 +162,2 @@ assert.equal(container.innerHTML, '<div>123</div>'); | ||
test('renders undefined', () => { | ||
const container = document.createElement('div'); | ||
render(html`<div>${undefined}</div>`, container); | ||
@@ -161,3 +168,2 @@ assert.equal(container.innerHTML, '<div></div>'); | ||
test('renders null', () => { | ||
const container = document.createElement('div'); | ||
render(html`<div>${null}</div>`, container); | ||
@@ -169,3 +175,2 @@ assert.equal(container.innerHTML, '<div></div>'); | ||
// This test just checks that we don't call the function | ||
const container = document.createElement('div'); | ||
render(html`<div>${(_: any) => 123}</div>`, container); | ||
@@ -176,3 +181,2 @@ assert.equal(container.innerHTML, '<div>(_) => 123</div>'); | ||
test('renders arrays', () => { | ||
const container = document.createElement('div'); | ||
render(html`<div>${[1, 2, 3]}</div>`, container); | ||
@@ -183,3 +187,2 @@ assert.equal(container.innerHTML, '<div>123</div>'); | ||
test('renders nested templates', () => { | ||
const container = document.createElement('div'); | ||
const partial = html`<h1>${'foo'}</h1>`; | ||
@@ -190,4 +193,52 @@ render(html`${partial}${'bar'}`, container); | ||
test('renders parts with whitespace after them', () => { | ||
render(html`<div>${'foo'} </div>`, container); | ||
assert.equal(container.innerHTML, '<div>foo </div>'); | ||
}); | ||
test('preserves whitespace between parts', () => { | ||
render(html`<div>${'foo'} ${'bar'}</div>`, container); | ||
assert.equal(container.innerHTML, '<div>foo bar</div>'); | ||
}); | ||
test('renders nested templates within table content', () => { | ||
let table = html`<table>${html`<tr>${html`<td></td>`}</tr>`}</table>`; | ||
render(table, container); | ||
assert.equal(container.innerHTML, '<table><tr><td></td></tr></table>'); | ||
table = html`<tbody>${html`<tr></tr>`}</tbody>`; | ||
render(table, container); | ||
assert.equal(container.innerHTML, '<tbody><tr></tr></tbody>'); | ||
table = html`<table><tr></tr>${html`<tr></tr>`}</table>`; | ||
render(table, container); | ||
assert.equal( | ||
container.innerHTML, | ||
'<table><tbody><tr></tr><tr></tr></tbody></table>'); | ||
table = html`<table><tr><td></td>${html`<td></td>`}</tr></table>`; | ||
render(table, container); | ||
assert.equal( | ||
container.innerHTML, | ||
'<table><tbody><tr><td></td><td></td></tr></tbody></table>'); | ||
table = html`<table><tr><td></td>${html`<td></td>`}${ | ||
html`<td></td>` | ||
}</tr></table>`; | ||
render(table, container); | ||
assert.equal( | ||
container.innerHTML, | ||
'<table><tbody><tr><td></td><td></td><td></td></tr></tbody></table>'); | ||
}); | ||
test( | ||
'renders quoted attributes with the text <table> before an expression', | ||
() => { | ||
const container = document.createElement('div'); | ||
const template = html`<div a="<table>${'foo'}"></div>`; | ||
render(template, container); | ||
assert.equal(container.innerHTML, '<div a="<table>foo"></div>'); | ||
}); | ||
test('values contain interpolated values', () => { | ||
const container = document.createElement('div'); | ||
const t = html`${'a'},${'b'},${'c'}`; | ||
@@ -199,3 +250,2 @@ render(t, container); | ||
// test('renders multiple nested templates', () => { | ||
// const container = document.createElement('div'); | ||
// const partial = html`<h1>${'foo'}</h1>`; | ||
@@ -208,3 +258,2 @@ // html`${partial}${'bar'}${partial}${'baz'}qux`, container); | ||
test('renders arrays of nested templates', () => { | ||
const container = document.createElement('div'); | ||
render(html`<div>${[1, 2, 3].map((i) => html`${i}`)}</div>`, container); | ||
@@ -215,3 +264,2 @@ assert.equal(container.innerHTML, '<div>123</div>'); | ||
test('renders an element', () => { | ||
const container = document.createElement('div'); | ||
const child = document.createElement('p'); | ||
@@ -223,3 +271,2 @@ render(html`<div>${child}</div>`, container); | ||
test('renders an array of elements', () => { | ||
const container = document.createElement('div'); | ||
const children = [ | ||
@@ -236,3 +283,2 @@ document.createElement('p'), | ||
test('renders to an attribute', () => { | ||
const container = document.createElement('div'); | ||
render(html`<div foo="${'bar'}"></div>`, container); | ||
@@ -243,3 +289,2 @@ assert.equal(container.innerHTML, '<div foo="bar"></div>'); | ||
test('renders to an attribute without quotes', () => { | ||
const container = document.createElement('div'); | ||
render(html`<div foo=${'bar'}></div>`, container); | ||
@@ -250,3 +295,2 @@ assert.equal(container.innerHTML, '<div foo="bar"></div>'); | ||
test('renders interpolation to an attribute', () => { | ||
const container = document.createElement('div'); | ||
render(html`<div foo="1${'bar'}2${'baz'}3"></div>`, container); | ||
@@ -258,3 +302,2 @@ assert.equal(container.innerHTML, '<div foo="1bar2baz3"></div>'); | ||
// This test just checks that we don't call the function | ||
const container = document.createElement('div'); | ||
render(html`<div foo=${(_: any) => 123}></div>`, container); | ||
@@ -265,3 +308,2 @@ assert.equal(container.innerHTML, '<div foo="(_) => 123"></div>'); | ||
test('renders an array to an attribute', () => { | ||
const container = document.createElement('div'); | ||
render(html`<div foo=${[1, 2, 3]}></div>`, container); | ||
@@ -272,3 +314,2 @@ assert.equal(container.innerHTML, '<div foo="123"></div>'); | ||
test('renders to an attribute before a node', () => { | ||
const container = document.createElement('div'); | ||
render(html`<div foo="${'bar'}">${'baz'}</div>`, container); | ||
@@ -279,3 +320,2 @@ assert.equal(container.innerHTML, '<div foo="bar">baz</div>'); | ||
test('renders to an attribute after a node', () => { | ||
const container = document.createElement('div'); | ||
render(html`<div>${'baz'}</div><div foo="${'bar'}"></div>`, container); | ||
@@ -287,3 +327,2 @@ assert.equal( | ||
test('renders a Promise', async () => { | ||
const container = document.createElement('div'); | ||
let resolve: (v: any) => void; | ||
@@ -301,3 +340,2 @@ const promise = new Promise((res, _) => { | ||
test('renders racing Promises correctly', async () => { | ||
const container = document.createElement('div'); | ||
let resolve1: (v: any) => void; | ||
@@ -337,3 +375,2 @@ const promise1 = new Promise((res, _) => { | ||
test('renders a combination of stuff', () => { | ||
const container = document.createElement('div'); | ||
render(html` | ||
@@ -349,2 +386,24 @@ <div foo="${'bar'}"> | ||
test('renders SVG', () => { | ||
const container = document.createElement('svg'); | ||
const t = svg`<line y1="1" y2="1"/>`; | ||
render(t, container); | ||
const line = container.firstElementChild!; | ||
assert.equal(line.tagName, 'line'); | ||
assert.equal(line.namespaceURI, 'http://www.w3.org/2000/svg'); | ||
}); | ||
test('renders templates with comments', () => { | ||
const t = html` | ||
<div> | ||
<!-- this is a comment --> | ||
<h1 class="${'foo'}">title</h1> | ||
<p>${'foo'}</p> | ||
</div>`; | ||
render(t, container); | ||
assert.equal(container.innerHTML, `<div> | ||
<!-- this is a comment --> | ||
<h1 class="foo">title</h1><p>foo</p></div>`); | ||
}); | ||
}); | ||
@@ -354,4 +413,9 @@ | ||
let container: HTMLElement; | ||
setup(() => { | ||
container = document.createElement('div'); | ||
}); | ||
test('dirty checks simple values', () => { | ||
const container = document.createElement('div'); | ||
const foo = 'aaa'; | ||
@@ -383,3 +447,2 @@ | ||
test('renders to and updates a container', () => { | ||
const container = document.createElement('div'); | ||
let foo = 'aaa'; | ||
@@ -403,3 +466,2 @@ | ||
test('renders to and updates sibling parts', () => { | ||
const container = document.createElement('div'); | ||
let foo = 'foo'; | ||
@@ -419,3 +481,2 @@ const bar = 'bar'; | ||
test('renders and updates attributes', () => { | ||
const container = document.createElement('div'); | ||
let foo = 'foo'; | ||
@@ -435,3 +496,2 @@ const bar = 'bar'; | ||
test('updates nested templates', () => { | ||
const container = document.createElement('div'); | ||
let foo = 'foo'; | ||
@@ -464,3 +524,2 @@ const bar = 'bar'; | ||
test('updates arrays', () => { | ||
const container = document.createElement('div'); | ||
let items = [1, 2, 3]; | ||
@@ -476,4 +535,20 @@ const t = () => html`<div>${items}</div>`; | ||
test('updates arrays that shrink then grow', () => { | ||
let items: number[]; | ||
const t = () => html`<div>${items}</div>`; | ||
items = [1, 2, 3]; | ||
render(t(), container); | ||
assert.equal(container.innerHTML, '<div>123</div>'); | ||
items = [4]; | ||
render(t(), container); | ||
assert.equal(container.innerHTML, '<div>4</div>'); | ||
items = [5, 6, 7]; | ||
render(t(), container); | ||
assert.equal(container.innerHTML, '<div>567</div>'); | ||
}); | ||
test('updates an element', () => { | ||
const container = document.createElement('div'); | ||
let child: any = document.createElement('p'); | ||
@@ -494,3 +569,2 @@ const t = () => html`<div>${child}<div></div></div>`; | ||
test('updates an array of elements', () => { | ||
const container = document.createElement('div'); | ||
let children: any = [ | ||
@@ -519,3 +593,2 @@ document.createElement('p'), | ||
() => { | ||
const container = document.createElement('div'); | ||
@@ -522,0 +595,0 @@ render(html`<div>foo</div>`, container); |
@@ -8,2 +8,3 @@ { | ||
"sourceMap": true, | ||
"inlineSources": true, | ||
"outDir": "./", | ||
@@ -10,0 +11,0 @@ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ |
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
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
374
752711
10
50
3890