repeat.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. /**
  2. * @license
  3. * Copyright 2017 Google LLC
  4. * SPDX-License-Identifier: BSD-3-Clause
  5. */
  6. import { noChange } from '../lit-html.js';
  7. import { directive, Directive, PartType } from '../directive.js';
  8. import { insertPart, getCommittedValue, removePart, setCommittedValue, setChildPartValue, } from '../directive-helpers.js';
  9. // Helper for generating a map of array item to its index over a subset
  10. // of an array (used to lazily generate `newKeyToIndexMap` and
  11. // `oldKeyToIndexMap`)
  12. const generateMap = (list, start, end) => {
  13. const map = new Map();
  14. for (let i = start; i <= end; i++) {
  15. map.set(list[i], i);
  16. }
  17. return map;
  18. };
  19. class RepeatDirective extends Directive {
  20. constructor(partInfo) {
  21. super(partInfo);
  22. if (partInfo.type !== PartType.CHILD) {
  23. throw new Error('repeat() can only be used in text expressions');
  24. }
  25. }
  26. _getValuesAndKeys(items, keyFnOrTemplate, template) {
  27. let keyFn;
  28. if (template === undefined) {
  29. template = keyFnOrTemplate;
  30. }
  31. else if (keyFnOrTemplate !== undefined) {
  32. keyFn = keyFnOrTemplate;
  33. }
  34. const keys = [];
  35. const values = [];
  36. let index = 0;
  37. for (const item of items) {
  38. keys[index] = keyFn ? keyFn(item, index) : index;
  39. values[index] = template(item, index);
  40. index++;
  41. }
  42. return {
  43. values,
  44. keys,
  45. };
  46. }
  47. render(items, keyFnOrTemplate, template) {
  48. return this._getValuesAndKeys(items, keyFnOrTemplate, template).values;
  49. }
  50. update(containerPart, [items, keyFnOrTemplate, template]) {
  51. var _a;
  52. // Old part & key lists are retrieved from the last update (which may
  53. // be primed by hydration)
  54. const oldParts = getCommittedValue(containerPart);
  55. const { values: newValues, keys: newKeys } = this._getValuesAndKeys(items, keyFnOrTemplate, template);
  56. // We check that oldParts, the committed value, is an Array as an
  57. // indicator that the previous value came from a repeat() call. If
  58. // oldParts is not an Array then this is the first render and we return
  59. // an array for lit-html's array handling to render, and remember the
  60. // keys.
  61. if (!Array.isArray(oldParts)) {
  62. this._itemKeys = newKeys;
  63. return newValues;
  64. }
  65. // In SSR hydration it's possible for oldParts to be an arrray but for us
  66. // to not have item keys because the update() hasn't run yet. We set the
  67. // keys to an empty array. This will cause all oldKey/newKey comparisons
  68. // to fail and execution to fall to the last nested brach below which
  69. // reuses the oldPart.
  70. const oldKeys = ((_a = this._itemKeys) !== null && _a !== void 0 ? _a : (this._itemKeys = []));
  71. // New part list will be built up as we go (either reused from
  72. // old parts or created for new keys in this update). This is
  73. // saved in the above cache at the end of the update.
  74. const newParts = [];
  75. // Maps from key to index for current and previous update; these
  76. // are generated lazily only when needed as a performance
  77. // optimization, since they are only required for multiple
  78. // non-contiguous changes in the list, which are less common.
  79. let newKeyToIndexMap;
  80. let oldKeyToIndexMap;
  81. // Head and tail pointers to old parts and new values
  82. let oldHead = 0;
  83. let oldTail = oldParts.length - 1;
  84. let newHead = 0;
  85. let newTail = newValues.length - 1;
  86. // Overview of O(n) reconciliation algorithm (general approach
  87. // based on ideas found in ivi, vue, snabbdom, etc.):
  88. //
  89. // * We start with the list of old parts and new values (and
  90. // arrays of their respective keys), head/tail pointers into
  91. // each, and we build up the new list of parts by updating
  92. // (and when needed, moving) old parts or creating new ones.
  93. // The initial scenario might look like this (for brevity of
  94. // the diagrams, the numbers in the array reflect keys
  95. // associated with the old parts or new values, although keys
  96. // and parts/values are actually stored in parallel arrays
  97. // indexed using the same head/tail pointers):
  98. //
  99. // oldHead v v oldTail
  100. // oldKeys: [0, 1, 2, 3, 4, 5, 6]
  101. // newParts: [ , , , , , , ]
  102. // newKeys: [0, 2, 1, 4, 3, 7, 6] <- reflects the user's new
  103. // item order
  104. // newHead ^ ^ newTail
  105. //
  106. // * Iterate old & new lists from both sides, updating,
  107. // swapping, or removing parts at the head/tail locations
  108. // until neither head nor tail can move.
  109. //
  110. // * Example below: keys at head pointers match, so update old
  111. // part 0 in-place (no need to move it) and record part 0 in
  112. // the `newParts` list. The last thing we do is advance the
  113. // `oldHead` and `newHead` pointers (will be reflected in the
  114. // next diagram).
  115. //
  116. // oldHead v v oldTail
  117. // oldKeys: [0, 1, 2, 3, 4, 5, 6]
  118. // newParts: [0, , , , , , ] <- heads matched: update 0
  119. // newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldHead
  120. // & newHead
  121. // newHead ^ ^ newTail
  122. //
  123. // * Example below: head pointers don't match, but tail
  124. // pointers do, so update part 6 in place (no need to move
  125. // it), and record part 6 in the `newParts` list. Last,
  126. // advance the `oldTail` and `oldHead` pointers.
  127. //
  128. // oldHead v v oldTail
  129. // oldKeys: [0, 1, 2, 3, 4, 5, 6]
  130. // newParts: [0, , , , , , 6] <- tails matched: update 6
  131. // newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldTail
  132. // & newTail
  133. // newHead ^ ^ newTail
  134. //
  135. // * If neither head nor tail match; next check if one of the
  136. // old head/tail items was removed. We first need to generate
  137. // the reverse map of new keys to index (`newKeyToIndexMap`),
  138. // which is done once lazily as a performance optimization,
  139. // since we only hit this case if multiple non-contiguous
  140. // changes were made. Note that for contiguous removal
  141. // anywhere in the list, the head and tails would advance
  142. // from either end and pass each other before we get to this
  143. // case and removals would be handled in the final while loop
  144. // without needing to generate the map.
  145. //
  146. // * Example below: The key at `oldTail` was removed (no longer
  147. // in the `newKeyToIndexMap`), so remove that part from the
  148. // DOM and advance just the `oldTail` pointer.
  149. //
  150. // oldHead v v oldTail
  151. // oldKeys: [0, 1, 2, 3, 4, 5, 6]
  152. // newParts: [0, , , , , , 6] <- 5 not in new map: remove
  153. // newKeys: [0, 2, 1, 4, 3, 7, 6] 5 and advance oldTail
  154. // newHead ^ ^ newTail
  155. //
  156. // * Once head and tail cannot move, any mismatches are due to
  157. // either new or moved items; if a new key is in the previous
  158. // "old key to old index" map, move the old part to the new
  159. // location, otherwise create and insert a new part. Note
  160. // that when moving an old part we null its position in the
  161. // oldParts array if it lies between the head and tail so we
  162. // know to skip it when the pointers get there.
  163. //
  164. // * Example below: neither head nor tail match, and neither
  165. // were removed; so find the `newHead` key in the
  166. // `oldKeyToIndexMap`, and move that old part's DOM into the
  167. // next head position (before `oldParts[oldHead]`). Last,
  168. // null the part in the `oldPart` array since it was
  169. // somewhere in the remaining oldParts still to be scanned
  170. // (between the head and tail pointers) so that we know to
  171. // skip that old part on future iterations.
  172. //
  173. // oldHead v v oldTail
  174. // oldKeys: [0, 1, -, 3, 4, 5, 6]
  175. // newParts: [0, 2, , , , , 6] <- stuck: update & move 2
  176. // newKeys: [0, 2, 1, 4, 3, 7, 6] into place and advance
  177. // newHead
  178. // newHead ^ ^ newTail
  179. //
  180. // * Note that for moves/insertions like the one above, a part
  181. // inserted at the head pointer is inserted before the
  182. // current `oldParts[oldHead]`, and a part inserted at the
  183. // tail pointer is inserted before `newParts[newTail+1]`. The
  184. // seeming asymmetry lies in the fact that new parts are
  185. // moved into place outside in, so to the right of the head
  186. // pointer are old parts, and to the right of the tail
  187. // pointer are new parts.
  188. //
  189. // * We always restart back from the top of the algorithm,
  190. // allowing matching and simple updates in place to
  191. // continue...
  192. //
  193. // * Example below: the head pointers once again match, so
  194. // simply update part 1 and record it in the `newParts`
  195. // array. Last, advance both head pointers.
  196. //
  197. // oldHead v v oldTail
  198. // oldKeys: [0, 1, -, 3, 4, 5, 6]
  199. // newParts: [0, 2, 1, , , , 6] <- heads matched: update 1
  200. // newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldHead
  201. // & newHead
  202. // newHead ^ ^ newTail
  203. //
  204. // * As mentioned above, items that were moved as a result of
  205. // being stuck (the final else clause in the code below) are
  206. // marked with null, so we always advance old pointers over
  207. // these so we're comparing the next actual old value on
  208. // either end.
  209. //
  210. // * Example below: `oldHead` is null (already placed in
  211. // newParts), so advance `oldHead`.
  212. //
  213. // oldHead v v oldTail
  214. // oldKeys: [0, 1, -, 3, 4, 5, 6] <- old head already used:
  215. // newParts: [0, 2, 1, , , , 6] advance oldHead
  216. // newKeys: [0, 2, 1, 4, 3, 7, 6]
  217. // newHead ^ ^ newTail
  218. //
  219. // * Note it's not critical to mark old parts as null when they
  220. // are moved from head to tail or tail to head, since they
  221. // will be outside the pointer range and never visited again.
  222. //
  223. // * Example below: Here the old tail key matches the new head
  224. // key, so the part at the `oldTail` position and move its
  225. // DOM to the new head position (before `oldParts[oldHead]`).
  226. // Last, advance `oldTail` and `newHead` pointers.
  227. //
  228. // oldHead v v oldTail
  229. // oldKeys: [0, 1, -, 3, 4, 5, 6]
  230. // newParts: [0, 2, 1, 4, , , 6] <- old tail matches new
  231. // newKeys: [0, 2, 1, 4, 3, 7, 6] head: update & move 4,
  232. // advance oldTail & newHead
  233. // newHead ^ ^ newTail
  234. //
  235. // * Example below: Old and new head keys match, so update the
  236. // old head part in place, and advance the `oldHead` and
  237. // `newHead` pointers.
  238. //
  239. // oldHead v oldTail
  240. // oldKeys: [0, 1, -, 3, 4, 5, 6]
  241. // newParts: [0, 2, 1, 4, 3, ,6] <- heads match: update 3
  242. // newKeys: [0, 2, 1, 4, 3, 7, 6] and advance oldHead &
  243. // newHead
  244. // newHead ^ ^ newTail
  245. //
  246. // * Once the new or old pointers move past each other then all
  247. // we have left is additions (if old list exhausted) or
  248. // removals (if new list exhausted). Those are handled in the
  249. // final while loops at the end.
  250. //
  251. // * Example below: `oldHead` exceeded `oldTail`, so we're done
  252. // with the main loop. Create the remaining part and insert
  253. // it at the new head position, and the update is complete.
  254. //
  255. // (oldHead > oldTail)
  256. // oldKeys: [0, 1, -, 3, 4, 5, 6]
  257. // newParts: [0, 2, 1, 4, 3, 7 ,6] <- create and insert 7
  258. // newKeys: [0, 2, 1, 4, 3, 7, 6]
  259. // newHead ^ newTail
  260. //
  261. // * Note that the order of the if/else clauses is not
  262. // important to the algorithm, as long as the null checks
  263. // come first (to ensure we're always working on valid old
  264. // parts) and that the final else clause comes last (since
  265. // that's where the expensive moves occur). The order of
  266. // remaining clauses is is just a simple guess at which cases
  267. // will be most common.
  268. //
  269. // * Note, we could calculate the longest
  270. // increasing subsequence (LIS) of old items in new position,
  271. // and only move those not in the LIS set. However that costs
  272. // O(nlogn) time and adds a bit more code, and only helps
  273. // make rare types of mutations require fewer moves. The
  274. // above handles removes, adds, reversal, swaps, and single
  275. // moves of contiguous items in linear time, in the minimum
  276. // number of moves. As the number of multiple moves where LIS
  277. // might help approaches a random shuffle, the LIS
  278. // optimization becomes less helpful, so it seems not worth
  279. // the code at this point. Could reconsider if a compelling
  280. // case arises.
  281. while (oldHead <= oldTail && newHead <= newTail) {
  282. if (oldParts[oldHead] === null) {
  283. // `null` means old part at head has already been used
  284. // below; skip
  285. oldHead++;
  286. }
  287. else if (oldParts[oldTail] === null) {
  288. // `null` means old part at tail has already been used
  289. // below; skip
  290. oldTail--;
  291. }
  292. else if (oldKeys[oldHead] === newKeys[newHead]) {
  293. // Old head matches new head; update in place
  294. newParts[newHead] = setChildPartValue(oldParts[oldHead], newValues[newHead]);
  295. oldHead++;
  296. newHead++;
  297. }
  298. else if (oldKeys[oldTail] === newKeys[newTail]) {
  299. // Old tail matches new tail; update in place
  300. newParts[newTail] = setChildPartValue(oldParts[oldTail], newValues[newTail]);
  301. oldTail--;
  302. newTail--;
  303. }
  304. else if (oldKeys[oldHead] === newKeys[newTail]) {
  305. // Old head matches new tail; update and move to new tail
  306. newParts[newTail] = setChildPartValue(oldParts[oldHead], newValues[newTail]);
  307. insertPart(containerPart, newParts[newTail + 1], oldParts[oldHead]);
  308. oldHead++;
  309. newTail--;
  310. }
  311. else if (oldKeys[oldTail] === newKeys[newHead]) {
  312. // Old tail matches new head; update and move to new head
  313. newParts[newHead] = setChildPartValue(oldParts[oldTail], newValues[newHead]);
  314. insertPart(containerPart, oldParts[oldHead], oldParts[oldTail]);
  315. oldTail--;
  316. newHead++;
  317. }
  318. else {
  319. if (newKeyToIndexMap === undefined) {
  320. // Lazily generate key-to-index maps, used for removals &
  321. // moves below
  322. newKeyToIndexMap = generateMap(newKeys, newHead, newTail);
  323. oldKeyToIndexMap = generateMap(oldKeys, oldHead, oldTail);
  324. }
  325. if (!newKeyToIndexMap.has(oldKeys[oldHead])) {
  326. // Old head is no longer in new list; remove
  327. removePart(oldParts[oldHead]);
  328. oldHead++;
  329. }
  330. else if (!newKeyToIndexMap.has(oldKeys[oldTail])) {
  331. // Old tail is no longer in new list; remove
  332. removePart(oldParts[oldTail]);
  333. oldTail--;
  334. }
  335. else {
  336. // Any mismatches at this point are due to additions or
  337. // moves; see if we have an old part we can reuse and move
  338. // into place
  339. const oldIndex = oldKeyToIndexMap.get(newKeys[newHead]);
  340. const oldPart = oldIndex !== undefined ? oldParts[oldIndex] : null;
  341. if (oldPart === null) {
  342. // No old part for this value; create a new one and
  343. // insert it
  344. const newPart = insertPart(containerPart, oldParts[oldHead]);
  345. setChildPartValue(newPart, newValues[newHead]);
  346. newParts[newHead] = newPart;
  347. }
  348. else {
  349. // Reuse old part
  350. newParts[newHead] = setChildPartValue(oldPart, newValues[newHead]);
  351. insertPart(containerPart, oldParts[oldHead], oldPart);
  352. // This marks the old part as having been used, so that
  353. // it will be skipped in the first two checks above
  354. oldParts[oldIndex] = null;
  355. }
  356. newHead++;
  357. }
  358. }
  359. }
  360. // Add parts for any remaining new values
  361. while (newHead <= newTail) {
  362. // For all remaining additions, we insert before last new
  363. // tail, since old pointers are no longer valid
  364. const newPart = insertPart(containerPart, newParts[newTail + 1]);
  365. setChildPartValue(newPart, newValues[newHead]);
  366. newParts[newHead++] = newPart;
  367. }
  368. // Remove any remaining unused old parts
  369. while (oldHead <= oldTail) {
  370. const oldPart = oldParts[oldHead++];
  371. if (oldPart !== null) {
  372. removePart(oldPart);
  373. }
  374. }
  375. // Save order of new parts for next round
  376. this._itemKeys = newKeys;
  377. // Directly set part value, bypassing it's dirty-checking
  378. setCommittedValue(containerPart, newParts);
  379. return noChange;
  380. }
  381. }
  382. /**
  383. * A directive that repeats a series of values (usually `TemplateResults`)
  384. * generated from an iterable, and updates those items efficiently when the
  385. * iterable changes based on user-provided `keys` associated with each item.
  386. *
  387. * Note that if a `keyFn` is provided, strict key-to-DOM mapping is maintained,
  388. * meaning previous DOM for a given key is moved into the new position if
  389. * needed, and DOM will never be reused with values for different keys (new DOM
  390. * will always be created for new keys). This is generally the most efficient
  391. * way to use `repeat` since it performs minimum unnecessary work for insertions
  392. * and removals.
  393. *
  394. * The `keyFn` takes two parameters, the item and its index, and returns a unique key value.
  395. *
  396. * ```js
  397. * html`
  398. * <ol>
  399. * ${repeat(this.items, (item) => item.id, (item, index) => {
  400. * return html`<li>${index}: ${item.name}</li>`;
  401. * })}
  402. * </ol>
  403. * `
  404. * ```
  405. *
  406. * **Important**: If providing a `keyFn`, keys *must* be unique for all items in a
  407. * given call to `repeat`. The behavior when two or more items have the same key
  408. * is undefined.
  409. *
  410. * If no `keyFn` is provided, this directive will perform similar to mapping
  411. * items to values, and DOM will be reused against potentially different items.
  412. */
  413. export const repeat = directive(RepeatDirective);
  414. //# sourceMappingURL=repeat.js.map