Skip to content

Commit 74b1442

Browse files
committed
list keying
1 parent cb14f1b commit 74b1442

10 files changed

+70
-87
lines changed

src/html.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import type { BoundTemplateInstance, CustomPartConstructor, Displayable, /*Key,*/ Renderable } from './types.ts'
1+
import type { BoundTemplateInstance, CustomPartConstructor, Displayable, Key, Renderable } from './types.ts'
22

33
export { CustomPartConstructor as CustomPart, Displayable, Renderable }
44

55
export function html(
66
statics: TemplateStringsArray,
77
...dynamics: Array<Displayable | CustomPartConstructor>
88
): BoundTemplateInstance
9-
// export function keyed<T extends Displayable & object>(value: T, key: Key): T
9+
export function keyed<T extends Displayable & object>(value: T, key: Key): T
1010
export function invalidate(renderable: Renderable): Promise<void>
1111
export function onUnmount(renderable: Renderable, callback: () => void): void
1212

src/html.js

Lines changed: 59 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @typedef {import('./types.ts').Displayable} Displayable
44
* @typedef {import('./types.ts').Renderable} Renderable
55
* @typedef {import('./types.ts').CompiledTemplate} CompiledTemplate
6-
* @_typedef {import('./types.ts').Key} Key
6+
* @typedef {import('./types.ts').Key} Key
77
* @typedef {import('./types.ts').CustomPartConstructor} CustomPartConstructor
88
* @typedef {import('./types.ts').CustomPartInstance} CustomPartInstance
99
*/
@@ -136,7 +136,7 @@ class BoundTemplateInstance {
136136
}
137137

138138
export class Root {
139-
// /** @type {Key | undefined} */ _key
139+
/** @type {Key | undefined} */ _key
140140

141141
constructor(span) {
142142
this.span = span
@@ -381,6 +381,13 @@ export function onUnmount(renderable, callback) {
381381
}
382382
}
383383

384+
const keys = new WeakMap()
385+
export function keyed(renderable, key) {
386+
if (keys.has(renderable)) throw new Error('renderable already has a key')
387+
keys.set(renderable, key)
388+
return renderable
389+
}
390+
384391
/** @implements {Part} */
385392
class ChildPart {
386393
#childIndex
@@ -476,82 +483,60 @@ class ChildPart {
476483
let i = 0
477484
let offset = this.#span.end
478485
for (const item of value) {
479-
const root = (this.#roots[i] ??= new Root(new Span(this.#span.parentNode, offset, offset)))
486+
// @ts-expect-error -- WeakMap lookups of non-objects always return undefined, which is fine
487+
const key = keys.get(item) ?? item
488+
489+
if (key !== undefined) {
490+
let first = this.#roots[i]
491+
let i1 = i
492+
if (first?._key !== key) {
493+
let i2 = this.#roots.findIndex(root => root?._key === key)
494+
if (i2 !== -1) {
495+
let second = this.#roots[i2]
496+
497+
if (second.span.start < first.span.start) {
498+
// first must refer to the lower index.
499+
;[first, second] = [second, first]
500+
;[i1, i2] = [i2, i1]
501+
}
502+
assert(first.span.start < second.span.start)
503+
assert(i1 < i2)
504+
505+
// swap the contents of the spans
506+
const content1 = second.span.extractContents()
507+
const content2 = first.span.extractContents()
508+
second.span.insertNode(content2)
509+
first.span.insertNode(content1)
510+
511+
// swap the spans back
512+
;[first.span, second.span] = [second.span, first.span]
513+
514+
// swap the roots
515+
this.#roots[i1] = second
516+
this.#roots[i2] = first
517+
518+
const difference = second.span.length - first.span.length
519+
for (let j = i1 + 1; j <= i2; j++) {
520+
this.#roots[j].span.start += difference
521+
this.#roots[j].span.end += difference
522+
}
523+
}
524+
}
525+
}
526+
527+
const root = (this.#roots[i++] ??= new Root(new Span(this.#span.parentNode, offset, offset)))
480528
root.render(item)
529+
// console.log(offset, root.span.end)
481530
offset = root.span.end
482-
i++
483-
484-
// @_ts-expect-error -- WeakMap lookups of non-objects always return undefined, which is fine
485-
// const key = keys.get(item) ?? item
486-
487-
// if (key !== undefined) {
488-
// let first = this.#roots[i]
489-
// let i1 = i
490-
// if (first?._key !== key) {
491-
// let i2 = this.#roots.findIndex(root => root?._key === key)
492-
// if (i2 !== -1) {
493-
// let second = this.#roots[i2]
494-
495-
// if (second.span.start < first.span.start) {
496-
// // first must refer to the lower index.
497-
// ;[first, second] = [second, first]
498-
// ;[i1, i2] = [i2, i1]
499-
// }
500-
501-
// // swap the contents of the spans
502-
// const content1 = second.span.extractContents()
503-
// const content2 = first.span.extractContents()
504-
// second.span.insertNode(content2)
505-
// first.span.insertNode(content1)
506-
507-
// // swap the spans back
508-
// ;[first.span, second.span] = [second.span, first.span]
509-
510-
// // swap the roots
511-
// this.#roots[i1] = second
512-
// this.#roots[i2] = first
513-
514-
// const difference = second.span.length - first.span.length
515-
// for (let j = i1 + 1; j <= i2; j++) {
516-
// this.#roots[j].span.start += difference
517-
// this.#roots[j].span.end += difference
518-
// }
519-
// }
520-
// }
521-
// }
522-
523-
// const root = (this.#roots[i++] ??= new Root(new Span(this.#span.parentNode, offset, offset)))
524-
// root.render(item)
525-
// console.log(offset, root.span.end)
526-
// offset = root.span.end
527-
528-
// // TODO: make this a weak relationship, because if key is collected, the comparison will always be false.
529-
// if (key !== undefined) root._key = key
530-
// }
531-
532-
// // and now remove excess roots if the iterable has shrunk.
533-
// console.log([...this.#roots])
534-
// const extra = this.#roots.splice(i)
535-
// this.#roots.length = i
536-
// // extra.sort((a, b) => b.span.start - a.span.start)
537-
// extra.reverse()
538-
// for (const root of extra) {
539-
// console.log(
540-
// 'detach',
541-
// [...root.span.parentNode.childNodes],
542-
// root.span.start,
543-
// root.span.end,
544-
// root._instance?.template._content.textContent,
545-
// [...root.span],
546-
// )
547-
// root.detach()
548-
// root.span.deleteContents()
549-
// console.log('after detach', [...root.span.parentNode.childNodes], root.span.start, root.span.end)
531+
532+
// TODO: make this a weak relationship, because if key is collected, the comparison will always be false.
533+
if (key !== undefined) root._key = key
550534
}
551535

552536
// and now remove excess roots if the iterable has shrunk.
553537
while (this.#roots.length > i) {
554-
const root = /** @type {Root} */ (this.#roots.pop())
538+
const root = this.#roots.pop()
539+
assert(root)
555540
root.detach()
556541
root.span.deleteContents()
557542
}
@@ -578,6 +563,8 @@ class ChildPart {
578563

579564
if (this.#value != null && value !== null && !(this.#value instanceof Node) && !(value instanceof Node)) {
580565
// we previously rendered a string, and we're rendering a string again.
566+
assert(this.#span.length === 1)
567+
assert(this.#span.parentNode.childNodes[this.#span.start] instanceof Text)
581568
this.#span.parentNode.childNodes[this.#span.start].data = value
582569
} else {
583570
this.#span.deleteContents()

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export declare class BoundTemplateInstance {
1111
#private
1212
}
1313

14-
// export type Key = string | number | bigint | boolean | symbol | object | null
14+
export type Key = string | number | bigint | boolean | symbol | object | null
1515

1616
export declare class Span {
1717
parentNode: Node

test/list-reorder-explicit-keyed.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
import { Root, html } from 'dhtml'
1+
import { Root, html, keyed } from 'dhtml'
22
import { expect, test } from 'vitest'
33

4-
function keyed<T>(renderable: T, _key: unknown) {
5-
return renderable
6-
}
7-
8-
test.todo('list-reorder-explicit-keyed', () => {
4+
test('list-reorder-explicit-keyed', () => {
95
const root = document.createElement('div')
106
const r = Root.appendInto(root)
117

test/list-reorder-implicit-keyed-renderable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Root, html } from 'dhtml'
22
import { expect, test } from 'vitest'
33

4-
test.todo('list-reorder-implicit-keyed-renderable', () => {
4+
test('list-reorder-implicit-keyed-renderable', () => {
55
const root = document.createElement('div')
66
const r = Root.appendInto(root)
77

test/list-reorder-implicit-keyed-resize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Root, html } from 'dhtml'
22
import { expect, test } from 'vitest'
33

4-
test.todo('list-reorder-implicit-keyed-resize', () => {
4+
test('list-reorder-implicit-keyed-resize', () => {
55
const root = document.createElement('div')
66
const r = Root.appendInto(root)
77

test/list-reorder-implicit-keyed.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Root, html } from 'dhtml'
22
import { expect, test } from 'vitest'
33

4-
test.todo('list-reorder-implicit-keyed', () => {
4+
test('list-reorder-implicit-keyed', () => {
55
const root = document.createElement('div')
66
const r = Root.appendInto(root)
77

test/list-reorder-unkeyed.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Root, html } from 'dhtml'
22
import { expect, test } from 'vitest'
33

4-
test.todo('list-reorder-unkeyed', () => {
4+
test('list-reorder-unkeyed', () => {
55
const root = document.createElement('div')
66
const r = Root.appendInto(root)
77

test/list-shift.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Root, html } from 'dhtml'
22
import { expect, test } from 'vitest'
33

4-
test.todo('list-shift', () => {
4+
test('list-shift', () => {
55
const root = document.createElement('div')
66
const r = Root.appendInto(root)
77

test/list-swap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Root, html } from 'dhtml'
22
import { expect, test } from 'vitest'
33

4-
test.todo('list-swap', () => {
4+
test('list-swap', () => {
55
const root = document.createElement('div')
66
const r = Root.appendInto(root)
77

0 commit comments

Comments
 (0)