/
repeat.ts
487 lines (468 loc) · 19.4 KB
/
repeat.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
import {ChildPart, noChange} from '../lit-html.js';
import {directive, Directive, PartInfo, PartType} from '../directive.js';
import {
insertPart,
getCommittedValue,
removePart,
setCommittedValue,
setChildPartValue,
} from '../directive-helpers.js';
export type KeyFn<T> = (item: T, index: number) => unknown;
export type ItemTemplate<T> = (item: T, index: number) => unknown;
// Helper for generating a map of array item to its index over a subset
// of an array (used to lazily generate `newKeyToIndexMap` and
// `oldKeyToIndexMap`)
const generateMap = (list: unknown[], start: number, end: number) => {
const map = new Map<unknown, number>();
for (let i = start; i <= end; i++) {
map.set(list[i], i);
}
return map;
};
class RepeatDirective extends Directive {
private _itemKeys?: unknown[];
constructor(partInfo: PartInfo) {
super(partInfo);
if (partInfo.type !== PartType.CHILD) {
throw new Error('repeat() can only be used in text expressions');
}
}
private _getValuesAndKeys<T>(
items: Iterable<T>,
keyFnOrTemplate: KeyFn<T> | ItemTemplate<T>,
template?: ItemTemplate<T>
) {
let keyFn: KeyFn<T> | undefined;
if (template === undefined) {
template = keyFnOrTemplate;
} else if (keyFnOrTemplate !== undefined) {
keyFn = keyFnOrTemplate as KeyFn<T>;
}
const keys = [];
const values = [];
let index = 0;
for (const item of items) {
keys[index] = keyFn ? keyFn(item, index) : index;
values[index] = template!(item, index);
index++;
}
return {
values,
keys,
};
}
render<T>(items: Iterable<T>, template: ItemTemplate<T>): Array<unknown>;
render<T>(
items: Iterable<T>,
keyFn: KeyFn<T> | ItemTemplate<T>,
template: ItemTemplate<T>
): Array<unknown>;
render<T>(
items: Iterable<T>,
keyFnOrTemplate: KeyFn<T> | ItemTemplate<T>,
template?: ItemTemplate<T>
) {
return this._getValuesAndKeys(items, keyFnOrTemplate, template).values;
}
override update<T>(
containerPart: ChildPart,
[items, keyFnOrTemplate, template]: [
Iterable<T>,
KeyFn<T> | ItemTemplate<T>,
ItemTemplate<T>
]
) {
// Old part & key lists are retrieved from the last update (which may
// be primed by hydration)
const oldParts = getCommittedValue(
containerPart
) as Array<ChildPart | null>;
const {values: newValues, keys: newKeys} = this._getValuesAndKeys(
items,
keyFnOrTemplate,
template
);
// We check that oldParts, the committed value, is an Array as an
// indicator that the previous value came from a repeat() call. If
// oldParts is not an Array then this is the first render and we return
// an array for lit-html's array handling to render, and remember the
// keys.
if (!Array.isArray(oldParts)) {
this._itemKeys = newKeys;
return newValues;
}
// In SSR hydration it's possible for oldParts to be an array but for us
// to not have item keys because the update() hasn't run yet. We set the
// keys to an empty array. This will cause all oldKey/newKey comparisons
// to fail and execution to fall to the last nested brach below which
// reuses the oldPart.
const oldKeys = (this._itemKeys ??= []);
// 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: ChildPart[] = [];
// Maps from key to index for current and previous update; these
// are generated lazily only when needed as a performance
// optimization, since they are only required for multiple
// non-contiguous changes in the list, which are less common.
let newKeyToIndexMap!: Map<unknown, number>;
let oldKeyToIndexMap!: Map<unknown, number>;
// Head and tail pointers to old parts and new values
let oldHead = 0;
let oldTail = oldParts.length - 1;
let newHead = 0;
let newTail = newValues.length - 1;
// Overview of O(n) reconciliation algorithm (general approach
// based on ideas found in ivi, vue, snabbdom, etc.):
//
// * We start with the list of old parts and new values (and
// arrays of their respective keys), head/tail pointers into
// each, and we build up the new list of parts by updating
// (and when needed, moving) old parts or creating new ones.
// The initial scenario might look like this (for brevity of
// the diagrams, the numbers in the array reflect keys
// associated with the old parts or new values, although keys
// and parts/values are actually stored in parallel arrays
// indexed using the same head/tail pointers):
//
// oldHead v v oldTail
// oldKeys: [0, 1, 2, 3, 4, 5, 6]
// newParts: [ , , , , , , ]
// newKeys: [0, 2, 1, 4, 3, 7, 6] <- reflects the user's new
// item order
// newHead ^ ^ newTail
//
// * Iterate old & new lists from both sides, updating,
// swapping, or removing parts at the head/tail locations
// until neither head nor tail can move.
//
// * Example below: keys at head pointers match, so update old
// part 0 in-place (no need to move it) and record part 0 in
// the `newParts` list. The last thing we do is advance the
// `oldHead` and `newHead` pointers (will be reflected in the
// next diagram).
//
// oldHead v v oldTail
// oldKeys: [0, 1, 2, 3, 4, 5, 6]
// newParts: [0, , , , , , ] <- heads matched: update 0
// newKeys: [0, 2, 1, 4, 3, 7, 6] and 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
// newKeys: [0, 2, 1, 4, 3, 7, 6] and 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
// newKeys: [0, 2, 1, 4, 3, 7, 6] 5 and 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
// newKeys: [0, 2, 1, 4, 3, 7, 6] into place 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
// newKeys: [0, 2, 1, 4, 3, 7, 6] and 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:
// newParts: [0, 2, 1, , , , 6] advance 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
// newKeys: [0, 2, 1, 4, 3, 7, 6] head: update & 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
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance 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.
//
// * 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] = setChildPartValue(
oldParts[oldHead]!,
newValues[newHead]
);
oldHead++;
newHead++;
} else if (oldKeys[oldTail] === newKeys[newTail]) {
// Old tail matches new tail; update in place
newParts[newTail] = setChildPartValue(
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] = setChildPartValue(
oldParts[oldHead]!,
newValues[newTail]
);
insertPart(containerPart, newParts[newTail + 1], oldParts[oldHead]!);
oldHead++;
newTail--;
} else if (oldKeys[oldTail] === newKeys[newHead]) {
// Old tail matches new head; update and move to new head
newParts[newHead] = setChildPartValue(
oldParts[oldTail]!,
newValues[newHead]
);
insertPart(containerPart, oldParts[oldHead]!, oldParts[oldTail]!);
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 = insertPart(containerPart, oldParts[oldHead]!);
setChildPartValue(newPart, newValues[newHead]);
newParts[newHead] = newPart;
} else {
// Reuse old part
newParts[newHead] = setChildPartValue(oldPart, newValues[newHead]);
insertPart(containerPart, oldParts[oldHead]!, oldPart);
// 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++;
}
}
}
// 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 = insertPart(containerPart, newParts[newTail + 1]);
setChildPartValue(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
this._itemKeys = newKeys;
// Directly set part value, bypassing it's dirty-checking
setCommittedValue(containerPart, newParts);
return noChange;
}
}
export interface RepeatDirectiveFn {
<T>(
items: Iterable<T>,
keyFnOrTemplate: KeyFn<T> | ItemTemplate<T>,
template?: ItemTemplate<T>
): unknown;
<T>(items: Iterable<T>, template: ItemTemplate<T>): unknown;
<T>(
items: Iterable<T>,
keyFn: KeyFn<T> | ItemTemplate<T>,
template: ItemTemplate<T>
): unknown;
}
/**
* A directive that repeats a series of values (usually `TemplateResults`)
* generated from an iterable, and updates those items efficiently when the
* iterable changes based on user-provided `keys` associated with each item.
*
* Note that if a `keyFn` is provided, strict key-to-DOM mapping is maintained,
* meaning previous DOM for a given key is moved into the new position if
* needed, and DOM will never be reused with values for different keys (new DOM
* will always be created for new keys). This is generally the most efficient
* way to use `repeat` since it performs minimum unnecessary work for insertions
* and removals.
*
* The `keyFn` takes two parameters, the item and its index, and returns a unique key value.
*
* ```js
* html`
* <ol>
* ${repeat(this.items, (item) => item.id, (item, index) => {
* return html`<li>${index}: ${item.name}</li>`;
* })}
* </ol>
* `
* ```
*
* **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(RepeatDirective) as RepeatDirectiveFn;
/**
* The type of the class that powers this directive. Necessary for naming the
* directive's return type.
*/
export type {RepeatDirective};