@matthewp/zebra 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@matthewp/zebra",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/view.ts",
7
+ "./list": "./src/list.ts",
8
+ "./server": "./src/server.ts"
9
+ }
10
+ }
package/src/list.ts ADDED
@@ -0,0 +1,146 @@
1
+ import { View } from './view.ts';
2
+
3
+ type ViewConstructor = new () => View;
4
+
5
+ export class List<T = Record<string, unknown>> {
6
+ private ViewClass: ViewConstructor;
7
+ private keyFn: (item: T) => unknown;
8
+ private views: (View | null)[] = [];
9
+ private keys: unknown[] = [];
10
+ private container: HTMLElement | null = null;
11
+
12
+ constructor(ViewClass: ViewConstructor, keyFn: (item: T) => unknown) {
13
+ this.ViewClass = ViewClass;
14
+ this.keyFn = keyFn;
15
+ }
16
+
17
+ template(items?: T[]): string {
18
+ if (!items || items.length === 0) return '';
19
+ return items.map(item => {
20
+ let view = new this.ViewClass();
21
+ return view.template(item as unknown as Record<string, unknown>);
22
+ }).join('');
23
+ }
24
+
25
+ mount(container: HTMLElement, items?: T[]) {
26
+ this.container = container;
27
+ let children = Array.from(container.children) as HTMLElement[];
28
+ if (children.length > 0 && items) {
29
+ for (let i = 0; i < children.length && i < items.length; i++) {
30
+ let view = new this.ViewClass();
31
+ view.mount(children[i]);
32
+ view.update(items[i] as unknown as Record<string, unknown>);
33
+ this.views.push(view);
34
+ this.keys.push(this.keyFn(items[i]));
35
+ }
36
+ }
37
+ }
38
+
39
+ private updateView(view: View, item: T) {
40
+ view.update(item as unknown as Record<string, unknown>);
41
+ }
42
+
43
+ update(items: T[]) {
44
+ let container = this.container!;
45
+ let oldViews = this.views;
46
+ let oldKeys = this.keys;
47
+ let newKeys = items.map(this.keyFn);
48
+ let newViews: (View | null)[] = new Array(items.length).fill(null);
49
+
50
+ let oldHead = 0;
51
+ let oldTail = oldViews.length - 1;
52
+ let newHead = 0;
53
+ let newTail = items.length - 1;
54
+
55
+ let oldKeyToIndex: Map<unknown, number> | undefined;
56
+
57
+ while (oldHead <= oldTail && newHead <= newTail) {
58
+ if (oldViews[oldHead] === null) {
59
+ oldHead++;
60
+ } else if (oldViews[oldTail] === null) {
61
+ oldTail--;
62
+ } else if (oldKeys[oldHead] === newKeys[newHead]) {
63
+ // Head-Head match
64
+ newViews[newHead] = oldViews[oldHead];
65
+ this.updateView(oldViews[oldHead]!, items[newHead]);
66
+ oldHead++;
67
+ newHead++;
68
+ } else if (oldKeys[oldTail] === newKeys[newTail]) {
69
+ // Tail-Tail match
70
+ newViews[newTail] = oldViews[oldTail];
71
+ this.updateView(oldViews[oldTail]!, items[newTail]);
72
+ oldTail--;
73
+ newTail--;
74
+ } else if (oldKeys[oldHead] === newKeys[newTail]) {
75
+ // Head-Tail match: move old head to after old tail
76
+ newViews[newTail] = oldViews[oldHead];
77
+ this.updateView(oldViews[oldHead]!, items[newTail]);
78
+ oldViews[oldTail]!.el.after(oldViews[oldHead]!.el);
79
+ oldHead++;
80
+ newTail--;
81
+ } else if (oldKeys[oldTail] === newKeys[newHead]) {
82
+ // Tail-Head match: move old tail to before old head
83
+ newViews[newHead] = oldViews[oldTail];
84
+ this.updateView(oldViews[oldTail]!, items[newHead]);
85
+ oldViews[oldHead]!.el.before(oldViews[oldTail]!.el);
86
+ oldTail--;
87
+ newHead++;
88
+ } else {
89
+ // Build map lazily
90
+ if (!oldKeyToIndex) {
91
+ oldKeyToIndex = new Map();
92
+ for (let i = oldHead; i <= oldTail; i++) {
93
+ if (oldViews[i] !== null) {
94
+ oldKeyToIndex.set(oldKeys[i], i);
95
+ }
96
+ }
97
+ }
98
+
99
+ let oldIndex = oldKeyToIndex.get(newKeys[newHead]);
100
+ if (oldIndex === undefined) {
101
+ // New item
102
+ let view = new this.ViewClass();
103
+ view.createAndMount();
104
+ newViews[newHead] = view;
105
+ oldViews[oldHead]!.el.before(view.el);
106
+ this.updateView(view, items[newHead]);
107
+ } else {
108
+ // Move existing
109
+ let view = oldViews[oldIndex]!;
110
+ this.updateView(view, items[newHead]);
111
+ newViews[newHead] = view;
112
+ oldViews[oldHead]!.el.before(view.el);
113
+ oldViews[oldIndex] = null;
114
+ }
115
+ newHead++;
116
+ }
117
+ }
118
+
119
+ // Remove remaining old items
120
+ while (oldHead <= oldTail) {
121
+ if (oldViews[oldHead] !== null) {
122
+ oldViews[oldHead]!.el.remove();
123
+ }
124
+ oldHead++;
125
+ }
126
+
127
+ // Add remaining new items
128
+ while (newHead <= newTail) {
129
+ let view = new this.ViewClass();
130
+ view.createAndMount();
131
+ newViews[newHead] = view;
132
+ let el = view.el;
133
+ let ref = newViews[newTail + 1]?.el;
134
+ if (ref) {
135
+ ref.before(el);
136
+ } else {
137
+ container.append(el);
138
+ }
139
+ this.updateView(view, items[newHead]);
140
+ newHead++;
141
+ }
142
+
143
+ this.views = newViews;
144
+ this.keys = newKeys;
145
+ }
146
+ }
package/src/server.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { View } from './view.ts';
2
+
3
+ export function renderToString(view: View, props?: Record<string, unknown>): string {
4
+ return view.template(props);
5
+ }
package/src/view.ts ADDED
@@ -0,0 +1,34 @@
1
+ export class View {
2
+ el!: HTMLElement;
3
+
4
+ createElement(): HTMLElement {
5
+ let tpl = document.createElement('template');
6
+ tpl.innerHTML = this.template();
7
+ this.el = document.importNode(tpl.content, true).firstElementChild as HTMLElement;
8
+ return this.el;
9
+ }
10
+
11
+ template(_props?: any): string {
12
+ return '';
13
+ }
14
+
15
+ createAndMount(): void {
16
+ this.mount(this.createElement());
17
+ }
18
+
19
+ mount(el: HTMLElement): void {
20
+ this.el = el;
21
+ }
22
+
23
+ update(_data?: Record<string, unknown>): HTMLElement {
24
+ return this.el;
25
+ }
26
+ }
27
+
28
+ interface Slottable {
29
+ template(props?: any): string;
30
+ }
31
+
32
+ export function slot<T extends Slottable>(target: T, ...args: Parameters<T['template']>): string {
33
+ return target.template(...args);
34
+ }