lit-html
Advanced tools
Comparing version 0.12.0 to 0.13.0
@@ -13,3 +13,3 @@ # Change Log | ||
<!-- ## Unreleased --> | ||
## Unreleased | ||
<!-- ### Added --> | ||
@@ -20,2 +20,11 @@ <!-- ### Changed --> | ||
## [0.13.0] - 2018-11-08 | ||
### Changed | ||
* [Breaking] Directives are now defined by passing the entire directive factory function to `directive()`. ([#562](https://github.com/Polymer/lit-html/pull/562)) | ||
### Fixed | ||
* Fix issue on obscure browsers that do not accept event listener objects by using callback as event part listener ([#581](https://github.com/Polymer/lit-html/pull/581)) | ||
* Fix KeyFn and ItemTemplate types ([#570](https://github.com/Polymer/lit-html/pull/570)) | ||
* Don't use export * to workaround rollup bug ([#556](https://github.com/Polymer/lit-html/pull/556)) | ||
* `eventContext` is no longer used as the `this` value for event listener objects (object with a `handleEvent` method), as the object itself is supposed to be the `this` value. ([#576](https://github.com/Polymer/lit-html/pull/576)) | ||
## [0.12.0] - 2018-10-05 | ||
@@ -22,0 +31,0 @@ ### Changed |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { Directive, NodePart } from '../lit-html.js'; | ||
import { Part } from '../lit-html.js'; | ||
/** | ||
@@ -33,3 +33,3 @@ * A directive that renders the items of an async iterable[1], appending new | ||
*/ | ||
export declare const asyncAppend: <T>(value: AsyncIterable<T>, mapper?: ((v: T, index?: number | undefined) => any) | undefined) => Directive<NodePart>; | ||
export declare const asyncAppend: <T>(value: AsyncIterable<T>, mapper?: ((v: T, index?: number | undefined) => any) | undefined) => (part: Part) => Promise<void>; | ||
//# sourceMappingURL=async-append.d.ts.map |
@@ -39,4 +39,7 @@ /** | ||
*/ | ||
export const asyncAppend = (value, mapper) => directive(async (part) => { | ||
export const asyncAppend = directive((value, mapper) => async (part) => { | ||
var e_1, _a; | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('asyncAppend can only be used in text bindings'); | ||
} | ||
// If we've already set up this particular iterable, we don't need | ||
@@ -55,4 +58,4 @@ // to do anything. | ||
let v = value_1_1.value; | ||
// When we get the first value, clear the part. This lets the previous | ||
// value display until we can replace it. | ||
// When we get the first value, clear the part. This lets the | ||
// previous value display until we can replace it. | ||
if (i === 0) { | ||
@@ -59,0 +62,0 @@ part.clear(); |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { Directive, NodePart } from '../lit-html.js'; | ||
import { Part } from '../lit-html.js'; | ||
/** | ||
@@ -34,3 +34,3 @@ * A directive that renders the items of an async iterable[1], replacing | ||
*/ | ||
export declare const asyncReplace: <T>(value: AsyncIterable<T>, mapper?: ((v: T, index?: number | undefined) => any) | undefined) => Directive<NodePart>; | ||
export declare const asyncReplace: <T>(value: AsyncIterable<T>, mapper?: ((v: T, index?: number | undefined) => any) | undefined) => (part: Part) => Promise<void>; | ||
//# sourceMappingURL=async-replace.d.ts.map |
@@ -40,4 +40,7 @@ /** | ||
*/ | ||
export const asyncReplace = (value, mapper) => directive(async (part) => { | ||
export const asyncReplace = directive((value, mapper) => async (part) => { | ||
var e_1, _a; | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('asyncReplace can only be used in text bindings'); | ||
} | ||
// If we've already set up this particular iterable, we don't need | ||
@@ -44,0 +47,0 @@ // to do anything. |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { AttributePart, Directive } from '../lit-html.js'; | ||
import { Part } from '../lit-html.js'; | ||
export interface ClassInfo { | ||
@@ -29,3 +29,3 @@ [name: string]: string | boolean | number; | ||
*/ | ||
export declare const classMap: (classInfo: ClassInfo) => Directive<AttributePart>; | ||
export declare const classMap: (classInfo: ClassInfo) => (part: Part) => void; | ||
//# sourceMappingURL=classMap.d.ts.map |
@@ -50,3 +50,3 @@ /** | ||
*/ | ||
export const classMap = (classInfo) => directive((part) => { | ||
export const classMap = directive((classInfo) => (part) => { | ||
if (!(part instanceof AttributePart) || (part instanceof PropertyPart) || | ||
@@ -53,0 +53,0 @@ part.committer.name !== 'class' || part.committer.parts.length > 1) { |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { Directive, NodePart } from '../lit-html.js'; | ||
import { Part } from '../lit-html.js'; | ||
/** | ||
@@ -35,3 +35,3 @@ * Creates a guard directive. Prevents any re-render until the identity of the | ||
*/ | ||
export declare const guard: (expression: any, valueFn: () => any) => Directive<NodePart>; | ||
export declare const guard: (expression: any, valueFn: () => any) => (part: Part) => void; | ||
//# sourceMappingURL=guard.d.ts.map |
@@ -35,3 +35,3 @@ /** | ||
*/ | ||
export const guard = (expression, valueFn) => directive((part) => { | ||
export const guard = directive((expression, valueFn) => (part) => { | ||
// Dirty check previous expression | ||
@@ -38,0 +38,0 @@ if (previousExpressions.get(part) === expression) { |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { Directive, Part } from '../lit-html.js'; | ||
import { Part } from '../lit-html.js'; | ||
/** | ||
@@ -22,3 +22,3 @@ * For AttributeParts, sets the attribute if the value is defined and removes | ||
*/ | ||
export declare const ifDefined: (value: any) => Directive<Part>; | ||
export declare const ifDefined: (value: any) => (part: Part) => void; | ||
//# sourceMappingURL=if-defined.d.ts.map |
@@ -21,3 +21,3 @@ /** | ||
*/ | ||
export const ifDefined = (value) => directive((part) => { | ||
export const ifDefined = directive((value) => (part) => { | ||
if (value === undefined && part instanceof AttributePart) { | ||
@@ -24,0 +24,0 @@ if (value !== part.value) { |
@@ -14,5 +14,5 @@ /** | ||
*/ | ||
import { Directive, NodePart } from '../lit-html.js'; | ||
export declare type KeyFn<T> = (item: T, index?: number) => any; | ||
export declare type ItemTemplate<T> = (item: T, index?: number) => any; | ||
import { DirectiveFn } from '../lib/directive.js'; | ||
export declare type KeyFn<T> = (item: T, index: number) => any; | ||
export declare type ItemTemplate<T> = (item: T, index: number) => any; | ||
/** | ||
@@ -30,5 +30,5 @@ * A directive that repeats a series of values (usually `TemplateResults`) | ||
* | ||
* 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. | ||
* IMPORTANT: If providing a `keyFn`, keys *must* be unique for all items in a | ||
* given call to `repeat`. The behavior when two or more items have the same key | ||
* is undefined. | ||
* | ||
@@ -38,4 +38,3 @@ * If no `keyFn` is provided, this directive will perform similar to mapping | ||
*/ | ||
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>; | ||
export declare const repeat: <T>(items: Iterable<T>, keyFnOrTemplate: KeyFn<T> | ItemTemplate<T>, template?: ItemTemplate<T> | undefined) => DirectiveFn; | ||
//# sourceMappingURL=repeat.d.ts.map |
@@ -56,18 +56,40 @@ /** | ||
const keyListCache = new WeakMap(); | ||
export function repeat(items, keyFnOrTemplate, template) { | ||
/** | ||
* 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 two or more items have the same key | ||
* 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 const repeat = directive((items, keyFnOrTemplate, template) => { | ||
let keyFn; | ||
if (arguments.length === 2) { | ||
if (template === undefined) { | ||
template = keyFnOrTemplate; | ||
} | ||
else if (arguments.length === 3) { | ||
else if (keyFnOrTemplate !== undefined) { | ||
keyFn = keyFnOrTemplate; | ||
} | ||
return directive((containerPart) => { | ||
// Old part & key lists are retrieved from the last update (associated with | ||
// the part for this instance of the directive) | ||
return (containerPart) => { | ||
if (!(containerPart instanceof NodePart)) { | ||
throw new Error('repeat can only be used in text bindings'); | ||
} | ||
// 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. | ||
// 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 = []; | ||
@@ -85,5 +107,5 @@ // New value list is eagerly generated from items along with a parallel | ||
// 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. | ||
// 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; | ||
@@ -102,7 +124,7 @@ let oldKeyToIndexMap; | ||
// 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): | ||
// 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): | ||
// | ||
@@ -112,13 +134,15 @@ // oldHead v v oldTail | ||
// newParts: [ , , , , , , ] | ||
// newKeys: [0, 2, 1, 4, 3, 7, 6] <- reflects the user's new item order | ||
// 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. | ||
// 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). | ||
// * 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). | ||
// | ||
@@ -132,4 +156,5 @@ // oldHead v v oldTail | ||
// * 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. | ||
// 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. | ||
// | ||
@@ -142,3 +167,4 @@ // oldHead v v oldTail | ||
// | ||
// * If neither head nor tail match; next check if one of the old head/tail | ||
// * 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 | ||
@@ -148,9 +174,10 @@ // keys to index (`newKeyToIndexMap`), which is done once lazily as a | ||
// 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. | ||
// 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. | ||
// `newKeyToIndexMap`), so remove that part from the DOM and advance | ||
// just the `oldTail` pointer. | ||
// | ||
@@ -163,36 +190,41 @@ // oldHead v v oldTail | ||
// | ||
// * Once head and tail cannot move, any mismatches are due to either new or | ||
// * 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. | ||
// 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. | ||
// * 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 | ||
// 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. | ||
// * 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 | ||
// * 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. | ||
// part 1 and record it in the `newParts` array. Last, advance both | ||
// head pointers. | ||
// | ||
@@ -205,6 +237,7 @@ // oldHead v v oldTail | ||
// | ||
// * 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. | ||
// * 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. | ||
// | ||
@@ -220,3 +253,4 @@ // * Example below: `oldHead` is null (already placed in newParts), so | ||
// | ||
// * Note it's not critical to mark old parts as null when they are moved | ||
// * 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 | ||
@@ -232,4 +266,5 @@ // pointer range and never visited again. | ||
// 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 | ||
// 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 | ||
@@ -242,4 +277,4 @@ // | ||
// 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 | ||
// 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 | ||
@@ -269,11 +304,11 @@ // | ||
// * 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. | ||
// 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) { | ||
@@ -290,3 +325,4 @@ if (oldParts[oldHead] === null) { | ||
// Old head matches new head; update in place | ||
newParts[newHead] = updatePart(oldParts[oldHead], newValues[newHead]); | ||
newParts[newHead] = | ||
updatePart(oldParts[oldHead], newValues[newHead]); | ||
oldHead++; | ||
@@ -297,3 +333,4 @@ newHead++; | ||
// Old tail matches new tail; update in place | ||
newParts[newTail] = updatePart(oldParts[oldTail], newValues[newTail]); | ||
newParts[newTail] = | ||
updatePart(oldParts[oldTail], newValues[newTail]); | ||
oldTail--; | ||
@@ -304,3 +341,4 @@ newTail--; | ||
// Old head matches new tail; update and move to new tail | ||
newParts[newTail] = updatePart(oldParts[oldHead], newValues[newTail]); | ||
newParts[newTail] = | ||
updatePart(oldParts[oldHead], newValues[newTail]); | ||
insertPartBefore(containerPart, oldParts[oldHead], newParts[newTail + 1]); | ||
@@ -312,3 +350,4 @@ oldHead++; | ||
// Old tail matches new head; update and move to new head | ||
newParts[newHead] = updatePart(oldParts[oldTail], newValues[newHead]); | ||
newParts[newHead] = | ||
updatePart(oldParts[oldTail], newValues[newHead]); | ||
insertPartBefore(containerPart, oldParts[oldTail], oldParts[oldHead]); | ||
@@ -320,3 +359,4 @@ oldTail--; | ||
if (newKeyToIndexMap === undefined) { | ||
// Lazily generate key-to-index maps, used for removals & moves below | ||
// Lazily generate key-to-index maps, used for removals & moves | ||
// below | ||
newKeyToIndexMap = generateMap(newKeys, newHead, newTail); | ||
@@ -336,4 +376,4 @@ oldKeyToIndexMap = generateMap(oldKeys, oldHead, 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 | ||
// 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]); | ||
@@ -351,4 +391,4 @@ const oldPart = oldIndex !== undefined ? oldParts[oldIndex] : null; | ||
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 | ||
// This marks the old part as having been used, so that it will | ||
// be skipped in the first two checks above | ||
oldParts[oldIndex] = null; | ||
@@ -378,4 +418,4 @@ } | ||
keyListCache.set(containerPart, newKeys); | ||
}); | ||
} | ||
}; | ||
}); | ||
//# sourceMappingURL=repeat.js.map |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { AttributePart, Directive } from '../lit-html.js'; | ||
import { Part } from '../lit-html.js'; | ||
export interface StyleInfo { | ||
@@ -27,3 +27,3 @@ [name: string]: string; | ||
*/ | ||
export declare const styleMap: (styleInfo: StyleInfo) => Directive<AttributePart>; | ||
export declare const styleMap: (styleInfo: StyleInfo) => (part: Part) => void; | ||
//# sourceMappingURL=styleMap.d.ts.map |
@@ -35,3 +35,3 @@ /** | ||
*/ | ||
export const styleMap = (styleInfo) => directive((part) => { | ||
export const styleMap = directive((styleInfo) => (part) => { | ||
if (!(part instanceof AttributePart) || (part instanceof PropertyPart) || | ||
@@ -38,0 +38,0 @@ part.committer.name !== 'style' || part.committer.parts.length > 1) { |
@@ -14,4 +14,4 @@ /** | ||
*/ | ||
import { Directive, NodePart } from '../lit-html.js'; | ||
export declare const unsafeHTML: (value: any) => Directive<NodePart>; | ||
import { Part } from '../lit-html.js'; | ||
export declare const unsafeHTML: (value: any) => (part: Part) => void; | ||
//# sourceMappingURL=unsafe-html.d.ts.map |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { directive, isPrimitive } from '../lit-html.js'; | ||
import { directive, isPrimitive, NodePart } from '../lit-html.js'; | ||
/** | ||
@@ -24,3 +24,6 @@ * Renders the result as HTML, rather than text. | ||
const previousValues = new WeakMap(); | ||
export const unsafeHTML = (value) => directive((part) => { | ||
export const unsafeHTML = directive((value) => (part) => { | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('unsafeHTML can only be used in text bindings'); | ||
} | ||
// Dirty check primitive values | ||
@@ -27,0 +30,0 @@ const previousValue = previousValues.get(part); |
@@ -14,7 +14,7 @@ /** | ||
*/ | ||
import { Directive, NodePart } from '../lit-html.js'; | ||
import { Part } from '../lit-html.js'; | ||
/** | ||
* Display `defaultContent` until `promise` resolves. | ||
*/ | ||
export declare const until: (promise: Promise<any>, defaultContent: any) => Directive<NodePart>; | ||
export declare const until: (promise: Promise<any>, defaultContent: any) => (part: Part) => void; | ||
//# sourceMappingURL=until.d.ts.map |
@@ -18,3 +18,3 @@ /** | ||
*/ | ||
export const until = (promise, defaultContent) => directive((part) => { | ||
export const until = directive((promise, defaultContent) => (part) => { | ||
part.setValue(defaultContent); | ||
@@ -21,0 +21,0 @@ part.commit(); |
@@ -14,3 +14,3 @@ /** | ||
*/ | ||
import { Directive, NodePart } from '../lit-html.js'; | ||
import { Part } from '../lit-html.js'; | ||
/** | ||
@@ -40,3 +40,3 @@ * Efficiently switches between two templates based on the given condition. The | ||
*/ | ||
export declare const when: (condition: any, trueValue: () => any, falseValue: () => any) => Directive<NodePart>; | ||
export declare const when: (condition: any, trueValue: () => any, falseValue: () => any) => (parentPart: Part) => void; | ||
//# sourceMappingURL=when.d.ts.map |
@@ -40,3 +40,6 @@ /** | ||
*/ | ||
export const when = (condition, trueValue, falseValue) => directive((parentPart) => { | ||
export const when = directive((condition, trueValue, falseValue) => (parentPart) => { | ||
if (!(parentPart instanceof NodePart)) { | ||
throw new Error('when can only be used in text bindings'); | ||
} | ||
let cache = partCaches.get(parentPart); | ||
@@ -43,0 +46,0 @@ // Create a new cache if this is the first render |
@@ -15,7 +15,25 @@ /** | ||
import { Part } from './part.js'; | ||
export interface Directive<P = Part> { | ||
(part: P): void; | ||
} | ||
export declare const directive: <P = Part>(f: Directive<P>) => Directive<P>; | ||
export declare type DirectiveFn = (part: Part) => void; | ||
/** | ||
* Brands a function as a directive so that lit-html will call the function | ||
* during template rendering, rather than passing as a value. | ||
* | ||
* @param f The directive factory function. Must be a function that returns a | ||
* function of the signature `(part: Part) => void`. The returned function will | ||
* be called with the part object | ||
* | ||
* @example | ||
* | ||
* ``` | ||
* import {directive, html} from 'lit-html'; | ||
* | ||
* const immutable = directive((v) => (part) => { | ||
* if (part.value !== v) { | ||
* part.setValue(v) | ||
* } | ||
* }); | ||
* ``` | ||
*/ | ||
export declare const directive: <F extends Function>(f: F) => F; | ||
export declare const isDirective: (o: any) => boolean; | ||
//# sourceMappingURL=directive.d.ts.map |
@@ -15,7 +15,28 @@ /** | ||
const directives = new WeakMap(); | ||
export const directive = (f) => { | ||
directives.set(f, true); | ||
return f; | ||
}; | ||
/** | ||
* Brands a function as a directive so that lit-html will call the function | ||
* during template rendering, rather than passing as a value. | ||
* | ||
* @param f The directive factory function. Must be a function that returns a | ||
* function of the signature `(part: Part) => void`. The returned function will | ||
* be called with the part object | ||
* | ||
* @example | ||
* | ||
* ``` | ||
* import {directive, html} from 'lit-html'; | ||
* | ||
* const immutable = directive((v) => (part) => { | ||
* if (part.value !== v) { | ||
* part.setValue(v) | ||
* } | ||
* }); | ||
* ``` | ||
*/ | ||
export const directive = (f) => ((...args) => { | ||
const d = f(...args); | ||
directives.set(d, true); | ||
return d; | ||
}); | ||
export const isDirective = (o) => typeof o === 'function' && directives.has(o); | ||
//# sourceMappingURL=directive.js.map |
@@ -120,9 +120,2 @@ /** | ||
} | ||
declare global { | ||
interface EventListenerOptions { | ||
capture?: boolean; | ||
once?: boolean; | ||
passive?: boolean; | ||
} | ||
} | ||
export declare class EventPart implements Part { | ||
@@ -133,8 +126,5 @@ element: Element; | ||
value: any; | ||
_options?: { | ||
capture?: boolean; | ||
passive?: boolean; | ||
once?: boolean; | ||
}; | ||
_options?: AddEventListenerOptions; | ||
_pendingValue: any; | ||
_boundHandleEvent: (event: Event) => void; | ||
constructor(element: Element, eventName: string, eventContext?: EventTarget); | ||
@@ -141,0 +131,0 @@ setValue(value: any): void; |
@@ -380,2 +380,3 @@ /** | ||
this.eventContext = eventContext; | ||
this._boundHandleEvent = (e) => this.handleEvent(e); | ||
} | ||
@@ -403,7 +404,7 @@ setValue(value) { | ||
if (shouldRemoveListener) { | ||
this.element.removeEventListener(this.eventName, this, this._options); | ||
this.element.removeEventListener(this.eventName, this._boundHandleEvent, this._options); | ||
} | ||
this._options = getOptions(newListener); | ||
if (shouldAddListener) { | ||
this.element.addEventListener(this.eventName, this, this._options); | ||
this.element.addEventListener(this.eventName, this._boundHandleEvent, this._options); | ||
} | ||
@@ -414,8 +415,8 @@ this.value = newListener; | ||
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); | ||
if (typeof this.value === 'function') { | ||
this.value.call(this.eventContext || this.element, event); | ||
} | ||
else { | ||
this.value.handleEvent(event); | ||
} | ||
} | ||
@@ -422,0 +423,0 @@ } |
@@ -17,9 +17,2 @@ /** | ||
export { html, svg, TemplateResult } from '../lit-html.js'; | ||
declare global { | ||
interface Window { | ||
ShadyCSS: any; | ||
} | ||
class ShadowRoot { | ||
} | ||
} | ||
export interface ShadyRenderOptions extends Partial<RenderOptions> { | ||
@@ -26,0 +19,0 @@ scopeName: string; |
@@ -15,13 +15,12 @@ /** | ||
import { SVGTemplateResult, TemplateResult } from './lib/template-result.js'; | ||
export * from './lib/template-result.js'; | ||
export * from './lib/template.js'; | ||
export * from './lib/template-processor.js'; | ||
export * from './lib/default-template-processor.js'; | ||
export * from './lib/template-instance.js'; | ||
export * from './lib/part.js'; | ||
export * from './lib/parts.js'; | ||
export * from './lib/dom.js'; | ||
export * from './lib/directive.js'; | ||
export * from './lib/render.js'; | ||
export * from './lib/template-factory.js'; | ||
export { DefaultTemplateProcessor, defaultTemplateProcessor } from './lib/default-template-processor.js'; | ||
export { directive, DirectiveFn, isDirective } from './lib/directive.js'; | ||
export { removeNodes, reparentNodes } from './lib/dom.js'; | ||
export { noChange, Part } from './lib/part.js'; | ||
export { AttributeCommitter, AttributePart, BooleanAttributePart, EventPart, isPrimitive, NodePart, PropertyCommitter, PropertyPart } from './lib/parts.js'; | ||
export { parts, render } from './lib/render.js'; | ||
export { templateCaches, templateFactory } from './lib/template-factory.js'; | ||
export { TemplateInstance } from './lib/template-instance.js'; | ||
export { SVGTemplateResult, TemplateResult } from './lib/template-result.js'; | ||
export { createMarker, isTemplatePartActive, Template } from './lib/template.js'; | ||
/** | ||
@@ -28,0 +27,0 @@ * Interprets a template literal as an HTML template that can efficiently |
@@ -16,12 +16,13 @@ /** | ||
import { SVGTemplateResult, TemplateResult } from './lib/template-result.js'; | ||
export * from './lib/template-result.js'; | ||
export * from './lib/template.js'; | ||
export * from './lib/default-template-processor.js'; | ||
export * from './lib/template-instance.js'; | ||
export * from './lib/part.js'; | ||
export * from './lib/parts.js'; | ||
export * from './lib/dom.js'; | ||
export * from './lib/directive.js'; | ||
export * from './lib/render.js'; | ||
export * from './lib/template-factory.js'; | ||
export { DefaultTemplateProcessor, defaultTemplateProcessor } from './lib/default-template-processor.js'; | ||
export { directive, isDirective } from './lib/directive.js'; | ||
// TODO(justinfagnani): remove line when we get NodePart moving methods | ||
export { removeNodes, reparentNodes } from './lib/dom.js'; | ||
export { noChange } from './lib/part.js'; | ||
export { AttributeCommitter, AttributePart, BooleanAttributePart, EventPart, isPrimitive, NodePart, PropertyCommitter, PropertyPart } from './lib/parts.js'; | ||
export { parts, render } from './lib/render.js'; | ||
export { templateCaches, templateFactory } from './lib/template-factory.js'; | ||
export { TemplateInstance } from './lib/template-instance.js'; | ||
export { SVGTemplateResult, TemplateResult } from './lib/template-result.js'; | ||
export { createMarker, isTemplatePartActive, Template } from './lib/template.js'; | ||
/** | ||
@@ -28,0 +29,0 @@ * Interprets a template literal as an HTML template that can efficiently |
{ | ||
"name": "lit-html", | ||
"version": "0.12.0", | ||
"version": "0.13.0", | ||
"description": "HTML template literals in JavaScript", | ||
@@ -36,2 +36,3 @@ "license": "BSD-3-Clause", | ||
"@types/mocha": "^5.2.0", | ||
"@webcomponents/shadycss": "^1.5.2", | ||
"@webcomponents/webcomponentsjs": "^2.0.4", | ||
@@ -48,4 +49,4 @@ "chai": "^4.1.2", | ||
"uglify-es": "^3.3.5", | ||
"wct-browser-legacy": "^1.0.1", | ||
"web-component-tester": "^6.8.0" | ||
"wct-mocha": "^1.0.0", | ||
"web-component-tester": "^6.9.0" | ||
}, | ||
@@ -52,0 +53,0 @@ "typings": "lit-html.d.ts", |
@@ -15,3 +15,3 @@ /** | ||
import {createMarker, directive, Directive, NodePart} from '../lit-html.js'; | ||
import {createMarker, directive, NodePart, Part} from '../lit-html.js'; | ||
@@ -35,5 +35,8 @@ /** | ||
*/ | ||
export const asyncAppend = <T>( | ||
value: AsyncIterable<T>, mapper?: (v: T, index?: number) => any): | ||
Directive<NodePart> => directive(async (part: NodePart) => { | ||
export const asyncAppend = directive( | ||
<T>(value: AsyncIterable<T>, | ||
mapper?: (v: T, index?: number) => any) => async (part: Part) => { | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('asyncAppend can only be used in text bindings'); | ||
} | ||
// If we've already set up this particular iterable, we don't need | ||
@@ -52,4 +55,4 @@ // to do anything. | ||
for await (let v of value) { | ||
// When we get the first value, clear the part. This lets the previous | ||
// value display until we can replace it. | ||
// When we get the first value, clear the part. This lets the | ||
// previous value display until we can replace it. | ||
if (i === 0) { | ||
@@ -56,0 +59,0 @@ part.clear(); |
@@ -15,3 +15,3 @@ /** | ||
import {directive, Directive, NodePart} from '../lit-html.js'; | ||
import {directive, NodePart, Part} from '../lit-html.js'; | ||
@@ -36,5 +36,8 @@ /** | ||
*/ | ||
export const asyncReplace = | ||
<T>(value: AsyncIterable<T>, mapper?: (v: T, index?: number) => any): | ||
Directive<NodePart> => directive(async (part: NodePart) => { | ||
export const asyncReplace = directive( | ||
<T>(value: AsyncIterable<T>, mapper?: (v: T, index?: number) => any) => | ||
async (part: Part) => { | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('asyncReplace can only be used in text bindings'); | ||
} | ||
// If we've already set up this particular iterable, we don't need | ||
@@ -41,0 +44,0 @@ // to do anything. |
@@ -15,3 +15,3 @@ /** | ||
import {AttributePart, directive, Directive, PropertyPart} from '../lit-html.js'; | ||
import {AttributePart, directive, Part, PropertyPart} from '../lit-html.js'; | ||
@@ -61,32 +61,30 @@ | ||
*/ | ||
export const classMap = (classInfo: ClassInfo): Directive<AttributePart> => | ||
directive((part: AttributePart) => { | ||
if (!(part instanceof AttributePart) || (part instanceof PropertyPart) || | ||
part.committer.name !== 'class' || part.committer.parts.length > 1) { | ||
throw new Error( | ||
'The `classMap` directive must be used in the `class` attribute ' + | ||
'and must be the only part in the attribute.'); | ||
} | ||
// handle static classes | ||
if (!classMapStatics.has(part)) { | ||
part.committer.element.className = part.committer.strings.join(' '); | ||
classMapStatics.set(part, true); | ||
} | ||
// remove old classes that no longer apply | ||
const oldInfo = classMapCache.get(part); | ||
for (const name in oldInfo) { | ||
if (!(name in classInfo)) { | ||
part.committer.element.classList.remove(name); | ||
} | ||
} | ||
// add new classes | ||
for (const name in classInfo) { | ||
if (!oldInfo || (oldInfo[name] !== classInfo[name])) { | ||
// We explicitly want a loose truthy check here because | ||
// it seems more convenient that '' and 0 are skipped. | ||
part.committer.element.classList.toggle( | ||
name, Boolean(classInfo[name])); | ||
} | ||
} | ||
classMapCache.set(part, classInfo); | ||
}); | ||
export const classMap = directive((classInfo: ClassInfo) => (part: Part) => { | ||
if (!(part instanceof AttributePart) || (part instanceof PropertyPart) || | ||
part.committer.name !== 'class' || part.committer.parts.length > 1) { | ||
throw new Error( | ||
'The `classMap` directive must be used in the `class` attribute ' + | ||
'and must be the only part in the attribute.'); | ||
} | ||
// handle static classes | ||
if (!classMapStatics.has(part)) { | ||
part.committer.element.className = part.committer.strings.join(' '); | ||
classMapStatics.set(part, true); | ||
} | ||
// remove old classes that no longer apply | ||
const oldInfo = classMapCache.get(part); | ||
for (const name in oldInfo) { | ||
if (!(name in classInfo)) { | ||
part.committer.element.classList.remove(name); | ||
} | ||
} | ||
// add new classes | ||
for (const name in classInfo) { | ||
if (!oldInfo || (oldInfo[name] !== classInfo[name])) { | ||
// We explicitly want a loose truthy check here because | ||
// it seems more convenient that '' and 0 are skipped. | ||
part.committer.element.classList.toggle(name, Boolean(classInfo[name])); | ||
} | ||
} | ||
classMapCache.set(part, classInfo); | ||
}); |
@@ -15,5 +15,5 @@ /** | ||
import {directive, Directive, NodePart} from '../lit-html.js'; | ||
import {directive, Part} from '../lit-html.js'; | ||
const previousExpressions = new WeakMap<NodePart, any>(); | ||
const previousExpressions = new WeakMap<Part, any>(); | ||
@@ -40,11 +40,10 @@ /** | ||
export const guard = | ||
(expression: any, valueFn: () => any): Directive<NodePart> => | ||
directive((part: NodePart): void => { | ||
// Dirty check previous expression | ||
if (previousExpressions.get(part) === expression) { | ||
return; | ||
} | ||
directive((expression: any, valueFn: () => any) => (part: Part): void => { | ||
// Dirty check previous expression | ||
if (previousExpressions.get(part) === expression) { | ||
return; | ||
} | ||
part.setValue(valueFn()); | ||
previousExpressions.set(part, expression); | ||
}); | ||
part.setValue(valueFn()); | ||
previousExpressions.set(part, expression); | ||
}); |
@@ -15,3 +15,3 @@ /** | ||
import {AttributePart, directive, Directive, Part} from '../lit-html.js'; | ||
import {AttributePart, directive, Part} from '../lit-html.js'; | ||
@@ -24,12 +24,11 @@ /** | ||
*/ | ||
export const ifDefined = (value: any): Directive<Part> => | ||
directive((part: Part) => { | ||
if (value === undefined && part instanceof AttributePart) { | ||
if (value !== part.value) { | ||
const name = part.committer.name; | ||
part.committer.element.removeAttribute(name); | ||
} | ||
} else { | ||
part.setValue(value); | ||
} | ||
}); | ||
export const ifDefined = directive((value: any) => (part: Part) => { | ||
if (value === undefined && part instanceof AttributePart) { | ||
if (value !== part.value) { | ||
const name = part.committer.name; | ||
part.committer.element.removeAttribute(name); | ||
} | ||
} else { | ||
part.setValue(value); | ||
} | ||
}); |
@@ -15,6 +15,7 @@ /** | ||
import {createMarker, directive, Directive, NodePart, removeNodes, reparentNodes} from '../lit-html.js'; | ||
import {DirectiveFn} from '../lib/directive.js'; | ||
import {createMarker, directive, NodePart, Part, removeNodes, reparentNodes} from '../lit-html.js'; | ||
export type KeyFn<T> = (item: T, index?: number) => 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; | ||
@@ -83,5 +84,5 @@ // Helper functions for manipulating parts | ||
* | ||
* 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. | ||
* IMPORTANT: If providing a `keyFn`, keys *must* be unique for all items in a | ||
* given call to `repeat`. The behavior when two or more items have the same key | ||
* is undefined. | ||
* | ||
@@ -91,313 +92,331 @@ * If no `keyFn` is provided, this directive will perform similar to mapping | ||
*/ | ||
export function repeat<T>( | ||
items: Iterable<T>, keyFn: KeyFn<T>, template: ItemTemplate<T>): | ||
Directive<NodePart>; | ||
export function repeat<T>( | ||
items: Iterable<T>, template: ItemTemplate<T>): Directive<NodePart>; | ||
export function repeat<T>( | ||
items: Iterable<T>, | ||
keyFnOrTemplate: KeyFn<T>|ItemTemplate<T>, | ||
template?: ItemTemplate<T>): Directive<NodePart> { | ||
let keyFn: KeyFn<T>; | ||
if (arguments.length === 2) { | ||
template = keyFnOrTemplate; | ||
} else if (arguments.length === 3) { | ||
keyFn = keyFnOrTemplate as KeyFn<T>; | ||
} | ||
export const repeat = directive( | ||
<T>(items: Iterable<T>, | ||
keyFnOrTemplate: KeyFn<T>|ItemTemplate<T>, | ||
template?: ItemTemplate<T>): DirectiveFn => { | ||
let keyFn: KeyFn<T>; | ||
if (template === undefined) { | ||
template = keyFnOrTemplate; | ||
} else if (keyFnOrTemplate !== undefined) { | ||
keyFn = keyFnOrTemplate as KeyFn<T>; | ||
} | ||
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) || []; | ||
return (containerPart: Part): void => { | ||
if (!(containerPart instanceof NodePart)) { | ||
throw new Error('repeat can only be used in text bindings'); | ||
} | ||
// 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 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) { | ||
newKeys[index] = keyFn ? keyFn(item, index) : index; | ||
newValues[index] = template !(item, index); | ||
index++; | ||
} | ||
// 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) { | ||
newKeys[index] = keyFn ? keyFn(item, index) : index; | ||
newValues[index] = template !(item, index); | ||
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!: Map<unknown, number>; | ||
let oldKeyToIndexMap!: Map<unknown, number>; | ||
// 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; | ||
// 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. | ||
// 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); | ||
} | ||
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; | ||
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 { | ||
// 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; | ||
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 as number] = null; | ||
} | ||
newHead++; | ||
} | ||
} | ||
newHead++; | ||
} | ||
} | ||
} | ||
// 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); | ||
}); | ||
} | ||
// 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); | ||
}; | ||
}); |
@@ -15,3 +15,3 @@ /** | ||
import {AttributePart, directive, Directive, PropertyPart} from '../lit-html.js'; | ||
import {AttributePart, directive, Part, PropertyPart} from '../lit-html.js'; | ||
@@ -44,25 +44,24 @@ export interface StyleInfo { | ||
*/ | ||
export const styleMap = (styleInfo: StyleInfo): Directive<AttributePart> => | ||
directive((part: AttributePart) => { | ||
if (!(part instanceof AttributePart) || (part instanceof PropertyPart) || | ||
part.committer.name !== 'style' || part.committer.parts.length > 1) { | ||
throw new Error( | ||
'The `styleMap` directive must be used in the style attribute ' + | ||
'and must be the only part in the attribute.'); | ||
} | ||
// handle static styles | ||
if (!styleMapStatics.has(part)) { | ||
(part.committer.element as HTMLElement).style.cssText = | ||
part.committer.strings.join(' '); | ||
styleMapStatics.set(part, true); | ||
} | ||
// remove old styles that no longer apply | ||
const oldInfo = styleMapCache.get(part); | ||
for (const name in oldInfo) { | ||
if (!(name in styleInfo)) { | ||
((part.committer.element as HTMLElement).style as any)[name] = null; | ||
} | ||
} | ||
Object.assign((part.committer.element as HTMLElement).style, styleInfo); | ||
styleMapCache.set(part, styleInfo); | ||
}); | ||
export const styleMap = directive((styleInfo: StyleInfo) => (part: Part) => { | ||
if (!(part instanceof AttributePart) || (part instanceof PropertyPart) || | ||
part.committer.name !== 'style' || part.committer.parts.length > 1) { | ||
throw new Error( | ||
'The `styleMap` directive must be used in the style attribute ' + | ||
'and must be the only part in the attribute.'); | ||
} | ||
// handle static styles | ||
if (!styleMapStatics.has(part)) { | ||
(part.committer.element as HTMLElement).style.cssText = | ||
part.committer.strings.join(' '); | ||
styleMapStatics.set(part, true); | ||
} | ||
// remove old styles that no longer apply | ||
const oldInfo = styleMapCache.get(part); | ||
for (const name in oldInfo) { | ||
if (!(name in styleInfo)) { | ||
((part.committer.element as HTMLElement).style as any)[name] = null; | ||
} | ||
} | ||
Object.assign((part.committer.element as HTMLElement).style, styleInfo); | ||
styleMapCache.set(part, styleInfo); | ||
}); |
@@ -15,3 +15,3 @@ /** | ||
import {directive, Directive, isPrimitive, NodePart} from '../lit-html.js'; | ||
import {directive, isPrimitive, NodePart, Part} from '../lit-html.js'; | ||
@@ -28,15 +28,17 @@ /** | ||
export const unsafeHTML = (value: any): Directive<NodePart> => | ||
directive((part: NodePart): void => { | ||
// Dirty check primitive values | ||
const previousValue = previousValues.get(part); | ||
if (previousValue === value && isPrimitive(value)) { | ||
return; | ||
} | ||
export const unsafeHTML = directive((value: any) => (part: Part): void => { | ||
if (!(part instanceof NodePart)) { | ||
throw new Error('unsafeHTML can only be used in text bindings'); | ||
} | ||
// Dirty check primitive values | ||
const previousValue = previousValues.get(part); | ||
if (previousValue === value && isPrimitive(value)) { | ||
return; | ||
} | ||
// Use a <template> to parse HTML into Nodes | ||
const tmp = document.createElement('template'); | ||
tmp.innerHTML = value; | ||
part.setValue(document.importNode(tmp.content, true)); | ||
previousValues.set(part, value); | ||
}); | ||
// Use a <template> to parse HTML into Nodes | ||
const tmp = document.createElement('template'); | ||
tmp.innerHTML = value; | ||
part.setValue(document.importNode(tmp.content, true)); | ||
previousValues.set(part, value); | ||
}); |
@@ -15,3 +15,3 @@ /** | ||
import {directive, Directive, NodePart} from '../lit-html.js'; | ||
import {directive, Part} from '../lit-html.js'; | ||
@@ -22,7 +22,6 @@ /** | ||
export const until = | ||
(promise: Promise<any>, defaultContent: any): Directive<NodePart> => | ||
directive((part: NodePart) => { | ||
part.setValue(defaultContent); | ||
part.commit(); | ||
part.setValue(promise); | ||
}); | ||
directive((promise: Promise<any>, defaultContent: any) => (part: Part) => { | ||
part.setValue(defaultContent); | ||
part.commit(); | ||
part.setValue(promise); | ||
}); |
@@ -15,3 +15,3 @@ /** | ||
import {directive, Directive, NodePart, reparentNodes} from '../lit-html.js'; | ||
import {directive, NodePart, Part, reparentNodes} from '../lit-html.js'; | ||
@@ -51,54 +51,57 @@ interface PartCache { | ||
*/ | ||
export const when = | ||
(condition: any, trueValue: () => any, falseValue: () => any): | ||
Directive<NodePart> => directive((parentPart: NodePart) => { | ||
let cache = partCaches.get(parentPart); | ||
export const when = directive( | ||
(condition: any, trueValue: () => any, falseValue: () => any) => ( | ||
parentPart: Part) => { | ||
if (!(parentPart instanceof NodePart)) { | ||
throw new Error('when can only be used in text bindings'); | ||
} | ||
// Create a new cache if this is the first render | ||
if (cache === undefined) { | ||
// Cache consists of two parts, one for each condition, and a | ||
// docment fragment which we cache the nodes of the condition that's | ||
// not currently rendered. | ||
cache = { | ||
truePart: new NodePart(parentPart.options), | ||
falsePart: new NodePart(parentPart.options), | ||
cacheContainer: document.createDocumentFragment(), | ||
}; | ||
partCaches.set(parentPart, cache); | ||
let cache = partCaches.get(parentPart); | ||
cache.truePart.appendIntoPart(parentPart); | ||
cache.falsePart.appendIntoPart(parentPart); | ||
} | ||
// Create a new cache if this is the first render | ||
if (cache === undefined) { | ||
// Cache consists of two parts, one for each condition, and a | ||
// docment fragment which we cache the nodes of the condition that's | ||
// not currently rendered. | ||
cache = { | ||
truePart: new NodePart(parentPart.options), | ||
falsePart: new NodePart(parentPart.options), | ||
cacheContainer: document.createDocumentFragment(), | ||
}; | ||
partCaches.set(parentPart, cache); | ||
// Based on the condition, select which part to render and which value | ||
// to set on that part. | ||
const nextPart = condition ? cache.truePart : cache.falsePart; | ||
const nextValue = condition ? trueValue() : falseValue(); | ||
cache.truePart.appendIntoPart(parentPart); | ||
cache.falsePart.appendIntoPart(parentPart); | ||
} | ||
// If we switched condition, swap nodes to/from the cache. | ||
if (!!condition !== cache.prevCondition) { | ||
// Get the part which was rendered for the opposite condition. This | ||
// should be added to the cache. | ||
const prevPart = condition ? cache.falsePart : cache.truePart; | ||
// Based on the condition, select which part to render and which value | ||
// to set on that part. | ||
const nextPart = condition ? cache.truePart : cache.falsePart; | ||
const nextValue = condition ? trueValue() : falseValue(); | ||
// If the next part was rendered, take it from the cache | ||
if (nextPart.value) { | ||
parentPart.startNode.parentNode!.appendChild( | ||
cache.cacheContainer); | ||
} | ||
// If we switched condition, swap nodes to/from the cache. | ||
if (!!condition !== cache.prevCondition) { | ||
// Get the part which was rendered for the opposite condition. This | ||
// should be added to the cache. | ||
const prevPart = condition ? cache.falsePart : cache.truePart; | ||
// If the prev part was rendered, move it to the cache | ||
if (prevPart.value) { | ||
reparentNodes( | ||
cache.cacheContainer, | ||
prevPart.startNode, | ||
prevPart.endNode.nextSibling); | ||
} | ||
} | ||
// If the next part was rendered, take it from the cache | ||
if (nextPart.value) { | ||
parentPart.startNode.parentNode!.appendChild(cache.cacheContainer); | ||
} | ||
// Set the next part's value | ||
nextPart.setValue(nextValue); | ||
nextPart.commit(); | ||
// If the prev part was rendered, move it to the cache | ||
if (prevPart.value) { | ||
reparentNodes( | ||
cache.cacheContainer, | ||
prevPart.startNode, | ||
prevPart.endNode.nextSibling); | ||
} | ||
} | ||
cache.prevCondition = !!condition; | ||
}); | ||
// Set the next part's value | ||
nextPart.setValue(nextValue); | ||
nextPart.commit(); | ||
cache.prevCondition = !!condition; | ||
}); |
@@ -0,1 +1,2 @@ | ||
/** | ||
@@ -19,12 +20,32 @@ * @license | ||
export interface Directive<P = Part> { | ||
(part: P): void; | ||
} | ||
export type DirectiveFn = (part: Part) => void; | ||
export const directive = <P = Part>(f: Directive<P>): Directive<P> => { | ||
directives.set(f, true); | ||
return f; | ||
}; | ||
/** | ||
* Brands a function as a directive so that lit-html will call the function | ||
* during template rendering, rather than passing as a value. | ||
* | ||
* @param f The directive factory function. Must be a function that returns a | ||
* function of the signature `(part: Part) => void`. The returned function will | ||
* be called with the part object | ||
* | ||
* @example | ||
* | ||
* ``` | ||
* import {directive, html} from 'lit-html'; | ||
* | ||
* const immutable = directive((v) => (part) => { | ||
* if (part.value !== v) { | ||
* part.setValue(v) | ||
* } | ||
* }); | ||
* ``` | ||
*/ | ||
export const directive = <F extends Function>(f: F): F => | ||
((...args: any[]) => { | ||
const d = f(...args); | ||
directives.set(d, true); | ||
return d; | ||
}) as unknown as F; | ||
export const isDirective = (o: any) => | ||
typeof o === 'function' && directives.has(o); |
@@ -407,10 +407,2 @@ /** | ||
declare global { | ||
interface EventListenerOptions { | ||
capture?: boolean; | ||
once?: boolean; | ||
passive?: boolean; | ||
} | ||
} | ||
// Detect event listener options support. If the `capture` property is read | ||
@@ -439,4 +431,5 @@ // from the options object, then options are supported. If not, then the thrid | ||
value: any = undefined; | ||
_options?: {capture?: boolean, passive?: boolean, once?: boolean}; | ||
_options?: AddEventListenerOptions; | ||
_pendingValue: any = undefined; | ||
_boundHandleEvent: (event: Event) => void; | ||
@@ -447,2 +440,3 @@ constructor(element: Element, eventName: string, eventContext?: EventTarget) { | ||
this.eventContext = eventContext; | ||
this._boundHandleEvent = (e) => this.handleEvent(e); | ||
} | ||
@@ -475,7 +469,9 @@ | ||
if (shouldRemoveListener) { | ||
this.element.removeEventListener(this.eventName, this, this._options); | ||
this.element.removeEventListener( | ||
this.eventName, this._boundHandleEvent, this._options); | ||
} | ||
this._options = getOptions(newListener); | ||
if (shouldAddListener) { | ||
this.element.addEventListener(this.eventName, this, this._options); | ||
this.element.addEventListener( | ||
this.eventName, this._boundHandleEvent, this._options); | ||
} | ||
@@ -487,8 +483,7 @@ this.value = newListener; | ||
handleEvent(event: 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); | ||
if (typeof this.value === 'function') { | ||
this.value.call(this.eventContext || this.element, event); | ||
} else { | ||
this.value.handleEvent(event); | ||
} | ||
} | ||
@@ -495,0 +490,0 @@ } |
@@ -25,8 +25,2 @@ /** | ||
declare global { | ||
interface Window { | ||
ShadyCSS: any; | ||
} | ||
class ShadowRoot {} | ||
} | ||
@@ -65,3 +59,3 @@ // Get a key to lookup in `templateCaches`. | ||
if (compatibleShadyCSSVersion) { | ||
window.ShadyCSS.prepareTemplateDom(element, scopeName); | ||
window.ShadyCSS!.prepareTemplateDom(element, scopeName); | ||
} | ||
@@ -142,4 +136,4 @@ template = new Template(result, element); | ||
// needed (e.g. @apply native Shadow DOM case). | ||
window.ShadyCSS.prepareTemplateStyles(template.element, scopeName); | ||
if (window.ShadyCSS.nativeShadow) { | ||
window.ShadyCSS!.prepareTemplateStyles(template.element, scopeName); | ||
if (window.ShadyCSS!.nativeShadow) { | ||
// When in native Shadow DOM, re-add styling to rendered content using | ||
@@ -190,5 +184,5 @@ // the style ShadyCSS produced. | ||
if (!hasRendered) { | ||
window.ShadyCSS.styleElement((container as ShadowRoot).host); | ||
window.ShadyCSS!.styleElement((container as ShadowRoot).host); | ||
} | ||
} | ||
}; |
@@ -18,13 +18,13 @@ /** | ||
export * from './lib/template-result.js'; | ||
export * from './lib/template.js'; | ||
export * from './lib/template-processor.js'; | ||
export * from './lib/default-template-processor.js'; | ||
export * from './lib/template-instance.js'; | ||
export * from './lib/part.js'; | ||
export * from './lib/parts.js'; | ||
export * from './lib/dom.js'; | ||
export * from './lib/directive.js'; | ||
export * from './lib/render.js'; | ||
export * from './lib/template-factory.js'; | ||
export {DefaultTemplateProcessor, defaultTemplateProcessor} from './lib/default-template-processor.js'; | ||
export {directive, DirectiveFn, isDirective} from './lib/directive.js'; | ||
// TODO(justinfagnani): remove line when we get NodePart moving methods | ||
export {removeNodes, reparentNodes} from './lib/dom.js'; | ||
export {noChange, Part} from './lib/part.js'; | ||
export {AttributeCommitter, AttributePart, BooleanAttributePart, EventPart, isPrimitive, NodePart, PropertyCommitter, PropertyPart} from './lib/parts.js'; | ||
export {parts, render} from './lib/render.js'; | ||
export {templateCaches, templateFactory} from './lib/template-factory.js'; | ||
export {TemplateInstance} from './lib/template-instance.js'; | ||
export {SVGTemplateResult, TemplateResult} from './lib/template-result.js'; | ||
export {createMarker, isTemplatePartActive, Template} from './lib/template.js'; | ||
@@ -31,0 +31,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
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
761672
158
8643
16