lit-html
Advanced tools
Comparing version 0.11.4 to 0.12.0
@@ -19,5 +19,17 @@ # Change Log | ||
## [0.12.0] - 2018-10-05 | ||
### Changed | ||
* Re-implemented repeat directive for better performance ([#501](https://github.com/Polymer/lit-html/pull/501)) | ||
* Updated TypeScript dependency to 3.1 | ||
* [Breaking] `render()` now takes an options object as the third argument. ([#523](https://github.com/Polymer/lit-html/pull/523)) | ||
### Added | ||
* Event listeners are called with a configurable `this` reference, which is set via the `eventContext` option to `render()`. ([#523](https://github.com/Polymer/lit-html/pull/523)) | ||
* Support for event listener options, by passing the listener itself as both the second and third arguments to add/removeEventListener(). | ||
<!-- ### Removed --> | ||
<!-- ### Fixed --> | ||
## [0.11.4] - 2018-09-17 | ||
### Fixed | ||
* Fixed issues with `shady-render` introduced in 0.11.3 ([#504](https://github.com/Polymer/lit-html/issues/504) and [#505](https://github.com/Polymer/lit-html/issues/505). | ||
* Fixed issues with `shady-render` introduced in 0.11.3 ([#504](https://github.com/Polymer/lit-html/issues/504) and [#505](https://github.com/Polymer/lit-html/issues/505)). | ||
@@ -24,0 +36,0 @@ ## [0.11.3] - 2018-09-13 |
@@ -33,1 +33,2 @@ /** | ||
export declare const asyncAppend: <T>(value: AsyncIterable<T>, mapper?: ((v: T, index?: number | undefined) => any) | undefined) => Directive<NodePart>; | ||
//# sourceMappingURL=async-append.d.ts.map |
@@ -88,3 +88,3 @@ /** | ||
} | ||
itemPart = new NodePart(part.templateFactory); | ||
itemPart = new NodePart(part.options); | ||
itemPart.insertAfterNode(itemStartNode); | ||
@@ -91,0 +91,0 @@ itemPart.setValue(v); |
@@ -34,1 +34,2 @@ /** | ||
export declare const asyncReplace: <T>(value: AsyncIterable<T>, mapper?: ((v: T, index?: number | undefined) => any) | undefined) => Directive<NodePart>; | ||
//# sourceMappingURL=async-replace.d.ts.map |
@@ -49,3 +49,3 @@ /** | ||
// of the iterable as a value itself. | ||
const itemPart = new NodePart(part.templateFactory); | ||
const itemPart = new NodePart(part.options); | ||
part.value = value; | ||
@@ -52,0 +52,0 @@ let i = 0; |
@@ -29,1 +29,2 @@ /** | ||
export declare const classMap: (classInfo: ClassInfo) => Directive<AttributePart>; | ||
//# sourceMappingURL=classMap.d.ts.map |
@@ -35,1 +35,2 @@ /** | ||
export declare const guard: (expression: any, valueFn: () => any) => Directive<NodePart>; | ||
//# sourceMappingURL=guard.d.ts.map |
@@ -22,1 +22,2 @@ /** | ||
export declare const ifDefined: (value: any) => Directive<Part>; | ||
//# sourceMappingURL=if-defined.d.ts.map |
@@ -15,5 +15,25 @@ /** | ||
import { Directive, NodePart } from '../lit-html.js'; | ||
export declare type KeyFn<T> = (item: T) => any; | ||
export declare type ItemTemplate<T> = (item: T, index: number) => any; | ||
export declare function repeat<T>(items: T[], keyFn: KeyFn<T>, template: ItemTemplate<T>): Directive<NodePart>; | ||
export declare function repeat<T>(items: T[], template: ItemTemplate<T>): Directive<NodePart>; | ||
export declare type KeyFn<T> = (item: T, index?: number) => any; | ||
export declare type ItemTemplate<T> = (item: T, index?: number) => any; | ||
/** | ||
* A directive that repeats a series of values (usually `TemplateResults`) | ||
* generated from an iterable, and updates those items efficiently when the | ||
* iterable changes based on user-provided `keys` associated with each item. | ||
* | ||
* Note that if a `keyFn` is provided, strict key-to-DOM mapping is maintained, | ||
* meaning previous DOM for a given key is moved into the new position if | ||
* needed, and DOM will never be reused with values for different keys (new DOM | ||
* will always be created for new keys). This is generally the most efficient | ||
* way to use `repeat` since it performs minimum unnecessary work for insertions | ||
* amd removals. | ||
* | ||
* IMPORTANT: if providing a `keyFn`, keys *must* be unique for all items in a | ||
* given call to `repeat`. The behavior when providing duplicate keys is | ||
* undefined. | ||
* | ||
* If no `keyFn` is provided, this directive will perform similar to mapping | ||
* items to values, and DOM will be reused against potentially different items. | ||
*/ | ||
export declare function repeat<T>(items: Iterable<T>, keyFn: KeyFn<T>, template: ItemTemplate<T>): Directive<NodePart>; | ||
export declare function repeat<T>(items: Iterable<T>, template: ItemTemplate<T>): Directive<NodePart>; | ||
//# sourceMappingURL=repeat.d.ts.map |
@@ -15,8 +15,43 @@ /** | ||
import { createMarker, directive, NodePart, removeNodes, reparentNodes } from '../lit-html.js'; | ||
const keyMapCache = new WeakMap(); | ||
function cleanMap(part, key, map) { | ||
if (!part.startNode.parentNode) { | ||
map.delete(key); | ||
// Helper functions for manipulating parts | ||
// TODO(kschaaf): Refactor into Part API? | ||
const createAndInsertPart = (containerPart, beforePart) => { | ||
const container = containerPart.startNode.parentNode; | ||
const beforeNode = beforePart === undefined ? containerPart.endNode : | ||
beforePart.startNode; | ||
const startNode = container.insertBefore(createMarker(), beforeNode); | ||
container.insertBefore(createMarker(), beforeNode); | ||
const newPart = new NodePart(containerPart.options); | ||
newPart.insertAfterNode(startNode); | ||
return newPart; | ||
}; | ||
const updatePart = (part, value) => { | ||
part.setValue(value); | ||
part.commit(); | ||
return part; | ||
}; | ||
const insertPartBefore = (containerPart, part, ref) => { | ||
const container = containerPart.startNode.parentNode; | ||
const beforeNode = ref ? ref.startNode : containerPart.endNode; | ||
const endNode = part.endNode.nextSibling; | ||
if (endNode !== beforeNode) { | ||
reparentNodes(container, part.startNode, endNode, beforeNode); | ||
} | ||
} | ||
}; | ||
const removePart = (part) => { | ||
removeNodes(part.startNode.parentNode, part.startNode, part.endNode.nextSibling); | ||
}; | ||
// Helper for generating a map of array item to its index over a subset | ||
// of an array (used to lazily generate `newKeyToIndexMap` and | ||
// `oldKeyToIndexMap`) | ||
const generateMap = (list, start, end) => { | ||
const map = new Map(); | ||
for (let i = start; i <= end; i++) { | ||
map.set(list[i], i); | ||
} | ||
return map; | ||
}; | ||
// Stores previous ordered list of parts and map of key to index | ||
const partListCache = new WeakMap(); | ||
const keyListCache = new WeakMap(); | ||
export function repeat(items, keyFnOrTemplate, template) { | ||
@@ -30,64 +65,298 @@ let keyFn; | ||
} | ||
return directive((part) => { | ||
let keyMap = keyMapCache.get(part); | ||
if (keyMap === undefined) { | ||
keyMap = new Map(); | ||
keyMapCache.set(part, keyMap); | ||
return directive((containerPart) => { | ||
// Old part & key lists are retrieved from the last update (associated with | ||
// the part for this instance of the directive) | ||
const oldParts = partListCache.get(containerPart) || []; | ||
const oldKeys = keyListCache.get(containerPart) || []; | ||
// New part list will be built up as we go (either reused from old parts or | ||
// created for new keys in this update). This is saved in the above cache | ||
// at the end of the update. | ||
const newParts = []; | ||
// New value list is eagerly generated from items along with a parallel | ||
// array indicating its key. | ||
const newValues = []; | ||
const newKeys = []; | ||
let index = 0; | ||
for (const item of items) { | ||
newKeys[index] = keyFn ? keyFn(item, index) : index; | ||
newValues[index] = template(item, index); | ||
index++; | ||
} | ||
const container = part.startNode.parentNode; | ||
let index = -1; | ||
let currentMarker = part.startNode.nextSibling; | ||
for (const item of items) { | ||
let result; | ||
let key; | ||
try { | ||
++index; | ||
result = template(item, index); | ||
key = keyFn ? keyFn(item) : index; | ||
// Maps from key to index for current and previous update; these are | ||
// generated lazily only when needed as a performance optimization, since | ||
// they are only required for multiple non-contiguous changes in the list, | ||
// which are less common. | ||
let newKeyToIndexMap; | ||
let oldKeyToIndexMap; | ||
// Head and tail pointers to old parts and new values | ||
let oldHead = 0; | ||
let oldTail = oldParts.length - 1; | ||
let newHead = 0; | ||
let newTail = newValues.length - 1; | ||
// Overview of O(n) reconciliation algorithm (general approach based on | ||
// ideas found in ivi, vue, snabbdom, etc.): | ||
// | ||
// * We start with the list of old parts and new values (and arrays of | ||
// their respective keys), head/tail pointers into each, and we build | ||
// up the new list of parts by updating (and when needed, moving) old | ||
// parts or creating new ones. The initial scenario might look like this | ||
// (for brevity of the diagrams, the numbers in the array reflect keys | ||
// associated with the old parts or new values, although keys and | ||
// parts/values are actually stored in parallel arrays indexed using the | ||
// same head/tail pointers): | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [ , , , , , , ] | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] <- reflects the user's new item order | ||
// newHead ^ ^ newTail | ||
// | ||
// * Iterate old & new lists from both sides, updating, swapping, or | ||
// removing parts at the head/tail locations until neither head nor tail | ||
// can move. | ||
// | ||
// * Example below: keys at head pointers match, so update old part 0 in- | ||
// place (no need to move it) and record part 0 in the `newParts` list. | ||
// The last thing we do is advance the `oldHead` and `newHead` pointers | ||
// (will be reflected in the next diagram). | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [0, , , , , , ] <- heads matched: update 0 and | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] advance both oldHead & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Example below: head pointers don't match, but tail pointers do, so | ||
// update part 6 in place (no need to move it), and record part 6 in the | ||
// `newParts` list. Last, advance the `oldTail` and `oldHead` pointers. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [0, , , , , , 6] <- tails matched: update 6 and | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] advance both oldTail & newTail | ||
// newHead ^ ^ newTail | ||
// | ||
// * If neither head nor tail match; next check if one of the old head/tail | ||
// items was removed. We first need to generate the reverse map of new | ||
// keys to index (`newKeyToIndexMap`), which is done once lazily as a | ||
// performance optimization, since we only hit this case if multiple | ||
// non-contiguous changes were made. Note that for contiguous removal | ||
// anywhere in the list, the head and tails would advance from either end | ||
// and pass each other before we get to this case and removals would be | ||
// handled in the final while loop without needing to generate the map. | ||
// | ||
// * Example below: The key at `oldTail` was removed (no longer in the | ||
// `newKeyToIndexMap`), so remove that part from the DOM and advance just | ||
// the `oldTail` pointer. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [0, , , , , , 6] <- 5 not in new map; remove 5 and | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] advance oldTail | ||
// newHead ^ ^ newTail | ||
// | ||
// * Once head and tail cannot move, any mismatches are due to either new or | ||
// moved items; if a new key is in the previous "old key to old index" | ||
// map, move the old part to the new location, otherwise create and insert | ||
// a new part. Note that when moving an old part we null its position in | ||
// the oldParts array if it lies between the head and tail so we know to | ||
// skip it when the pointers get there. | ||
// | ||
// * Example below: neither head nor tail match, and neither were removed; | ||
// so find the `newHead` key in the `oldKeyToIndexMap`, and move that old | ||
// part's DOM into the next head position (before `oldParts[oldHead]`). | ||
// Last, null the part in the `oldPart` array since it was somewhere in | ||
// the remaining oldParts still to be scanned (between the head and tail | ||
// pointers) so that we know to skip that old part on future iterations. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, , , , , 6] <- stuck; update & move 2 into place | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Note that for moves/insertions like the one above, a part inserted at | ||
// the head pointer is inserted before the current `oldParts[oldHead]`, | ||
// and a part inserted at the tail pointer is inserted before | ||
// `newParts[newTail+1]`. The seeming asymmetry lies in the fact that new | ||
// parts are moved into place outside in, so to the right of the head | ||
// pointer are old parts, and to the right of the tail pointer are new | ||
// parts. | ||
// | ||
// * We always restart back from the top of the algorithm, allowing matching | ||
// and simple updates in place to continue... | ||
// | ||
// * Example below: the head pointers once again match, so simply update | ||
// part 1 and record it in the `newParts` array. Last, advance both head | ||
// pointers. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, , , , 6] <- heads matched; update 1 and | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] advance both oldHead & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * As mentioned above, items that were moved as a result of being stuck | ||
// (the final else clause in the code below) are marked with null, so we | ||
// always advance old pointers over these so we're comparing the next | ||
// actual old value on either end. | ||
// | ||
// * Example below: `oldHead` is null (already placed in newParts), so | ||
// advance `oldHead`. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] // old head already used; advance | ||
// newParts: [0, 2, 1, , , , 6] // oldHead | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] | ||
// newHead ^ ^ newTail | ||
// | ||
// * Note it's not critical to mark old parts as null when they are moved | ||
// from head to tail or tail to head, since they will be outside the | ||
// pointer range and never visited again. | ||
// | ||
// * Example below: Here the old tail key matches the new head key, so | ||
// the part at the `oldTail` position and move its DOM to the new | ||
// head position (before `oldParts[oldHead]`). Last, advance `oldTail` | ||
// and `newHead` pointers. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, 4, , , 6] <- old tail matches new head: update | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] & move 4, advance oldTail & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Example below: Old and new head keys match, so update the old head | ||
// part in place, and advance the `oldHead` and `newHead` pointers. | ||
// | ||
// oldHead v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, 4, 3, ,6] <- heads match: update 3 and advance | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] oldHead & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Once the new or old pointers move past each other then all we have | ||
// left is additions (if old list exhausted) or removals (if new list | ||
// exhausted). Those are handled in the final while loops at the end. | ||
// | ||
// * Example below: `oldHead` exceeded `oldTail`, so we're done with the | ||
// main loop. Create the remaining part and insert it at the new head | ||
// position, and the update is complete. | ||
// | ||
// (oldHead > oldTail) | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, 4, 3, 7 ,6] <- create and insert 7 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] | ||
// newHead ^ newTail | ||
// | ||
// * Note that the order of the if/else clauses is not important to the | ||
// algorithm, as long as the null checks come first (to ensure we're | ||
// always working on valid old parts) and that the final else clause | ||
// comes last (since that's where the expensive moves occur). The | ||
// order of remaining clauses is is just a simple guess at which cases | ||
// will be most common. | ||
// | ||
// * TODO(kschaaf) Note, we could calculate the longest increasing | ||
// subsequence (LIS) of old items in new position, and only move those not | ||
// in the LIS set. However that costs O(nlogn) time and adds a bit more | ||
// code, and only helps make rare types of mutations require fewer moves. | ||
// The above handles removes, adds, reversal, swaps, and single moves of | ||
// contiguous items in linear time, in the minimum number of moves. As | ||
// the number of multiple moves where LIS might help approaches a random | ||
// shuffle, the LIS optimization becomes less helpful, so it seems not | ||
// worth the code at this point. Could reconsider if a compelling case | ||
// arises. | ||
while (oldHead <= oldTail && newHead <= newTail) { | ||
if (oldParts[oldHead] === null) { | ||
// `null` means old part at head has already been used below; skip | ||
oldHead++; | ||
} | ||
catch (e) { | ||
console.error(e); | ||
continue; | ||
else if (oldParts[oldTail] === null) { | ||
// `null` means old part at tail has already been used below; skip | ||
oldTail--; | ||
} | ||
// Try to reuse a part | ||
let itemPart = keyMap.get(key); | ||
if (itemPart === undefined) { | ||
// TODO(justinfagnani): We really want to avoid manual marker creation | ||
// here and instead use something like part.insertBeforePart(). This | ||
// requires a little refactoring, like iterating through values and | ||
// existing parts like NodePart#_setIterable does. We can also remove | ||
// keyMapCache and use part._value instead. | ||
// But... repeat() is badly in need of rewriting, so we'll do this for | ||
// now and revisit soon. | ||
const marker = createMarker(); | ||
const endNode = createMarker(); | ||
container.insertBefore(marker, currentMarker); | ||
container.insertBefore(endNode, currentMarker); | ||
itemPart = new NodePart(part.templateFactory); | ||
itemPart.insertAfterNode(marker); | ||
if (key !== undefined) { | ||
keyMap.set(key, itemPart); | ||
} | ||
else if (oldKeys[oldHead] === newKeys[newHead]) { | ||
// Old head matches new head; update in place | ||
newParts[newHead] = updatePart(oldParts[oldHead], newValues[newHead]); | ||
oldHead++; | ||
newHead++; | ||
} | ||
else if (currentMarker !== itemPart.startNode) { | ||
// Existing part in the wrong position | ||
const end = itemPart.endNode.nextSibling; | ||
if (currentMarker !== end) { | ||
reparentNodes(container, itemPart.startNode, end, currentMarker); | ||
} | ||
else if (oldKeys[oldTail] === newKeys[newTail]) { | ||
// Old tail matches new tail; update in place | ||
newParts[newTail] = updatePart(oldParts[oldTail], newValues[newTail]); | ||
oldTail--; | ||
newTail--; | ||
} | ||
else if (oldKeys[oldHead] === newKeys[newTail]) { | ||
// Old head matches new tail; update and move to new tail | ||
newParts[newTail] = updatePart(oldParts[oldHead], newValues[newTail]); | ||
insertPartBefore(containerPart, oldParts[oldHead], newParts[newTail + 1]); | ||
oldHead++; | ||
newTail--; | ||
} | ||
else if (oldKeys[oldTail] === newKeys[newHead]) { | ||
// Old tail matches new head; update and move to new head | ||
newParts[newHead] = updatePart(oldParts[oldTail], newValues[newHead]); | ||
insertPartBefore(containerPart, oldParts[oldTail], oldParts[oldHead]); | ||
oldTail--; | ||
newHead++; | ||
} | ||
else { | ||
// else part is in the correct position already | ||
currentMarker = itemPart.endNode.nextSibling; | ||
if (newKeyToIndexMap === undefined) { | ||
// Lazily generate key-to-index maps, used for removals & moves below | ||
newKeyToIndexMap = generateMap(newKeys, newHead, newTail); | ||
oldKeyToIndexMap = generateMap(oldKeys, oldHead, oldTail); | ||
} | ||
if (!newKeyToIndexMap.has(oldKeys[oldHead])) { | ||
// Old head is no longer in new list; remove | ||
removePart(oldParts[oldHead]); | ||
oldHead++; | ||
} | ||
else if (!newKeyToIndexMap.has(oldKeys[oldTail])) { | ||
// Old tail is no longer in new list; remove | ||
removePart(oldParts[oldTail]); | ||
oldTail--; | ||
} | ||
else { | ||
// Any mismatches at this point are due to additions or moves; see if | ||
// we have an old part we can reuse and move into place | ||
const oldIndex = oldKeyToIndexMap.get(newKeys[newHead]); | ||
const oldPart = oldIndex !== undefined ? oldParts[oldIndex] : null; | ||
if (oldPart === null) { | ||
// No old part for this value; create a new one and insert it | ||
const newPart = createAndInsertPart(containerPart, oldParts[oldHead]); | ||
updatePart(newPart, newValues[newHead]); | ||
newParts[newHead] = newPart; | ||
} | ||
else { | ||
// Reuse old part | ||
newParts[newHead] = updatePart(oldPart, newValues[newHead]); | ||
insertPartBefore(containerPart, oldPart, oldParts[oldHead]); | ||
// This marks the old part as having been used, so that it will be | ||
// skipped in the first two checks above | ||
oldParts[oldIndex] = null; | ||
} | ||
newHead++; | ||
} | ||
} | ||
itemPart.setValue(result); | ||
itemPart.commit(); | ||
} | ||
// Cleanup | ||
if (currentMarker !== part.endNode) { | ||
removeNodes(container, currentMarker, part.endNode); | ||
keyMap.forEach(cleanMap); | ||
// Add parts for any remaining new values | ||
while (newHead <= newTail) { | ||
// For all remaining additions, we insert before last new tail, | ||
// since old pointers are no longer valid | ||
const newPart = createAndInsertPart(containerPart, newParts[newTail + 1]); | ||
updatePart(newPart, newValues[newHead]); | ||
newParts[newHead++] = newPart; | ||
} | ||
// Remove any remaining unused old parts | ||
while (oldHead <= oldTail) { | ||
const oldPart = oldParts[oldHead++]; | ||
if (oldPart !== null) { | ||
removePart(oldPart); | ||
} | ||
} | ||
// Save order of new parts for next round | ||
partListCache.set(containerPart, newParts); | ||
keyListCache.set(containerPart, newKeys); | ||
}); | ||
} | ||
//# sourceMappingURL=repeat.js.map |
@@ -27,1 +27,2 @@ /** | ||
export declare const styleMap: (styleInfo: StyleInfo) => Directive<AttributePart>; | ||
//# sourceMappingURL=styleMap.d.ts.map |
@@ -16,1 +16,2 @@ /** | ||
export declare const unsafeHTML: (value: any) => Directive<NodePart>; | ||
//# sourceMappingURL=unsafe-html.d.ts.map |
@@ -19,1 +19,2 @@ /** | ||
export declare const until: (promise: Promise<any>, defaultContent: any) => Directive<NodePart>; | ||
//# sourceMappingURL=until.d.ts.map |
@@ -26,2 +26,3 @@ /** | ||
* | ||
* ``` | ||
* let checked = false; | ||
@@ -33,2 +34,3 @@ * | ||
* ` | ||
* ``` | ||
* | ||
@@ -40,1 +42,2 @@ * @param condition the condition to test truthiness against | ||
export declare const when: (condition: any, trueValue: () => any, falseValue: () => any) => Directive<NodePart>; | ||
//# sourceMappingURL=when.d.ts.map |
@@ -27,2 +27,3 @@ /** | ||
* | ||
* ``` | ||
* let checked = false; | ||
@@ -34,2 +35,3 @@ * | ||
* ` | ||
* ``` | ||
* | ||
@@ -48,4 +50,4 @@ * @param condition the condition to test truthiness against | ||
cache = { | ||
truePart: new NodePart(parentPart.templateFactory), | ||
falsePart: new NodePart(parentPart.templateFactory), | ||
truePart: new NodePart(parentPart.options), | ||
falsePart: new NodePart(parentPart.options), | ||
cacheContainer: document.createDocumentFragment(), | ||
@@ -52,0 +54,0 @@ }; |
@@ -14,9 +14,10 @@ /** | ||
*/ | ||
import { TemplateFactory } from '../lit-html.js'; | ||
import { Part } from './part.js'; | ||
import { NodePart } from './parts.js'; | ||
import { RenderOptions } from './render-options.js'; | ||
import { TemplateProcessor } from './template-processor.js'; | ||
/** | ||
* Creates Parts when a template is instantiated. | ||
*/ | ||
export declare class DefaultTemplateProcessor { | ||
export declare class DefaultTemplateProcessor implements TemplateProcessor { | ||
/** | ||
@@ -31,3 +32,3 @@ * Create parts for an attribute-position binding, given the event, attribute | ||
*/ | ||
handleAttributeExpressions(element: Element, name: string, strings: string[]): Part[]; | ||
handleAttributeExpressions(element: Element, name: string, strings: string[], options: RenderOptions): Part[]; | ||
/** | ||
@@ -37,4 +38,5 @@ * Create parts for a text-position binding. | ||
*/ | ||
handleTextExpression(templateFactory: TemplateFactory): NodePart; | ||
handleTextExpression(options: RenderOptions): NodePart; | ||
} | ||
export declare const defaultTemplateProcessor: DefaultTemplateProcessor; | ||
//# sourceMappingURL=default-template-processor.d.ts.map |
@@ -28,3 +28,3 @@ /** | ||
*/ | ||
handleAttributeExpressions(element, name, strings) { | ||
handleAttributeExpressions(element, name, strings, options) { | ||
const prefix = name[0]; | ||
@@ -36,3 +36,3 @@ if (prefix === '.') { | ||
if (prefix === '@') { | ||
return [new EventPart(element, name.slice(1))]; | ||
return [new EventPart(element, name.slice(1), options.eventContext)]; | ||
} | ||
@@ -49,4 +49,4 @@ if (prefix === '?') { | ||
*/ | ||
handleTextExpression(templateFactory) { | ||
return new NodePart(templateFactory); | ||
handleTextExpression(options) { | ||
return new NodePart(options); | ||
} | ||
@@ -53,0 +53,0 @@ } |
@@ -20,1 +20,2 @@ /** | ||
export declare const isDirective: (o: any) => boolean; | ||
//# sourceMappingURL=directive.d.ts.map |
@@ -27,1 +27,2 @@ /** | ||
export declare const removeNodes: (container: Node, startNode: Node | null, endNode?: Node | null) => void; | ||
//# sourceMappingURL=dom.d.ts.map |
@@ -38,1 +38,2 @@ /** | ||
export declare function insertNodeIntoTemplate(template: Template, node: Node, refNode?: Node | null): void; | ||
//# sourceMappingURL=modify-template.d.ts.map |
@@ -22,1 +22,2 @@ /** | ||
export declare const noChange: {}; | ||
//# sourceMappingURL=part.d.ts.map |
@@ -15,3 +15,3 @@ /** | ||
import { Part } from './part.js'; | ||
import { TemplateFactory } from './template-factory.js'; | ||
import { RenderOptions } from './render-options.js'; | ||
export declare const isPrimitive: (value: any) => boolean; | ||
@@ -44,3 +44,3 @@ /** | ||
export declare class NodePart implements Part { | ||
templateFactory: TemplateFactory; | ||
options: RenderOptions; | ||
startNode: Node; | ||
@@ -50,3 +50,3 @@ endNode: Node; | ||
_pendingValue: any; | ||
constructor(templateFactory: TemplateFactory); | ||
constructor(options: RenderOptions); | ||
/** | ||
@@ -123,8 +123,21 @@ * Inserts this part into a container. | ||
} | ||
declare global { | ||
interface EventListenerOptions { | ||
capture?: boolean; | ||
once?: boolean; | ||
passive?: boolean; | ||
} | ||
} | ||
export declare class EventPart implements Part { | ||
element: Element; | ||
eventName: string; | ||
eventContext?: EventTarget; | ||
value: any; | ||
_options?: { | ||
capture?: boolean; | ||
passive?: boolean; | ||
once?: boolean; | ||
}; | ||
_pendingValue: any; | ||
constructor(element: Element, eventName: string); | ||
constructor(element: Element, eventName: string, eventContext?: EventTarget); | ||
setValue(value: any): void; | ||
@@ -134,1 +147,2 @@ commit(): void; | ||
} | ||
//# sourceMappingURL=parts.d.ts.map |
@@ -102,6 +102,6 @@ /** | ||
export class NodePart { | ||
constructor(templateFactory) { | ||
constructor(options) { | ||
this.value = undefined; | ||
this._pendingValue = undefined; | ||
this.templateFactory = templateFactory; | ||
this.options = options; | ||
} | ||
@@ -209,3 +209,3 @@ /** | ||
_commitTemplateResult(value) { | ||
const template = this.templateFactory(value); | ||
const template = this.options.templateFactory(value); | ||
if (this.value && this.value.template === template) { | ||
@@ -216,5 +216,6 @@ this.value.update(value.values); | ||
// Make sure we propagate the template processor from the TemplateResult | ||
// so that we use it's syntax extension, etc. The template factory comes | ||
// from the render function so that it can control caching. | ||
const instance = new TemplateInstance(template, value.processor, this.templateFactory); | ||
// so that we use its syntax extension, etc. The template factory comes | ||
// from the render function options so that it can control template | ||
// caching and preprocessing. | ||
const instance = new TemplateInstance(template, value.processor, this.options); | ||
const fragment = instance._clone(); | ||
@@ -250,3 +251,3 @@ instance.update(value.values); | ||
if (itemPart === undefined) { | ||
itemPart = new NodePart(this.templateFactory); | ||
itemPart = new NodePart(this.options); | ||
itemParts.push(itemPart); | ||
@@ -359,4 +360,21 @@ if (partIndex === 0) { | ||
} | ||
// Detect event listener options support. If the `capture` property is read | ||
// from the options object, then options are supported. If not, then the thrid | ||
// argument to add/removeEventListener is interpreted as the boolean capture | ||
// value so we should only pass the `capture` property. | ||
let eventOptionsSupported = false; | ||
try { | ||
const options = { | ||
get capture() { | ||
eventOptionsSupported = true; | ||
return false; | ||
} | ||
}; | ||
window.addEventListener('test', options, options); | ||
window.removeEventListener('test', options, options); | ||
} | ||
catch (_e) { | ||
} | ||
export class EventPart { | ||
constructor(element, eventName) { | ||
constructor(element, eventName, eventContext) { | ||
this.value = undefined; | ||
@@ -366,2 +384,3 @@ this._pendingValue = undefined; | ||
this.eventName = eventName; | ||
this.eventContext = eventContext; | ||
} | ||
@@ -380,22 +399,36 @@ setValue(value) { | ||
} | ||
if ((this._pendingValue == null) !== (this.value == null)) { | ||
if (this._pendingValue == null) { | ||
this.element.removeEventListener(this.eventName, this); | ||
} | ||
else { | ||
this.element.addEventListener(this.eventName, this); | ||
} | ||
const newListener = this._pendingValue; | ||
const oldListener = this.value; | ||
const shouldRemoveListener = newListener == null || | ||
oldListener != null && | ||
(newListener.capture !== oldListener.capture || | ||
newListener.once !== oldListener.once || | ||
newListener.passive !== oldListener.passive); | ||
const shouldAddListener = newListener != null && (oldListener == null || shouldRemoveListener); | ||
if (shouldRemoveListener) { | ||
this.element.removeEventListener(this.eventName, this, this._options); | ||
} | ||
this.value = this._pendingValue; | ||
this._options = getOptions(newListener); | ||
if (shouldAddListener) { | ||
this.element.addEventListener(this.eventName, this, this._options); | ||
} | ||
this.value = newListener; | ||
this._pendingValue = noChange; | ||
} | ||
handleEvent(event) { | ||
if (typeof this.value === 'function') { | ||
this.value.call(this.element, event); | ||
} | ||
else if (typeof this.value.handleEvent === 'function') { | ||
this.value.handleEvent(event); | ||
} | ||
const listener = (typeof this.value === 'function') ? | ||
this.value : | ||
(typeof this.value.handleEvent === 'function') ? | ||
this.value.handleEvent : | ||
() => null; | ||
listener.call(this.eventContext || this.element, event); | ||
} | ||
} | ||
// We copy options because of the inconsistent behavior of browsers when reading | ||
// the third argument of add/removeEventListener. IE11 doesn't support options | ||
// at all. Chrome 41 only reads `capture` if the argument is an object. | ||
const getOptions = (o) => o && | ||
(eventOptionsSupported ? | ||
{ capture: o.capture, passive: o.passive, once: o.once } : | ||
o.capture); | ||
//# sourceMappingURL=parts.js.map |
@@ -15,3 +15,3 @@ /** | ||
import { NodePart } from './parts.js'; | ||
import { TemplateFactory } from './template-factory.js'; | ||
import { RenderOptions } from './render-options.js'; | ||
import { TemplateResult } from './template-result.js'; | ||
@@ -30,5 +30,7 @@ export declare const parts: WeakMap<Node, NodePart>; | ||
* rendered there. | ||
* @param templateFactory a function to create a Template or retreive one from | ||
* cache. | ||
* @param options RenderOptions for the entire render tree rendered to this | ||
* container. Render options must *not* change between renders to the same | ||
* container, as those changes will not effect previously rendered DOM. | ||
*/ | ||
export declare function render(result: TemplateResult, container: Element | DocumentFragment, templateFactory?: TemplateFactory): void; | ||
export declare const render: (result: TemplateResult, container: Element | DocumentFragment, options?: Partial<RenderOptions> | undefined) => void; | ||
//# sourceMappingURL=render.d.ts.map |
@@ -16,3 +16,3 @@ /** | ||
import { NodePart } from './parts.js'; | ||
import { templateFactory as defaultTemplateFactory } from './template-factory.js'; | ||
import { templateFactory } from './template-factory.js'; | ||
export const parts = new WeakMap(); | ||
@@ -30,10 +30,11 @@ /** | ||
* rendered there. | ||
* @param templateFactory a function to create a Template or retreive one from | ||
* cache. | ||
* @param options RenderOptions for the entire render tree rendered to this | ||
* container. Render options must *not* change between renders to the same | ||
* container, as those changes will not effect previously rendered DOM. | ||
*/ | ||
export function render(result, container, templateFactory = defaultTemplateFactory) { | ||
export const render = (result, container, options) => { | ||
let part = parts.get(container); | ||
if (part === undefined) { | ||
removeNodes(container, container.firstChild); | ||
parts.set(container, part = new NodePart(templateFactory)); | ||
parts.set(container, part = new NodePart(Object.assign({ templateFactory }, options))); | ||
part.appendInto(container); | ||
@@ -43,3 +44,3 @@ } | ||
part.commit(); | ||
} | ||
}; | ||
//# sourceMappingURL=render.js.map |
@@ -14,2 +14,3 @@ /** | ||
*/ | ||
import { RenderOptions } from './render-options.js'; | ||
import { TemplateResult } from './template-result.js'; | ||
@@ -24,2 +25,6 @@ export { html, svg, TemplateResult } from '../lit-html.js'; | ||
} | ||
export declare function render(result: TemplateResult, container: Element | DocumentFragment, scopeName: string): void; | ||
export interface ShadyRenderOptions extends Partial<RenderOptions> { | ||
scopeName: string; | ||
} | ||
export declare const render: (result: TemplateResult, container: Element | DocumentFragment, options: ShadyRenderOptions) => void; | ||
//# sourceMappingURL=shady-render.d.ts.map |
@@ -58,3 +58,3 @@ /** | ||
*/ | ||
function removeStylesFromLitTemplates(scopeName) { | ||
const removeStylesFromLitTemplates = (scopeName) => { | ||
TEMPLATE_TYPES.forEach((type) => { | ||
@@ -74,3 +74,3 @@ const templates = templateCaches.get(getTemplateCacheKey(type, scopeName)); | ||
}); | ||
} | ||
}; | ||
const shadyRenderSet = new Set(); | ||
@@ -138,5 +138,6 @@ /** | ||
}; | ||
export function render(result, container, scopeName) { | ||
export const render = (result, container, options) => { | ||
const scopeName = options.scopeName; | ||
const hasRendered = parts.has(container); | ||
litRender(result, container, shadyTemplateFactory(scopeName)); | ||
litRender(result, container, Object.assign({ templateFactory: shadyTemplateFactory(scopeName) }, options)); | ||
// When rendering a TemplateResult, scope the template with ShadyCSS | ||
@@ -156,3 +157,3 @@ if (container instanceof ShadowRoot && compatibleShadyCSSVersion && | ||
} | ||
} | ||
}; | ||
//# sourceMappingURL=shady-render.js.map |
@@ -43,1 +43,2 @@ /** | ||
export declare const templateCaches: Map<string, Map<TemplateStringsArray, Template>>; | ||
//# sourceMappingURL=template-factory.d.ts.map |
@@ -15,3 +15,3 @@ /** | ||
import { Part } from './part.js'; | ||
import { TemplateFactory } from './template-factory.js'; | ||
import { RenderOptions } from './render-options.js'; | ||
import { TemplateProcessor } from './template-processor.js'; | ||
@@ -26,12 +26,8 @@ import { Template } from './template.js'; | ||
processor: TemplateProcessor; | ||
_getTemplate: TemplateFactory; | ||
options: RenderOptions; | ||
template: Template; | ||
constructor(template: Template, processor: TemplateProcessor, getTemplate: TemplateFactory); | ||
constructor(template: Template, processor: TemplateProcessor, options: RenderOptions); | ||
update(values: any[]): void; | ||
_clone(): DocumentFragment; | ||
} | ||
declare global { | ||
class CustomElementRegistry { | ||
upgrade(node: Node): void; | ||
} | ||
} | ||
//# sourceMappingURL=template-instance.d.ts.map |
@@ -21,7 +21,7 @@ /** | ||
export class TemplateInstance { | ||
constructor(template, processor, getTemplate) { | ||
constructor(template, processor, options) { | ||
this._parts = []; | ||
this.template = template; | ||
this.processor = processor; | ||
this._getTemplate = getTemplate; | ||
this.options = options; | ||
} | ||
@@ -74,3 +74,3 @@ update(values) { | ||
if (part.type === 'node') { | ||
const part = this.processor.handleTextExpression(this._getTemplate); | ||
const part = this.processor.handleTextExpression(this.options); | ||
part.insertAfterNode(node); | ||
@@ -80,3 +80,3 @@ this._parts.push(part); | ||
else { | ||
this._parts.push(...this.processor.handleAttributeExpressions(node, part.name, part.strings)); | ||
this._parts.push(...this.processor.handleAttributeExpressions(node, part.name, part.strings, this.options)); | ||
} | ||
@@ -83,0 +83,0 @@ partIndex++; |
@@ -16,3 +16,3 @@ /** | ||
import { NodePart } from './parts.js'; | ||
import { TemplateFactory } from './template-factory.js'; | ||
import { RenderOptions } from './render-options.js'; | ||
export interface TemplateProcessor { | ||
@@ -28,8 +28,9 @@ /** | ||
*/ | ||
handleAttributeExpressions(element: Element, name: string, strings: string[]): Part[]; | ||
handleAttributeExpressions(element: Element, name: string, strings: string[], options: RenderOptions): Part[]; | ||
/** | ||
* Create parts for a text-position binding. | ||
* @param templateFactory | ||
* @param partOptions | ||
*/ | ||
handleTextExpression(templateFactory: TemplateFactory): NodePart; | ||
handleTextExpression(options: RenderOptions): NodePart; | ||
} | ||
//# sourceMappingURL=template-processor.d.ts.map |
@@ -26,3 +26,3 @@ /** | ||
/** | ||
* Returns a string of HTML used to create a <template> element. | ||
* Returns a string of HTML used to create a `<template>` element. | ||
*/ | ||
@@ -35,4 +35,4 @@ getHTML(): string; | ||
* | ||
* 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 | ||
* 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. | ||
@@ -44,1 +44,2 @@ */ | ||
} | ||
//# sourceMappingURL=template-result.d.ts.map |
@@ -28,3 +28,3 @@ /** | ||
/** | ||
* Returns a string of HTML used to create a <template> element. | ||
* Returns a string of HTML used to create a `<template>` element. | ||
*/ | ||
@@ -67,4 +67,4 @@ getHTML() { | ||
* | ||
* 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 | ||
* 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. | ||
@@ -71,0 +71,0 @@ */ |
@@ -88,1 +88,2 @@ /** | ||
export declare const lastAttributeNameRegex: RegExp; | ||
//# sourceMappingURL=template.d.ts.map |
@@ -36,1 +36,2 @@ /** | ||
export declare const svg: (strings: TemplateStringsArray, ...values: any[]) => SVGTemplateResult; | ||
//# sourceMappingURL=lit-html.d.ts.map |
{ | ||
"name": "lit-html", | ||
"version": "0.11.4", | ||
"version": "0.12.0", | ||
"description": "HTML template literals in JavaScript", | ||
@@ -16,2 +16,3 @@ "license": "BSD-3-Clause", | ||
"/lit-html.d.ts", | ||
"/lit-html.d.ts.map", | ||
"/lib/", | ||
@@ -43,5 +44,5 @@ "/directives/", | ||
"rollup-plugin-terser": "^1.0.1", | ||
"tslint": "^5.9.1", | ||
"tslint": "^5.11.0", | ||
"typedoc": "^0.12.0", | ||
"typescript": "^2.8.3", | ||
"typescript": "^3.1.1", | ||
"uglify-es": "^3.3.5", | ||
@@ -48,0 +49,0 @@ "wct-browser-legacy": "^1.0.1", |
@@ -9,2 +9,3 @@ > ## 🛠 Status: In Development | ||
[![Published on npm](https://img.shields.io/npm/v/lit-html.svg)](https://www.npmjs.com/package/lit-html) | ||
[![Mentioned in Awesome lit-html](https://awesome.re/mentioned-badge.svg)](https://github.com/web-padawan/awesome-lit-html) | ||
@@ -11,0 +12,0 @@ ## Documentation |
@@ -89,3 +89,3 @@ /** | ||
} | ||
itemPart = new NodePart(part.templateFactory); | ||
itemPart = new NodePart(part.options); | ||
itemPart.insertAfterNode(itemStartNode); | ||
@@ -92,0 +92,0 @@ itemPart.setValue(v); |
@@ -46,3 +46,3 @@ /** | ||
// of the iterable as a value itself. | ||
const itemPart = new NodePart(part.templateFactory); | ||
const itemPart = new NodePart(part.options); | ||
part.value = value; | ||
@@ -49,0 +49,0 @@ |
@@ -17,18 +17,79 @@ /** | ||
export type KeyFn<T> = (item: T) => any; | ||
export type ItemTemplate<T> = (item: T, index: number) => any; | ||
export type KeyFn<T> = (item: T, index?: number) => any; | ||
export type ItemTemplate<T> = (item: T, index?: number) => any; | ||
const keyMapCache = new WeakMap<NodePart, Map<any, NodePart>>(); | ||
// Helper functions for manipulating parts | ||
// TODO(kschaaf): Refactor into Part API? | ||
const createAndInsertPart = | ||
(containerPart: NodePart, beforePart?: NodePart): NodePart => { | ||
const container = containerPart.startNode.parentNode as Node; | ||
const beforeNode = beforePart === undefined ? containerPart.endNode : | ||
beforePart.startNode; | ||
const startNode = container.insertBefore(createMarker(), beforeNode); | ||
container.insertBefore(createMarker(), beforeNode); | ||
const newPart = new NodePart(containerPart.options); | ||
newPart.insertAfterNode(startNode); | ||
return newPart; | ||
}; | ||
function cleanMap(part: NodePart, key: any, map: Map<any, NodePart>) { | ||
if (!part.startNode.parentNode) { | ||
map.delete(key); | ||
const updatePart = (part: NodePart, value: unknown) => { | ||
part.setValue(value); | ||
part.commit(); | ||
return part; | ||
}; | ||
const insertPartBefore = | ||
(containerPart: NodePart, part: NodePart, ref?: NodePart) => { | ||
const container = containerPart.startNode.parentNode as Node; | ||
const beforeNode = ref ? ref.startNode : containerPart.endNode; | ||
const endNode = part.endNode.nextSibling; | ||
if (endNode !== beforeNode) { | ||
reparentNodes(container, part.startNode, endNode, beforeNode); | ||
} | ||
}; | ||
const removePart = (part: NodePart) => { | ||
removeNodes( | ||
part.startNode.parentNode!, part.startNode, part.endNode.nextSibling); | ||
}; | ||
// Helper for generating a map of array item to its index over a subset | ||
// of an array (used to lazily generate `newKeyToIndexMap` and | ||
// `oldKeyToIndexMap`) | ||
const generateMap = (list: unknown[], start: number, end: number) => { | ||
const map = new Map(); | ||
for (let i = start; i <= end; i++) { | ||
map.set(list[i], i); | ||
} | ||
} | ||
return map; | ||
}; | ||
// Stores previous ordered list of parts and map of key to index | ||
const partListCache = new WeakMap<NodePart, (NodePart | null)[]>(); | ||
const keyListCache = new WeakMap<NodePart, unknown[]>(); | ||
/** | ||
* A directive that repeats a series of values (usually `TemplateResults`) | ||
* generated from an iterable, and updates those items efficiently when the | ||
* iterable changes based on user-provided `keys` associated with each item. | ||
* | ||
* Note that if a `keyFn` is provided, strict key-to-DOM mapping is maintained, | ||
* meaning previous DOM for a given key is moved into the new position if | ||
* needed, and DOM will never be reused with values for different keys (new DOM | ||
* will always be created for new keys). This is generally the most efficient | ||
* way to use `repeat` since it performs minimum unnecessary work for insertions | ||
* amd removals. | ||
* | ||
* IMPORTANT: if providing a `keyFn`, keys *must* be unique for all items in a | ||
* given call to `repeat`. The behavior when providing duplicate keys is | ||
* undefined. | ||
* | ||
* If no `keyFn` is provided, this directive will perform similar to mapping | ||
* items to values, and DOM will be reused against potentially different items. | ||
*/ | ||
export function repeat<T>( | ||
items: T[], keyFn: KeyFn<T>, template: ItemTemplate<T>): | ||
items: Iterable<T>, keyFn: KeyFn<T>, template: ItemTemplate<T>): | ||
Directive<NodePart>; | ||
export function repeat<T>( | ||
items: T[], template: ItemTemplate<T>): Directive<NodePart>; | ||
items: Iterable<T>, template: ItemTemplate<T>): Directive<NodePart>; | ||
export function repeat<T>( | ||
@@ -45,65 +106,297 @@ items: Iterable<T>, | ||
return directive((part: NodePart): void => { | ||
let keyMap = keyMapCache.get(part); | ||
if (keyMap === undefined) { | ||
keyMap = new Map(); | ||
keyMapCache.set(part, keyMap); | ||
} | ||
const container = part.startNode.parentNode as HTMLElement | ShadowRoot | | ||
DocumentFragment; | ||
let index = -1; | ||
let currentMarker = part.startNode.nextSibling!; | ||
return directive((containerPart: NodePart): void => { | ||
// Old part & key lists are retrieved from the last update (associated with | ||
// the part for this instance of the directive) | ||
const oldParts = partListCache.get(containerPart) || []; | ||
const oldKeys = keyListCache.get(containerPart) || []; | ||
// New part list will be built up as we go (either reused from old parts or | ||
// created for new keys in this update). This is saved in the above cache | ||
// at the end of the update. | ||
const newParts: NodePart[] = []; | ||
// New value list is eagerly generated from items along with a parallel | ||
// array indicating its key. | ||
const newValues: unknown[] = []; | ||
const newKeys: unknown[] = []; | ||
let index = 0; | ||
for (const item of items) { | ||
let result; | ||
let key; | ||
try { | ||
++index; | ||
result = template !(item, index); | ||
key = keyFn ? keyFn(item) : index; | ||
} catch (e) { | ||
console.error(e); | ||
continue; | ||
} | ||
newKeys[index] = keyFn ? keyFn(item, index) : index; | ||
newValues[index] = template !(item, index); | ||
index++; | ||
} | ||
// Try to reuse a part | ||
let itemPart = keyMap.get(key); | ||
if (itemPart === undefined) { | ||
// TODO(justinfagnani): We really want to avoid manual marker creation | ||
// here and instead use something like part.insertBeforePart(). This | ||
// requires a little refactoring, like iterating through values and | ||
// existing parts like NodePart#_setIterable does. We can also remove | ||
// keyMapCache and use part._value instead. | ||
// But... repeat() is badly in need of rewriting, so we'll do this for | ||
// now and revisit soon. | ||
const marker = createMarker(); | ||
const endNode = createMarker(); | ||
container.insertBefore(marker, currentMarker); | ||
container.insertBefore(endNode, currentMarker); | ||
itemPart = new NodePart(part.templateFactory); | ||
itemPart.insertAfterNode(marker); | ||
if (key !== undefined) { | ||
keyMap.set(key, itemPart); | ||
// Maps from key to index for current and previous update; these are | ||
// generated lazily only when needed as a performance optimization, since | ||
// they are only required for multiple non-contiguous changes in the list, | ||
// which are less common. | ||
let newKeyToIndexMap!: Map<unknown, number>; | ||
let oldKeyToIndexMap!: Map<unknown, number>; | ||
// Head and tail pointers to old parts and new values | ||
let oldHead = 0; | ||
let oldTail = oldParts.length - 1; | ||
let newHead = 0; | ||
let newTail = newValues.length - 1; | ||
// Overview of O(n) reconciliation algorithm (general approach based on | ||
// ideas found in ivi, vue, snabbdom, etc.): | ||
// | ||
// * We start with the list of old parts and new values (and arrays of | ||
// their respective keys), head/tail pointers into each, and we build | ||
// up the new list of parts by updating (and when needed, moving) old | ||
// parts or creating new ones. The initial scenario might look like this | ||
// (for brevity of the diagrams, the numbers in the array reflect keys | ||
// associated with the old parts or new values, although keys and | ||
// parts/values are actually stored in parallel arrays indexed using the | ||
// same head/tail pointers): | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [ , , , , , , ] | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] <- reflects the user's new item order | ||
// newHead ^ ^ newTail | ||
// | ||
// * Iterate old & new lists from both sides, updating, swapping, or | ||
// removing parts at the head/tail locations until neither head nor tail | ||
// can move. | ||
// | ||
// * Example below: keys at head pointers match, so update old part 0 in- | ||
// place (no need to move it) and record part 0 in the `newParts` list. | ||
// The last thing we do is advance the `oldHead` and `newHead` pointers | ||
// (will be reflected in the next diagram). | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [0, , , , , , ] <- heads matched: update 0 and | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] advance both oldHead & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Example below: head pointers don't match, but tail pointers do, so | ||
// update part 6 in place (no need to move it), and record part 6 in the | ||
// `newParts` list. Last, advance the `oldTail` and `oldHead` pointers. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [0, , , , , , 6] <- tails matched: update 6 and | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] advance both oldTail & newTail | ||
// newHead ^ ^ newTail | ||
// | ||
// * If neither head nor tail match; next check if one of the old head/tail | ||
// items was removed. We first need to generate the reverse map of new | ||
// keys to index (`newKeyToIndexMap`), which is done once lazily as a | ||
// performance optimization, since we only hit this case if multiple | ||
// non-contiguous changes were made. Note that for contiguous removal | ||
// anywhere in the list, the head and tails would advance from either end | ||
// and pass each other before we get to this case and removals would be | ||
// handled in the final while loop without needing to generate the map. | ||
// | ||
// * Example below: The key at `oldTail` was removed (no longer in the | ||
// `newKeyToIndexMap`), so remove that part from the DOM and advance just | ||
// the `oldTail` pointer. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, 2, 3, 4, 5, 6] | ||
// newParts: [0, , , , , , 6] <- 5 not in new map; remove 5 and | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] advance oldTail | ||
// newHead ^ ^ newTail | ||
// | ||
// * Once head and tail cannot move, any mismatches are due to either new or | ||
// moved items; if a new key is in the previous "old key to old index" | ||
// map, move the old part to the new location, otherwise create and insert | ||
// a new part. Note that when moving an old part we null its position in | ||
// the oldParts array if it lies between the head and tail so we know to | ||
// skip it when the pointers get there. | ||
// | ||
// * Example below: neither head nor tail match, and neither were removed; | ||
// so find the `newHead` key in the `oldKeyToIndexMap`, and move that old | ||
// part's DOM into the next head position (before `oldParts[oldHead]`). | ||
// Last, null the part in the `oldPart` array since it was somewhere in | ||
// the remaining oldParts still to be scanned (between the head and tail | ||
// pointers) so that we know to skip that old part on future iterations. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, , , , , 6] <- stuck; update & move 2 into place | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Note that for moves/insertions like the one above, a part inserted at | ||
// the head pointer is inserted before the current `oldParts[oldHead]`, | ||
// and a part inserted at the tail pointer is inserted before | ||
// `newParts[newTail+1]`. The seeming asymmetry lies in the fact that new | ||
// parts are moved into place outside in, so to the right of the head | ||
// pointer are old parts, and to the right of the tail pointer are new | ||
// parts. | ||
// | ||
// * We always restart back from the top of the algorithm, allowing matching | ||
// and simple updates in place to continue... | ||
// | ||
// * Example below: the head pointers once again match, so simply update | ||
// part 1 and record it in the `newParts` array. Last, advance both head | ||
// pointers. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, , , , 6] <- heads matched; update 1 and | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] advance both oldHead & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * As mentioned above, items that were moved as a result of being stuck | ||
// (the final else clause in the code below) are marked with null, so we | ||
// always advance old pointers over these so we're comparing the next | ||
// actual old value on either end. | ||
// | ||
// * Example below: `oldHead` is null (already placed in newParts), so | ||
// advance `oldHead`. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] // old head already used; advance | ||
// newParts: [0, 2, 1, , , , 6] // oldHead | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] | ||
// newHead ^ ^ newTail | ||
// | ||
// * Note it's not critical to mark old parts as null when they are moved | ||
// from head to tail or tail to head, since they will be outside the | ||
// pointer range and never visited again. | ||
// | ||
// * Example below: Here the old tail key matches the new head key, so | ||
// the part at the `oldTail` position and move its DOM to the new | ||
// head position (before `oldParts[oldHead]`). Last, advance `oldTail` | ||
// and `newHead` pointers. | ||
// | ||
// oldHead v v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, 4, , , 6] <- old tail matches new head: update | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] & move 4, advance oldTail & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Example below: Old and new head keys match, so update the old head | ||
// part in place, and advance the `oldHead` and `newHead` pointers. | ||
// | ||
// oldHead v oldTail | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, 4, 3, ,6] <- heads match: update 3 and advance | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] oldHead & newHead | ||
// newHead ^ ^ newTail | ||
// | ||
// * Once the new or old pointers move past each other then all we have | ||
// left is additions (if old list exhausted) or removals (if new list | ||
// exhausted). Those are handled in the final while loops at the end. | ||
// | ||
// * Example below: `oldHead` exceeded `oldTail`, so we're done with the | ||
// main loop. Create the remaining part and insert it at the new head | ||
// position, and the update is complete. | ||
// | ||
// (oldHead > oldTail) | ||
// oldKeys: [0, 1, -, 3, 4, 5, 6] | ||
// newParts: [0, 2, 1, 4, 3, 7 ,6] <- create and insert 7 | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] | ||
// newHead ^ newTail | ||
// | ||
// * Note that the order of the if/else clauses is not important to the | ||
// algorithm, as long as the null checks come first (to ensure we're | ||
// always working on valid old parts) and that the final else clause | ||
// comes last (since that's where the expensive moves occur). The | ||
// order of remaining clauses is is just a simple guess at which cases | ||
// will be most common. | ||
// | ||
// * TODO(kschaaf) Note, we could calculate the longest increasing | ||
// subsequence (LIS) of old items in new position, and only move those not | ||
// in the LIS set. However that costs O(nlogn) time and adds a bit more | ||
// code, and only helps make rare types of mutations require fewer moves. | ||
// The above handles removes, adds, reversal, swaps, and single moves of | ||
// contiguous items in linear time, in the minimum number of moves. As | ||
// the number of multiple moves where LIS might help approaches a random | ||
// shuffle, the LIS optimization becomes less helpful, so it seems not | ||
// worth the code at this point. Could reconsider if a compelling case | ||
// arises. | ||
while (oldHead <= oldTail && newHead <= newTail) { | ||
if (oldParts[oldHead] === null) { | ||
// `null` means old part at head has already been used below; skip | ||
oldHead++; | ||
} else if (oldParts[oldTail] === null) { | ||
// `null` means old part at tail has already been used below; skip | ||
oldTail--; | ||
} else if (oldKeys[oldHead] === newKeys[newHead]) { | ||
// Old head matches new head; update in place | ||
newParts[newHead] = updatePart(oldParts[oldHead]!, newValues[newHead]); | ||
oldHead++; | ||
newHead++; | ||
} else if (oldKeys[oldTail] === newKeys[newTail]) { | ||
// Old tail matches new tail; update in place | ||
newParts[newTail] = updatePart(oldParts[oldTail]!, newValues[newTail]); | ||
oldTail--; | ||
newTail--; | ||
} else if (oldKeys[oldHead] === newKeys[newTail]) { | ||
// Old head matches new tail; update and move to new tail | ||
newParts[newTail] = updatePart(oldParts[oldHead]!, newValues[newTail]); | ||
insertPartBefore( | ||
containerPart, oldParts[oldHead]!, newParts[newTail + 1]); | ||
oldHead++; | ||
newTail--; | ||
} else if (oldKeys[oldTail] === newKeys[newHead]) { | ||
// Old tail matches new head; update and move to new head | ||
newParts[newHead] = updatePart(oldParts[oldTail]!, newValues[newHead]); | ||
insertPartBefore(containerPart, oldParts[oldTail]!, oldParts[oldHead]!); | ||
oldTail--; | ||
newHead++; | ||
} else { | ||
if (newKeyToIndexMap === undefined) { | ||
// Lazily generate key-to-index maps, used for removals & moves below | ||
newKeyToIndexMap = generateMap(newKeys, newHead, newTail); | ||
oldKeyToIndexMap = generateMap(oldKeys, oldHead, oldTail); | ||
} | ||
} else if (currentMarker !== itemPart.startNode) { | ||
// Existing part in the wrong position | ||
const end = itemPart.endNode.nextSibling!; | ||
if (currentMarker !== end) { | ||
reparentNodes(container, itemPart.startNode, end, currentMarker); | ||
if (!newKeyToIndexMap.has(oldKeys[oldHead])) { | ||
// Old head is no longer in new list; remove | ||
removePart(oldParts[oldHead]!); | ||
oldHead++; | ||
} else if (!newKeyToIndexMap.has(oldKeys[oldTail])) { | ||
// Old tail is no longer in new list; remove | ||
removePart(oldParts[oldTail]!); | ||
oldTail--; | ||
} else { | ||
// Any mismatches at this point are due to additions or moves; see if | ||
// we have an old part we can reuse and move into place | ||
const oldIndex = oldKeyToIndexMap.get(newKeys[newHead]); | ||
const oldPart = oldIndex !== undefined ? oldParts[oldIndex] : null; | ||
if (oldPart === null) { | ||
// No old part for this value; create a new one and insert it | ||
const newPart = | ||
createAndInsertPart(containerPart, oldParts[oldHead]!); | ||
updatePart(newPart, newValues[newHead]); | ||
newParts[newHead] = newPart; | ||
} else { | ||
// Reuse old part | ||
newParts[newHead] = updatePart(oldPart, newValues[newHead]); | ||
insertPartBefore(containerPart, oldPart, oldParts[oldHead]!); | ||
// This marks the old part as having been used, so that it will be | ||
// skipped in the first two checks above | ||
oldParts[oldIndex as number] = null; | ||
} | ||
newHead++; | ||
} | ||
} else { | ||
// else part is in the correct position already | ||
currentMarker = itemPart.endNode.nextSibling!; | ||
} | ||
itemPart.setValue(result); | ||
itemPart.commit(); | ||
} | ||
// Cleanup | ||
if (currentMarker !== part.endNode) { | ||
removeNodes(container, currentMarker, part.endNode); | ||
keyMap.forEach(cleanMap); | ||
// Add parts for any remaining new values | ||
while (newHead <= newTail) { | ||
// For all remaining additions, we insert before last new tail, | ||
// since old pointers are no longer valid | ||
const newPart = | ||
createAndInsertPart(containerPart, newParts[newTail + 1]!); | ||
updatePart(newPart, newValues[newHead]); | ||
newParts[newHead++] = newPart; | ||
} | ||
// Remove any remaining unused old parts | ||
while (oldHead <= oldTail) { | ||
const oldPart = oldParts[oldHead++]; | ||
if (oldPart !== null) { | ||
removePart(oldPart); | ||
} | ||
} | ||
// Save order of new parts for next round | ||
partListCache.set(containerPart, newParts); | ||
keyListCache.set(containerPart, newKeys); | ||
}); | ||
} |
@@ -37,2 +37,3 @@ /** | ||
* | ||
* ``` | ||
* let checked = false; | ||
@@ -44,2 +45,3 @@ * | ||
* ` | ||
* ``` | ||
* | ||
@@ -61,4 +63,4 @@ * @param condition the condition to test truthiness against | ||
cache = { | ||
truePart: new NodePart(parentPart.templateFactory), | ||
falsePart: new NodePart(parentPart.templateFactory), | ||
truePart: new NodePart(parentPart.options), | ||
falsePart: new NodePart(parentPart.options), | ||
cacheContainer: document.createDocumentFragment(), | ||
@@ -65,0 +67,0 @@ }; |
@@ -15,6 +15,6 @@ /** | ||
import {TemplateFactory} from '../lit-html.js'; | ||
import {Part} from './part.js'; | ||
import {AttributeCommitter, BooleanAttributePart, EventPart, NodePart, PropertyCommitter} from './parts.js'; | ||
import {RenderOptions} from './render-options.js'; | ||
import {TemplateProcessor} from './template-processor.js'; | ||
@@ -24,3 +24,3 @@ /** | ||
*/ | ||
export class DefaultTemplateProcessor { | ||
export class DefaultTemplateProcessor implements TemplateProcessor { | ||
/** | ||
@@ -35,4 +35,5 @@ * Create parts for an attribute-position binding, given the event, attribute | ||
*/ | ||
handleAttributeExpressions(element: Element, name: string, strings: string[]): | ||
Part[] { | ||
handleAttributeExpressions( | ||
element: Element, name: string, strings: string[], | ||
options: RenderOptions): Part[] { | ||
const prefix = name[0]; | ||
@@ -44,3 +45,3 @@ if (prefix === '.') { | ||
if (prefix === '@') { | ||
return [new EventPart(element, name.slice(1))]; | ||
return [new EventPart(element, name.slice(1), options.eventContext)]; | ||
} | ||
@@ -57,4 +58,4 @@ if (prefix === '?') { | ||
*/ | ||
handleTextExpression(templateFactory: TemplateFactory) { | ||
return new NodePart(templateFactory); | ||
handleTextExpression(options: RenderOptions) { | ||
return new NodePart(options); | ||
} | ||
@@ -61,0 +62,0 @@ } |
@@ -18,3 +18,3 @@ /** | ||
import {noChange, Part} from './part.js'; | ||
import {TemplateFactory} from './template-factory.js'; | ||
import {RenderOptions} from './render-options.js'; | ||
import {TemplateInstance} from './template-instance.js'; | ||
@@ -123,3 +123,3 @@ import {TemplateResult} from './template-result.js'; | ||
export class NodePart implements Part { | ||
templateFactory: TemplateFactory; | ||
options: RenderOptions; | ||
startNode!: Node; | ||
@@ -130,4 +130,4 @@ endNode!: Node; | ||
constructor(templateFactory: TemplateFactory) { | ||
this.templateFactory = templateFactory; | ||
constructor(options: RenderOptions) { | ||
this.options = options; | ||
} | ||
@@ -240,3 +240,3 @@ | ||
private _commitTemplateResult(value: TemplateResult): void { | ||
const template = this.templateFactory(value); | ||
const template = this.options.templateFactory(value); | ||
if (this.value && this.value.template === template) { | ||
@@ -246,6 +246,7 @@ this.value.update(value.values); | ||
// Make sure we propagate the template processor from the TemplateResult | ||
// so that we use it's syntax extension, etc. The template factory comes | ||
// from the render function so that it can control caching. | ||
// so that we use its syntax extension, etc. The template factory comes | ||
// from the render function options so that it can control template | ||
// caching and preprocessing. | ||
const instance = | ||
new TemplateInstance(template, value.processor, this.templateFactory); | ||
new TemplateInstance(template, value.processor, this.options); | ||
const fragment = instance._clone(); | ||
@@ -286,3 +287,3 @@ instance.update(value.values); | ||
if (itemPart === undefined) { | ||
itemPart = new NodePart(this.templateFactory); | ||
itemPart = new NodePart(this.options); | ||
itemParts.push(itemPart); | ||
@@ -412,11 +413,40 @@ if (partIndex === 0) { | ||
declare global { | ||
interface EventListenerOptions { | ||
capture?: boolean; | ||
once?: boolean; | ||
passive?: boolean; | ||
} | ||
} | ||
// Detect event listener options support. If the `capture` property is read | ||
// from the options object, then options are supported. If not, then the thrid | ||
// argument to add/removeEventListener is interpreted as the boolean capture | ||
// value so we should only pass the `capture` property. | ||
let eventOptionsSupported = false; | ||
try { | ||
const options = { | ||
get capture() { | ||
eventOptionsSupported = true; | ||
return false; | ||
} | ||
}; | ||
window.addEventListener('test', options as any, options); | ||
window.removeEventListener('test', options as any, options); | ||
} catch (_e) { | ||
} | ||
export class EventPart implements Part { | ||
element: Element; | ||
eventName: string; | ||
eventContext?: EventTarget; | ||
value: any = undefined; | ||
_options?: {capture?: boolean, passive?: boolean, once?: boolean}; | ||
_pendingValue: any = undefined; | ||
constructor(element: Element, eventName: string) { | ||
constructor(element: Element, eventName: string, eventContext?: EventTarget) { | ||
this.element = element; | ||
this.eventName = eventName; | ||
this.eventContext = eventContext; | ||
} | ||
@@ -437,10 +467,21 @@ | ||
} | ||
if ((this._pendingValue == null) !== (this.value == null)) { | ||
if (this._pendingValue == null) { | ||
this.element.removeEventListener(this.eventName, this); | ||
} else { | ||
this.element.addEventListener(this.eventName, this); | ||
} | ||
const newListener = this._pendingValue; | ||
const oldListener = this.value; | ||
const shouldRemoveListener = newListener == null || | ||
oldListener != null && | ||
(newListener.capture !== oldListener.capture || | ||
newListener.once !== oldListener.once || | ||
newListener.passive !== oldListener.passive); | ||
const shouldAddListener = | ||
newListener != null && (oldListener == null || shouldRemoveListener); | ||
if (shouldRemoveListener) { | ||
this.element.removeEventListener(this.eventName, this, this._options); | ||
} | ||
this.value = this._pendingValue; | ||
this._options = getOptions(newListener); | ||
if (shouldAddListener) { | ||
this.element.addEventListener(this.eventName, this, this._options); | ||
} | ||
this.value = newListener; | ||
this._pendingValue = noChange; | ||
@@ -450,8 +491,17 @@ } | ||
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); | ||
} | ||
const listener = (typeof this.value === 'function') ? | ||
this.value : | ||
(typeof this.value.handleEvent === 'function') ? | ||
this.value.handleEvent : | ||
() => null; | ||
listener.call(this.eventContext || this.element, event); | ||
} | ||
} | ||
// We copy options because of the inconsistent behavior of browsers when reading | ||
// the third argument of add/removeEventListener. IE11 doesn't support options | ||
// at all. Chrome 41 only reads `capture` if the argument is an object. | ||
const getOptions = (o: any) => o && | ||
(eventOptionsSupported ? | ||
{capture: o.capture, passive: o.passive, once: o.once} : | ||
o.capture); |
@@ -17,3 +17,4 @@ /** | ||
import {NodePart} from './parts.js'; | ||
import {templateFactory as defaultTemplateFactory, TemplateFactory} from './template-factory.js'; | ||
import {RenderOptions} from './render-options.js'; | ||
import {templateFactory} from './template-factory.js'; | ||
import {TemplateResult} from './template-result.js'; | ||
@@ -34,17 +35,21 @@ | ||
* rendered there. | ||
* @param templateFactory a function to create a Template or retreive one from | ||
* cache. | ||
* @param options RenderOptions for the entire render tree rendered to this | ||
* container. Render options must *not* change between renders to the same | ||
* container, as those changes will not effect previously rendered DOM. | ||
*/ | ||
export function render( | ||
result: TemplateResult, | ||
container: Element|DocumentFragment, | ||
templateFactory: TemplateFactory = defaultTemplateFactory) { | ||
let part = parts.get(container); | ||
if (part === undefined) { | ||
removeNodes(container, container.firstChild); | ||
parts.set(container, part = new NodePart(templateFactory)); | ||
part.appendInto(container); | ||
} | ||
part.setValue(result); | ||
part.commit(); | ||
} | ||
export const render = | ||
(result: TemplateResult, | ||
container: Element|DocumentFragment, | ||
options?: Partial<RenderOptions>) => { | ||
let part = parts.get(container); | ||
if (part === undefined) { | ||
removeNodes(container, container.firstChild); | ||
parts.set(container, part = new NodePart({ | ||
templateFactory, | ||
...options, | ||
})); | ||
part.appendInto(container); | ||
} | ||
part.setValue(result); | ||
part.commit(); | ||
}; |
@@ -16,2 +16,3 @@ /** | ||
import {insertNodeIntoTemplate, removeNodesFromTemplate} from './modify-template.js'; | ||
import {RenderOptions} from './render-options.js'; | ||
import {parts, render as litRender} from './render.js'; | ||
@@ -78,3 +79,3 @@ import {templateCaches} from './template-factory.js'; | ||
*/ | ||
function removeStylesFromLitTemplates(scopeName: string) { | ||
const removeStylesFromLitTemplates = (scopeName: string) => { | ||
TEMPLATE_TYPES.forEach((type) => { | ||
@@ -94,3 +95,3 @@ const templates = templateCaches.get(getTemplateCacheKey(type, scopeName)); | ||
}); | ||
} | ||
}; | ||
@@ -163,23 +164,31 @@ const shadyRenderSet = new Set<string>(); | ||
export function render( | ||
result: TemplateResult, | ||
container: Element|DocumentFragment, | ||
scopeName: string) { | ||
const hasRendered = parts.has(container); | ||
litRender(result, container, shadyTemplateFactory(scopeName)); | ||
// When rendering a TemplateResult, scope the template with ShadyCSS | ||
if (container instanceof ShadowRoot && compatibleShadyCSSVersion && | ||
result instanceof TemplateResult) { | ||
// Scope the element template one time only for this scope. | ||
if (!shadyRenderSet.has(scopeName)) { | ||
const part = parts.get(container)!; | ||
const instance = part.value as TemplateInstance; | ||
prepareTemplateStyles( | ||
(container as ShadowRoot), instance.template, scopeName); | ||
} | ||
// Update styling if this is the initial render to this container. | ||
if (!hasRendered) { | ||
window.ShadyCSS.styleElement((container as ShadowRoot).host); | ||
} | ||
} | ||
export interface ShadyRenderOptions extends Partial<RenderOptions> { | ||
scopeName: string; | ||
} | ||
export const render = | ||
(result: TemplateResult, | ||
container: Element|DocumentFragment, | ||
options: ShadyRenderOptions) => { | ||
const scopeName = options.scopeName; | ||
const hasRendered = parts.has(container); | ||
litRender(result, container, { | ||
templateFactory: shadyTemplateFactory(scopeName), | ||
...options, | ||
} as RenderOptions); | ||
// When rendering a TemplateResult, scope the template with ShadyCSS | ||
if (container instanceof ShadowRoot && compatibleShadyCSSVersion && | ||
result instanceof TemplateResult) { | ||
// Scope the element template one time only for this scope. | ||
if (!shadyRenderSet.has(scopeName)) { | ||
const part = parts.get(container)!; | ||
const instance = part.value as TemplateInstance; | ||
prepareTemplateStyles( | ||
(container as ShadowRoot), instance.template, scopeName); | ||
} | ||
// Update styling if this is the initial render to this container. | ||
if (!hasRendered) { | ||
window.ShadyCSS.styleElement((container as ShadowRoot).host); | ||
} | ||
} | ||
}; |
@@ -18,3 +18,4 @@ /** | ||
import {Part} from './part.js'; | ||
import {TemplateFactory} from './template-factory.js'; | ||
import {RenderOptions} from './render-options.js'; | ||
// import {TemplateFactory} from './template-factory.js'; | ||
import {TemplateProcessor} from './template-processor.js'; | ||
@@ -30,3 +31,3 @@ import {isTemplatePartActive, Template} from './template.js'; | ||
processor: TemplateProcessor; | ||
_getTemplate: TemplateFactory; | ||
options: RenderOptions; | ||
template: Template; | ||
@@ -36,6 +37,6 @@ | ||
template: Template, processor: TemplateProcessor, | ||
getTemplate: TemplateFactory) { | ||
options: RenderOptions) { | ||
this.template = template; | ||
this.processor = processor; | ||
this._getTemplate = getTemplate; | ||
this.options = options; | ||
} | ||
@@ -94,3 +95,3 @@ | ||
if (part.type === 'node') { | ||
const part = this.processor.handleTextExpression(this._getTemplate); | ||
const part = this.processor.handleTextExpression(this.options); | ||
part.insertAfterNode(node); | ||
@@ -100,3 +101,3 @@ this._parts.push(part); | ||
this._parts.push(...this.processor.handleAttributeExpressions( | ||
node as Element, part.name, part.strings)); | ||
node as Element, part.name, part.strings, this.options)); | ||
} | ||
@@ -121,7 +122,1 @@ partIndex++; | ||
} | ||
declare global { | ||
class CustomElementRegistry { | ||
upgrade(node: Node): void; | ||
} | ||
} |
@@ -17,3 +17,3 @@ /** | ||
import {NodePart} from './parts.js'; | ||
import {TemplateFactory} from './template-factory.js'; | ||
import {RenderOptions} from './render-options.js'; | ||
@@ -30,10 +30,11 @@ export interface TemplateProcessor { | ||
*/ | ||
handleAttributeExpressions(element: Element, name: string, strings: string[]): | ||
Part[]; | ||
handleAttributeExpressions( | ||
element: Element, name: string, strings: string[], | ||
options: RenderOptions): Part[]; | ||
/** | ||
* Create parts for a text-position binding. | ||
* @param templateFactory | ||
* @param partOptions | ||
*/ | ||
handleTextExpression(templateFactory: TemplateFactory): NodePart; | ||
handleTextExpression(options: RenderOptions): NodePart; | ||
} |
@@ -39,3 +39,3 @@ /** | ||
/** | ||
* Returns a string of HTML used to create a <template> element. | ||
* Returns a string of HTML used to create a `<template>` element. | ||
*/ | ||
@@ -81,4 +81,4 @@ getHTML(): string { | ||
* | ||
* 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 | ||
* 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. | ||
@@ -85,0 +85,0 @@ */ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
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
413488
129
5575
53