UNPKG

23.2 kBSource Map (JSON)View Raw
1{"version":3,"file":"repeat.js","sources":["../src/directives/repeat.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\n\nimport {ChildPart, noChange} from '../lit-html.js';\nimport {directive, Directive, PartInfo, PartType} from '../directive.js';\nimport {\n insertPart,\n getCommittedValue,\n removePart,\n setCommittedValue,\n setChildPartValue,\n} from '../directive-helpers.js';\n\nexport type KeyFn<T> = (item: T, index: number) => unknown;\nexport type ItemTemplate<T> = (item: T, index: number) => unknown;\n\n// Helper for generating a map of array item to its index over a subset\n// of an array (used to lazily generate `newKeyToIndexMap` and\n// `oldKeyToIndexMap`)\nconst generateMap = (list: unknown[], start: number, end: number) => {\n const map = new Map<unknown, number>();\n for (let i = start; i <= end; i++) {\n map.set(list[i], i);\n }\n return map;\n};\n\nclass RepeatDirective extends Directive {\n private _itemKeys?: unknown[];\n\n constructor(partInfo: PartInfo) {\n super(partInfo);\n if (partInfo.type !== PartType.CHILD) {\n throw new Error('repeat() can only be used in text expressions');\n }\n }\n\n private _getValuesAndKeys<T>(\n items: Iterable<T>,\n keyFnOrTemplate: KeyFn<T> | ItemTemplate<T>,\n template?: ItemTemplate<T>\n ) {\n let keyFn: KeyFn<T> | undefined;\n if (template === undefined) {\n template = keyFnOrTemplate;\n } else if (keyFnOrTemplate !== undefined) {\n keyFn = keyFnOrTemplate as KeyFn<T>;\n }\n const keys = [];\n const values = [];\n let index = 0;\n for (const item of items) {\n keys[index] = keyFn ? keyFn(item, index) : index;\n values[index] = template!(item, index);\n index++;\n }\n return {\n values,\n keys,\n };\n }\n\n render<T>(items: Iterable<T>, template: ItemTemplate<T>): Array<unknown>;\n render<T>(\n items: Iterable<T>,\n keyFn: KeyFn<T> | ItemTemplate<T>,\n template: ItemTemplate<T>\n ): Array<unknown>;\n render<T>(\n items: Iterable<T>,\n keyFnOrTemplate: KeyFn<T> | ItemTemplate<T>,\n template?: ItemTemplate<T>\n ) {\n return this._getValuesAndKeys(items, keyFnOrTemplate, template).values;\n }\n\n override update<T>(\n containerPart: ChildPart,\n [items, keyFnOrTemplate, template]: [\n Iterable<T>,\n KeyFn<T> | ItemTemplate<T>,\n ItemTemplate<T>\n ]\n ) {\n // Old part & key lists are retrieved from the last update (which may\n // be primed by hydration)\n const oldParts = getCommittedValue(\n containerPart\n ) as Array<ChildPart | null>;\n const {values: newValues, keys: newKeys} = this._getValuesAndKeys(\n items,\n keyFnOrTemplate,\n template\n );\n\n // We check that oldParts, the committed value, is an Array as an\n // indicator that the previous value came from a repeat() call. If\n // oldParts is not an Array then this is the first render and we return\n // an array for lit-html's array handling to render, and remember the\n // keys.\n if (!Array.isArray(oldParts)) {\n this._itemKeys = newKeys;\n return newValues;\n }\n\n // In SSR hydration it's possible for oldParts to be an arrray but for us\n // to not have item keys because the update() hasn't run yet. We set the\n // keys to an empty array. This will cause all oldKey/newKey comparisons\n // to fail and execution to fall to the last nested brach below which\n // reuses the oldPart.\n const oldKeys = (this._itemKeys ??= []);\n\n // New part list will be built up as we go (either reused from\n // old parts or created for new keys in this update). This is\n // saved in the above cache at the end of the update.\n const newParts: ChildPart[] = [];\n\n // Maps from key to index for current and previous update; these\n // are generated lazily only when needed as a performance\n // optimization, since they are only required for multiple\n // non-contiguous changes in the list, which are less common.\n let newKeyToIndexMap!: Map<unknown, number>;\n let oldKeyToIndexMap!: Map<unknown, number>;\n\n // Head and tail pointers to old parts and new values\n let oldHead = 0;\n let oldTail = oldParts.length - 1;\n let newHead = 0;\n let newTail = newValues.length - 1;\n\n // Overview of O(n) reconciliation algorithm (general approach\n // based on ideas found in ivi, vue, snabbdom, etc.):\n //\n // * We start with the list of old parts and new values (and\n // arrays of their respective keys), head/tail pointers into\n // each, and we build up the new list of parts by updating\n // (and when needed, moving) old parts or creating new ones.\n // The initial scenario might look like this (for brevity of\n // the diagrams, the numbers in the array reflect keys\n // associated with the old parts or new values, although keys\n // and parts/values are actually stored in parallel arrays\n // indexed using the same head/tail pointers):\n //\n // oldHead v v oldTail\n // oldKeys: [0, 1, 2, 3, 4, 5, 6]\n // newParts: [ , , , , , , ]\n // newKeys: [0, 2, 1, 4, 3, 7, 6] <- reflects the user's new\n // item order\n // newHead ^ ^ newTail\n //\n // * Iterate old & new lists from both sides, updating,\n // swapping, or removing parts at the head/tail locations\n // until neither head nor tail can move.\n //\n // * Example below: keys at head pointers match, so update old\n // part 0 in-place (no need to move it) and record part 0 in\n // the `newParts` list. The last thing we do is advance the\n // `oldHead` and `newHead` pointers (will be reflected in the\n // next diagram).\n //\n // oldHead v v oldTail\n // oldKeys: [0, 1, 2, 3, 4, 5, 6]\n // newParts: [0, , , , , , ] <- heads matched: update 0\n // newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldHead\n // & newHead\n // newHead ^ ^ newTail\n //\n // * Example below: head pointers don't match, but tail\n // pointers do, so update part 6 in place (no need to move\n // it), and record part 6 in the `newParts` list. Last,\n // advance the `oldTail` and `oldHead` pointers.\n //\n // oldHead v v oldTail\n // oldKeys: [0, 1, 2, 3, 4, 5, 6]\n // newParts: [0, , , , , , 6] <- tails matched: update 6\n // newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldTail\n // & newTail\n // newHead ^ ^ newTail\n //\n // * If neither head nor tail match; next check if one of the\n // old head/tail items was removed. We first need to generate\n // the reverse map of new keys to index (`newKeyToIndexMap`),\n // which is done once lazily as a performance optimization,\n // since we only hit this case if multiple non-contiguous\n // changes were made. Note that for contiguous removal\n // anywhere in the list, the head and tails would advance\n // from either end and pass each other before we get to this\n // case and removals would be handled in the final while loop\n // without needing to generate the map.\n //\n // * Example below: The key at `oldTail` was removed (no longer\n // in the `newKeyToIndexMap`), so remove that part from the\n // DOM and advance just the `oldTail` pointer.\n //\n // oldHead v v oldTail\n // oldKeys: [0, 1, 2, 3, 4, 5, 6]\n // newParts: [0, , , , , , 6] <- 5 not in new map: remove\n // newKeys: [0, 2, 1, 4, 3, 7, 6] 5 and advance oldTail\n // newHead ^ ^ newTail\n //\n // * Once head and tail cannot move, any mismatches are due to\n // either new or moved items; if a new key is in the previous\n // \"old key to old index\" map, move the old part to the new\n // location, otherwise create and insert a new part. Note\n // that when moving an old part we null its position in the\n // oldParts array if it lies between the head and tail so we\n // know to skip it when the pointers get there.\n //\n // * Example below: neither head nor tail match, and neither\n // were removed; so find the `newHead` key in the\n // `oldKeyToIndexMap`, and move that old part's DOM into the\n // next head position (before `oldParts[oldHead]`). Last,\n // null the part in the `oldPart` array since it was\n // somewhere in the remaining oldParts still to be scanned\n // (between the head and tail pointers) so that we know to\n // skip that old part on future iterations.\n //\n // oldHead v v oldTail\n // oldKeys: [0, 1, -, 3, 4, 5, 6]\n // newParts: [0, 2, , , , , 6] <- stuck: update & move 2\n // newKeys: [0, 2, 1, 4, 3, 7, 6] into place and advance\n // newHead\n // newHead ^ ^ newTail\n //\n // * Note that for moves/insertions like the one above, a part\n // inserted at the head pointer is inserted before the\n // current `oldParts[oldHead]`, and a part inserted at the\n // tail pointer is inserted before `newParts[newTail+1]`. The\n // seeming asymmetry lies in the fact that new parts are\n // moved into place outside in, so to the right of the head\n // pointer are old parts, and to the right of the tail\n // pointer are new parts.\n //\n // * We always restart back from the top of the algorithm,\n // allowing matching and simple updates in place to\n // continue...\n //\n // * Example below: the head pointers once again match, so\n // simply update part 1 and record it in the `newParts`\n // array. Last, advance both head pointers.\n //\n // oldHead v v oldTail\n // oldKeys: [0, 1, -, 3, 4, 5, 6]\n // newParts: [0, 2, 1, , , , 6] <- heads matched: update 1\n // newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldHead\n // & newHead\n // newHead ^ ^ newTail\n //\n // * As mentioned above, items that were moved as a result of\n // being stuck (the final else clause in the code below) are\n // marked with null, so we always advance old pointers over\n // these so we're comparing the next actual old value on\n // either end.\n //\n // * Example below: `oldHead` is null (already placed in\n // newParts), so advance `oldHead`.\n //\n // oldHead v v oldTail\n // oldKeys: [0, 1, -, 3, 4, 5, 6] <- old head already used:\n // newParts: [0, 2, 1, , , , 6] advance oldHead\n // newKeys: [0, 2, 1, 4, 3, 7, 6]\n // newHead ^ ^ newTail\n //\n // * Note it's not critical to mark old parts as null when they\n // are moved from head to tail or tail to head, since they\n // will be outside the pointer range and never visited again.\n //\n // * Example below: Here the old tail key matches the new head\n // key, so the part at the `oldTail` position and move its\n // DOM to the new head position (before `oldParts[oldHead]`).\n // Last, advance `oldTail` and `newHead` pointers.\n //\n // oldHead v v oldTail\n // oldKeys: [0, 1, -, 3, 4, 5, 6]\n // newParts: [0, 2, 1, 4, , , 6] <- old tail matches new\n // newKeys: [0, 2, 1, 4, 3, 7, 6] head: update & move 4,\n // advance oldTail & newHead\n // newHead ^ ^ newTail\n //\n // * Example below: Old and new head keys match, so update the\n // old head part in place, and advance the `oldHead` and\n // `newHead` pointers.\n //\n // oldHead v oldTail\n // oldKeys: [0, 1, -, 3, 4, 5, 6]\n // newParts: [0, 2, 1, 4, 3, ,6] <- heads match: update 3\n // newKeys: [0, 2, 1, 4, 3, 7, 6] and advance oldHead &\n // newHead\n // newHead ^ ^ newTail\n //\n // * Once the new or old pointers move past each other then all\n // we have left is additions (if old list exhausted) or\n // removals (if new list exhausted). Those are handled in the\n // final while loops at the end.\n //\n // * Example below: `oldHead` exceeded `oldTail`, so we're done\n // with the main loop. Create the remaining part and insert\n // it at the new head position, and the update is complete.\n //\n // (oldHead > oldTail)\n // oldKeys: [0, 1, -, 3, 4, 5, 6]\n // newParts: [0, 2, 1, 4, 3, 7 ,6] <- create and insert 7\n // newKeys: [0, 2, 1, 4, 3, 7, 6]\n // newHead ^ newTail\n //\n // * Note that the order of the if/else clauses is not\n // important to the algorithm, as long as the null checks\n // come first (to ensure we're always working on valid old\n // parts) and that the final else clause comes last (since\n // that's where the expensive moves occur). The order of\n // remaining clauses is is just a simple guess at which cases\n // will be most common.\n //\n // * Note, we could calculate the longest\n // increasing subsequence (LIS) of old items in new position,\n // and only move those not in the LIS set. However that costs\n // O(nlogn) time and adds a bit more code, and only helps\n // make rare types of mutations require fewer moves. The\n // above handles removes, adds, reversal, swaps, and single\n // moves of contiguous items in linear time, in the minimum\n // number of moves. As the number of multiple moves where LIS\n // might help approaches a random shuffle, the LIS\n // optimization becomes less helpful, so it seems not worth\n // the code at this point. Could reconsider if a compelling\n // case arises.\n\n while (oldHead <= oldTail && newHead <= newTail) {\n if (oldParts[oldHead] === null) {\n // `null` means old part at head has already been used\n // below; skip\n oldHead++;\n } else if (oldParts[oldTail] === null) {\n // `null` means old part at tail has already been used\n // below; skip\n oldTail--;\n } else if (oldKeys[oldHead] === newKeys[newHead]) {\n // Old head matches new head; update in place\n newParts[newHead] = setChildPartValue(\n oldParts[oldHead]!,\n newValues[newHead]\n );\n oldHead++;\n newHead++;\n } else if (oldKeys[oldTail] === newKeys[newTail]) {\n // Old tail matches new tail; update in place\n newParts[newTail] = setChildPartValue(\n oldParts[oldTail]!,\n newValues[newTail]\n );\n oldTail--;\n newTail--;\n } else if (oldKeys[oldHead] === newKeys[newTail]) {\n // Old head matches new tail; update and move to new tail\n newParts[newTail] = setChildPartValue(\n oldParts[oldHead]!,\n newValues[newTail]\n );\n insertPart(containerPart, newParts[newTail + 1], oldParts[oldHead]!);\n oldHead++;\n newTail--;\n } else if (oldKeys[oldTail] === newKeys[newHead]) {\n // Old tail matches new head; update and move to new head\n newParts[newHead] = setChildPartValue(\n oldParts[oldTail]!,\n newValues[newHead]\n );\n insertPart(containerPart, oldParts[oldHead]!, oldParts[oldTail]!);\n oldTail--;\n newHead++;\n } else {\n if (newKeyToIndexMap === undefined) {\n // Lazily generate key-to-index maps, used for removals &\n // moves below\n newKeyToIndexMap = generateMap(newKeys, newHead, newTail);\n oldKeyToIndexMap = generateMap(oldKeys, oldHead, oldTail);\n }\n if (!newKeyToIndexMap.has(oldKeys[oldHead])) {\n // Old head is no longer in new list; remove\n removePart(oldParts[oldHead]!);\n oldHead++;\n } else if (!newKeyToIndexMap.has(oldKeys[oldTail])) {\n // Old tail is no longer in new list; remove\n removePart(oldParts[oldTail]!);\n oldTail--;\n } else {\n // Any mismatches at this point are due to additions or\n // moves; see if we have an old part we can reuse and move\n // into place\n const oldIndex = oldKeyToIndexMap.get(newKeys[newHead]);\n const oldPart = oldIndex !== undefined ? oldParts[oldIndex] : null;\n if (oldPart === null) {\n // No old part for this value; create a new one and\n // insert it\n const newPart = insertPart(containerPart, oldParts[oldHead]!);\n setChildPartValue(newPart, newValues[newHead]);\n newParts[newHead] = newPart;\n } else {\n // Reuse old part\n newParts[newHead] = setChildPartValue(oldPart, newValues[newHead]);\n insertPart(containerPart, oldParts[oldHead]!, oldPart);\n // This marks the old part as having been used, so that\n // it will be skipped in the first two checks above\n oldParts[oldIndex as number] = null;\n }\n newHead++;\n }\n }\n }\n // Add parts for any remaining new values\n while (newHead <= newTail) {\n // For all remaining additions, we insert before last new\n // tail, since old pointers are no longer valid\n const newPart = insertPart(containerPart, newParts[newTail + 1]);\n setChildPartValue(newPart, newValues[newHead]);\n newParts[newHead++] = newPart;\n }\n // Remove any remaining unused old parts\n while (oldHead <= oldTail) {\n const oldPart = oldParts[oldHead++];\n if (oldPart !== null) {\n removePart(oldPart);\n }\n }\n\n // Save order of new parts for next round\n this._itemKeys = newKeys;\n // Directly set part value, bypassing it's dirty-checking\n setCommittedValue(containerPart, newParts);\n return noChange;\n }\n}\n\nexport interface RepeatDirectiveFn {\n <T>(\n items: Iterable<T>,\n keyFnOrTemplate: KeyFn<T> | ItemTemplate<T>,\n template?: ItemTemplate<T>\n ): unknown;\n <T>(items: Iterable<T>, template: ItemTemplate<T>): unknown;\n <T>(\n items: Iterable<T>,\n keyFn: KeyFn<T> | ItemTemplate<T>,\n template: ItemTemplate<T>\n ): unknown;\n}\n\n/**\n * A directive that repeats a series of values (usually `TemplateResults`)\n * generated from an iterable, and updates those items efficiently when the\n * iterable changes based on user-provided `keys` associated with each item.\n *\n * Note that if a `keyFn` is provided, strict key-to-DOM mapping is maintained,\n * meaning previous DOM for a given key is moved into the new position if\n * needed, and DOM will never be reused with values for different keys (new DOM\n * will always be created for new keys). This is generally the most efficient\n * way to use `repeat` since it performs minimum unnecessary work for insertions\n * and removals.\n *\n * The `keyFn` takes two parameters, the item and its index, and returns a unique key value.\n *\n * ```js\n * html`\n * <ol>\n * ${repeat(this.items, (item) => item.id, (item, index) => {\n * return html`<li>${index}: ${item.name}</li>`;\n * })}\n * </ol>\n * `\n * ```\n *\n * **Important**: If providing a `keyFn`, keys *must* be unique for all items in a\n * given call to `repeat`. The behavior when two or more items have the same key\n * is undefined.\n *\n * If no `keyFn` is provided, this directive will perform similar to mapping\n * items to values, and DOM will be reused against potentially different items.\n */\nexport const repeat = directive(RepeatDirective) as RepeatDirectiveFn;\n\n/**\n * The type of the class that powers this directive. Necessary for naming the\n * directive's return type.\n */\nexport type {RepeatDirective};\n"],"names":["generateMap","list","start","end","map","Map","i","set","repeat","directive","Directive","constructor","partInfo","super","type","PartType","CHILD","Error","_getValuesAndKeys","items","keyFnOrTemplate","template","keyFn","undefined","keys","values","index","item","render","this","update","containerPart","oldParts","getCommittedValue","newValues","newKeys","Array","isArray","_itemKeys","oldKeys","newParts","newKeyToIndexMap","oldKeyToIndexMap","oldHead","oldTail","length","newHead","newTail","setChildPartValue","insertPart","has","oldIndex","get","oldPart","newPart","removePart","setCommittedValue","noChange"],"mappings":"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"}
\No newline at end of file