Comparing version 0.8.0-pre.1 to 0.8.0
@@ -13,7 +13,16 @@ # Change Log | ||
## Unreleased | ||
<!-- ## Unreleased --> | ||
* Added the `asyncAppend` and `asyncReplace` directives to handle async iterable values | ||
in expressions. | ||
## [0.8.0] - 2018-01-12 | ||
* Allow all valid HTML attribute names, including emoji and Angular-style | ||
`(foo)=` and `[foo]=` names. | ||
* Drastically improved performance of the `repeat` directive. | ||
* Fixed an issue with expressions directly following elements. | ||
* Fixed numerous bugs with the `repeat` directive. | ||
* Performance improvements for template setup | ||
* Internal code cleanup | ||
* Support synchronous thenables | ||
* Added the `asyncAppend` and `asyncReplace` directives to handle async iterable values in expressions. | ||
## [0.7.0] - 2017-10-06 | ||
@@ -20,0 +29,0 @@ |
@@ -43,3 +43,2 @@ /** | ||
} | ||
part.clear(); | ||
part._previousValue = value; | ||
@@ -53,2 +52,7 @@ // We keep track of item Parts across iterations, so that we can | ||
let v = await value_1_1.value; | ||
// When we get the first value, clear the part. This let's the previous | ||
// value display until we can replace it. | ||
if (i === 0) { | ||
part.clear(); | ||
} | ||
// Check to make sure that value is the still the current value of | ||
@@ -55,0 +59,0 @@ // the part, and if not bail because a new value owns this part |
@@ -47,3 +47,2 @@ /** | ||
const itemPart = new NodePart(part.instance, part.startNode, part.endNode); | ||
part.clear(); | ||
part._previousValue = itemPart; | ||
@@ -54,2 +53,7 @@ let i = 0; | ||
let v = await value_1_1.value; | ||
// When we get the first value, clear the part. This let's the previous | ||
// value display until we can replace it. | ||
if (i === 0) { | ||
part.clear(); | ||
} | ||
// Check to make sure that value is the still the current value of | ||
@@ -56,0 +60,0 @@ // the part, and if not bail because a new value owns this part |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { directive, NodePart } from '../lit-html.js'; | ||
import { directive, NodePart, removeNodes, reparentNodes } from '../lit-html.js'; | ||
const keyMapCache = new WeakMap(); | ||
@@ -31,2 +31,5 @@ function cleanMap(part, key, map) { | ||
return directive((part) => { | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('repeat can only be used on NodeParts'); | ||
} | ||
let keyMap = keyMapCache.get(part); | ||
@@ -67,6 +70,4 @@ if (keyMap === undefined) { | ||
const end = itemPart.endNode.nextSibling; | ||
for (let node = itemPart.startNode; node !== end;) { | ||
const n = node.nextSibling; | ||
container.insertBefore(node, currentMarker); | ||
node = n; | ||
if (currentMarker !== end) { | ||
reparentNodes(container, itemPart.startNode, end, currentMarker); | ||
} | ||
@@ -82,8 +83,3 @@ } | ||
if (currentMarker !== part.endNode) { | ||
const end = part.endNode; | ||
for (let node = currentMarker; node !== end;) { | ||
const n = node.nextSibling; | ||
container.removeChild(node); | ||
node = n; | ||
} | ||
removeNodes(container, currentMarker, part.endNode); | ||
keyMap.forEach(cleanMap); | ||
@@ -90,0 +86,0 @@ } |
@@ -68,4 +68,4 @@ /** | ||
export declare const getValue: (part: Part, value: any) => any; | ||
export declare type DirectiveFn = (part: Part) => any; | ||
export declare const directive: <F extends DirectiveFn>(f: F) => F; | ||
export declare type DirectiveFn<P extends Part = Part> = (part: P) => any; | ||
export declare const directive: <P extends Part = Part, F = DirectiveFn<P>>(f: F) => F; | ||
export interface Part { | ||
@@ -72,0 +72,0 @@ instance: TemplateInstance; |
@@ -87,5 +87,29 @@ /** | ||
const markerRegex = new RegExp(`${marker}|${nodeMarker}`); | ||
const nonWhitespace = /[^\s]/; | ||
const lastAttributeNameRegex = /([\w\-.$]+)=(?:[^"']*|"[^"]*|'[^']*)$/; | ||
/** | ||
* 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. | ||
* | ||
* See attributes in the HTML spec: | ||
* https://www.w3.org/TR/html5/syntax.html#attributes-0 | ||
* | ||
* "\0-\x1F\x7F-\x9F" are Unicode control characters | ||
* | ||
* " \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-(') | ||
*/ | ||
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. | ||
@@ -184,33 +208,19 @@ * This has 3 possible return values: | ||
const nodeValue = node.nodeValue; | ||
if (nodeValue.indexOf(marker) > -1) { | ||
const parent = node.parentNode; | ||
const strings = nodeValue.split(markerRegex); | ||
const lastIndex = strings.length - 1; | ||
// We have a part for each match found | ||
partIndex += lastIndex; | ||
// We keep this current node, but reset its content to the last | ||
// literal part. We insert new literal nodes before this so that the | ||
// tree walker keeps its position correctly. | ||
node.textContent = strings[lastIndex]; | ||
// Generate a new text node for each literal section | ||
// These nodes are also used as the markers for node parts | ||
for (let i = 0; i < lastIndex; i++) { | ||
parent.insertBefore(document.createTextNode(strings[i]), node); | ||
this.parts.push(new TemplatePart('node', index++)); | ||
} | ||
if (nodeValue.indexOf(marker) < 0) { | ||
continue; | ||
} | ||
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 */) && | ||
!nonWhitespace.test(nodeValue)) { | ||
nodesToRemove.push(node); | ||
currentNode = previousNode; | ||
index--; | ||
} | ||
const parent = node.parentNode; | ||
const strings = nodeValue.split(markerRegex); | ||
const lastIndex = strings.length - 1; | ||
// We have a part for each match found | ||
partIndex += lastIndex; | ||
// We keep this current node, but reset its content to the last | ||
// literal part. We insert new literal nodes before this so that the | ||
// tree walker keeps its position correctly. | ||
node.textContent = strings[lastIndex]; | ||
// Generate a new text node for each literal section | ||
// These nodes are also used as the markers for node parts | ||
for (let i = 0; i < lastIndex; i++) { | ||
parent.insertBefore(document.createTextNode(strings[i]), node); | ||
this.parts.push(new TemplatePart('node', index++)); | ||
} | ||
@@ -217,0 +227,0 @@ } |
{ | ||
"name": "lit-html", | ||
"version": "0.8.0-pre.1", | ||
"version": "0.8.0", | ||
"description": "HTML template literals in JavaScript", | ||
@@ -27,18 +27,19 @@ "license": "BSD-3-Clause", | ||
"format": "find src test | grep '\\.js$\\|\\.ts$' | xargs clang-format --style=file -i", | ||
"lint": "tslint --project ./" | ||
"lint": "tslint --project ./", | ||
"prepare": "npm run build" | ||
}, | ||
"author": "The Polymer Authors", | ||
"devDependencies": { | ||
"@types/chai": "^4.0.1", | ||
"@types/mocha": "^2.2.41", | ||
"chai": "^4.0.2", | ||
"mocha": "^3.4.2", | ||
"tslint": "^5.7.0", | ||
"typedoc": "^0.8.0", | ||
"typescript": "^2.4.1", | ||
"uglify-es": "^3.0.27", | ||
"wct-browser-legacy": "^0.0.1-pre.10", | ||
"web-component-tester": "^6.3.0", | ||
"@webcomponents/template": "^1.1.0", | ||
"get-own-property-symbols": "^0.9.2" | ||
"@types/chai": "^4.1.0", | ||
"@types/mocha": "^2.2.46", | ||
"@webcomponents/template": "^1.2.2", | ||
"babel-polyfill": "^6.26.0", | ||
"chai": "^4.1.2", | ||
"mocha": "^3.5.3", | ||
"tslint": "^5.9.1", | ||
"typedoc": "^0.9.0", | ||
"typescript": "^2.6.2", | ||
"uglify-es": "^3.3.5", | ||
"wct-browser-legacy": "0.0.1-pre.11", | ||
"web-component-tester": "^6.4.3" | ||
}, | ||
@@ -45,0 +46,0 @@ "typings": "lit-html.d.ts", |
@@ -170,2 +170,12 @@ # lit-html | ||
### 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 | ||
@@ -246,6 +256,9 @@ | ||
`asyncAppend` renders the values of an [async iterable](https://github.com/tc39/proposal-async-iteration), | ||
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), | ||
* `asyncReplace` renders the values of an [async iterable](https://github.com/tc39/proposal-async-iteration), | ||
replacing the previous value with the new value. | ||
@@ -256,28 +269,30 @@ | ||
```javascript | ||
const wait = (t) => new Promise((resolve) => setTimeout(resolve, t)); | ||
/** | ||
* Returns an async iterable that yields the number of seconds since `date`, | ||
* approximately every second. | ||
* Returns an async iterable that yields increasing integers. | ||
*/ | ||
async function* secondsAgo(date) { | ||
const seconds = (Date.now() - date.getTime()) / 1000; | ||
const wholeSeconds = Math.floor(seconds); | ||
yield wholeSeconds; | ||
yield* await new Promise((res) => { | ||
setTimeout(() => { | ||
res(secondsAgo(date)); | ||
}, 1000 - (seconds - wholeSeconds) * 1000); | ||
}); | ||
async function* countUp() { | ||
let i = 0; | ||
while (true) { | ||
yield i++; | ||
await wait(1000); | ||
} | ||
} | ||
render(html` | ||
This example started ${asyncReplace(secondsAgo(new Date()))} seconds ago. | ||
Count: <span>${asyncReplace(countUp())}</span>. | ||
`, document.body); | ||
``` | ||
In the near future, `ReadableStream`s will be async iterables, enabling streaming `fetch()` | ||
directly into a template: | ||
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('https://cors-anywhere.herokuapp.com/http://stuff.mit.edu/afs/sipb/contrib/pi/pi-billion.txt'); | ||
const response = await fetch(url); | ||
return response.body.getReader(); | ||
@@ -288,12 +303,2 @@ })(); | ||
### 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())}. | ||
`; | ||
``` | ||
### Composability | ||
@@ -300,0 +305,0 @@ |
@@ -42,3 +42,2 @@ /** | ||
} | ||
part.clear(); | ||
part._previousValue = value; | ||
@@ -52,2 +51,8 @@ | ||
for await (let v of value) { | ||
// When we get the first value, clear the part. This let's the previous | ||
// value display until we can replace it. | ||
if (i === 0) { | ||
part.clear(); | ||
} | ||
// Check to make sure that value is the still the current value of | ||
@@ -54,0 +59,0 @@ // the part, and if not bail because a new value owns this part |
@@ -38,3 +38,2 @@ /** | ||
directive(async (part: NodePart) => { | ||
// If we've already set up this particular iterable, we don't need | ||
@@ -51,3 +50,2 @@ // to do anything. | ||
part.clear(); | ||
part._previousValue = itemPart; | ||
@@ -58,2 +56,8 @@ | ||
for await (let v of value) { | ||
// When we get the first value, clear the part. This let's the previous | ||
// value display until we can replace it. | ||
if (i === 0) { | ||
part.clear(); | ||
} | ||
// Check to make sure that value is the still the current value of | ||
@@ -60,0 +64,0 @@ // the part, and if not bail because a new value owns this part |
@@ -15,3 +15,3 @@ /** | ||
import {directive, DirectiveFn, NodePart} from '../lit-html.js'; | ||
import {directive, DirectiveFn, NodePart, removeNodes, reparentNodes, Part} from '../lit-html.js'; | ||
@@ -43,3 +43,6 @@ export type KeyFn<T> = (item: T) => any; | ||
return directive((part: NodePart): any => { | ||
return directive((part: Part): any => { | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('repeat can only be used on NodeParts'); | ||
} | ||
@@ -81,7 +84,9 @@ let keyMap = keyMapCache.get(part); | ||
// Existing part in the wrong position | ||
const end = itemPart.endNode.nextSibling; | ||
for (let node = itemPart.startNode; node !== end;) { | ||
const n = node.nextSibling!; | ||
container.insertBefore(node, currentMarker); | ||
node = n; | ||
const end = itemPart.endNode.nextSibling!; | ||
if (currentMarker !== end) { | ||
reparentNodes( | ||
container, | ||
itemPart.startNode, | ||
end, | ||
currentMarker); | ||
} | ||
@@ -98,8 +103,3 @@ } else { | ||
if (currentMarker !== part.endNode) { | ||
const end = part.endNode; | ||
for (let node = currentMarker; node !== end;) { | ||
const n = node.nextSibling!; | ||
container.removeChild(node); | ||
node = n; | ||
} | ||
removeNodes(container, currentMarker, part.endNode); | ||
keyMap.forEach(cleanMap); | ||
@@ -106,0 +106,0 @@ } |
@@ -112,6 +112,32 @@ /** | ||
const markerRegex = new RegExp(`${marker}|${nodeMarker}`); | ||
const nonWhitespace = /[^\s]/; | ||
const lastAttributeNameRegex = /([\w\-.$]+)=(?:[^"']*|"[^"]*|'[^']*)$/; | ||
/** | ||
* 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. | ||
* | ||
* See attributes in the HTML spec: | ||
* https://www.w3.org/TR/html5/syntax.html#attributes-0 | ||
* | ||
* "\0-\x1F\x7F-\x9F" are Unicode control characters | ||
* | ||
* " \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-(') | ||
*/ | ||
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. | ||
@@ -226,35 +252,23 @@ * This has 3 possible return values: | ||
const nodeValue = node.nodeValue!; | ||
if (nodeValue.indexOf(marker) > -1) { | ||
const parent = node.parentNode!; | ||
const strings = nodeValue.split(markerRegex); | ||
const lastIndex = strings.length - 1; | ||
if (nodeValue.indexOf(marker) < 0) { | ||
continue; | ||
} | ||
// We have a part for each match found | ||
partIndex += lastIndex; | ||
const parent = node.parentNode!; | ||
const strings = nodeValue.split(markerRegex); | ||
const lastIndex = strings.length - 1; | ||
// We keep this current node, but reset its content to the last | ||
// literal part. We insert new literal nodes before this so that the | ||
// tree walker keeps its position correctly. | ||
node.textContent = strings[lastIndex]; | ||
// 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(document.createTextNode(strings[i]), node); | ||
this.parts.push(new TemplatePart('node', index++)); | ||
} | ||
} 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 */) && | ||
!nonWhitespace.test(nodeValue)) { | ||
nodesToRemove.push(node); | ||
currentNode = previousNode; | ||
index--; | ||
} | ||
// We keep this current node, but reset its content to the last | ||
// literal part. We insert new literal nodes before this so that the | ||
// tree walker keeps its position correctly. | ||
node.textContent = strings[lastIndex]; | ||
// Generate a new text node for each literal section | ||
// These nodes are also used as the markers for node parts | ||
for (let i = 0; i < lastIndex; i++) { | ||
parent.insertBefore(document.createTextNode(strings[i]), node); | ||
this.parts.push(new TemplatePart('node', index++)); | ||
} | ||
@@ -342,5 +356,5 @@ } else if ( | ||
export type DirectiveFn = (part: Part) => any; | ||
export type DirectiveFn<P extends Part = Part> = (part: P) => any; | ||
export const directive = <F extends DirectiveFn>(f: F): F => { | ||
export const directive = <P extends Part = Part, F = DirectiveFn<P>>(f: F): F => { | ||
(f as any).__litDirective = true; | ||
@@ -560,3 +574,4 @@ return f; | ||
clear(startNode: Node = this.startNode) { | ||
removeNodes(this.startNode.parentNode!, startNode.nextSibling!, this.endNode); | ||
removeNodes( | ||
this.startNode.parentNode!, startNode.nextSibling!, this.endNode); | ||
} | ||
@@ -664,11 +679,10 @@ } | ||
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; | ||
} | ||
}; | ||
(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
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
177155
35
2293
421