@derivesome/tree 0.1.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.~undo-tree~ +4 -0
- package/.tsconfig.json.~undo-tree~ +4 -0
- package/dist/cjs/brands.d.ts +3 -0
- package/dist/cjs/brands.d.ts.map +1 -0
- package/dist/cjs/brands.js +5 -0
- package/dist/cjs/brands.js.map +1 -0
- package/dist/cjs/context.d.ts +28 -0
- package/dist/cjs/context.d.ts.map +1 -0
- package/dist/cjs/context.js +48 -0
- package/dist/cjs/context.js.map +1 -0
- package/dist/cjs/index.d.ts +6 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +22 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/mount.d.ts +6 -0
- package/dist/cjs/mount.d.ts.map +1 -0
- package/dist/cjs/mount.js +209 -0
- package/dist/cjs/mount.js.map +1 -0
- package/dist/cjs/props.d.ts +12 -0
- package/dist/cjs/props.d.ts.map +1 -0
- package/dist/cjs/props.js +80 -0
- package/dist/cjs/props.js.map +1 -0
- package/dist/cjs/renderer.d.ts +29 -0
- package/dist/cjs/renderer.d.ts.map +1 -0
- package/dist/cjs/renderer.js +3 -0
- package/dist/cjs/renderer.js.map +1 -0
- package/dist/cjs/tree-node-like.d.ts +8 -0
- package/dist/cjs/tree-node-like.d.ts.map +1 -0
- package/dist/cjs/tree-node-like.js +4 -0
- package/dist/cjs/tree-node-like.js.map +1 -0
- package/dist/cjs/tree.d.ts +46 -0
- package/dist/cjs/tree.d.ts.map +1 -0
- package/dist/cjs/tree.js +154 -0
- package/dist/cjs/tree.js.map +1 -0
- package/dist/cjs/velement.d.ts +185 -0
- package/dist/cjs/velement.d.ts.map +1 -0
- package/dist/cjs/velement.js +874 -0
- package/dist/cjs/velement.js.map +1 -0
- package/dist/esm/brands.d.ts +3 -0
- package/dist/esm/brands.d.ts.map +1 -0
- package/dist/esm/brands.js +5 -0
- package/dist/esm/brands.js.map +1 -0
- package/dist/esm/context.d.ts +28 -0
- package/dist/esm/context.d.ts.map +1 -0
- package/dist/esm/context.js +48 -0
- package/dist/esm/context.js.map +1 -0
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +22 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/mount.d.ts +6 -0
- package/dist/esm/mount.d.ts.map +1 -0
- package/dist/esm/mount.js +209 -0
- package/dist/esm/mount.js.map +1 -0
- package/dist/esm/props.d.ts +12 -0
- package/dist/esm/props.d.ts.map +1 -0
- package/dist/esm/props.js +80 -0
- package/dist/esm/props.js.map +1 -0
- package/dist/esm/renderer.d.ts +29 -0
- package/dist/esm/renderer.d.ts.map +1 -0
- package/dist/esm/renderer.js +3 -0
- package/dist/esm/renderer.js.map +1 -0
- package/dist/esm/tree-node-like.d.ts +8 -0
- package/dist/esm/tree-node-like.d.ts.map +1 -0
- package/dist/esm/tree-node-like.js +4 -0
- package/dist/esm/tree-node-like.js.map +1 -0
- package/dist/esm/tree.d.ts +46 -0
- package/dist/esm/tree.d.ts.map +1 -0
- package/dist/esm/tree.js +154 -0
- package/dist/esm/tree.js.map +1 -0
- package/dist/esm/velement.d.ts +185 -0
- package/dist/esm/velement.d.ts.map +1 -0
- package/dist/esm/velement.js +874 -0
- package/dist/esm/velement.js.map +1 -0
- package/package.json +46 -0
- package/package.json~ +52 -0
- package/src/#mount.test.ts# +372 -0
- package/src/.brands.ts.~undo-tree~ +6 -0
- package/src/.context.ts.~undo-tree~ +6 -0
- package/src/.index.ts.~undo-tree~ +11 -0
- package/src/.mount.test.ts.~undo-tree~ +438 -0
- package/src/.mount.ts.~undo-tree~ +70 -0
- package/src/.node-like.ts.~undo-tree~ +8 -0
- package/src/.props.ts.~undo-tree~ +125 -0
- package/src/.renderer.ts.~undo-tree~ +18 -0
- package/src/.tree-node-like.ts.~undo-tree~ +12 -0
- package/src/.tree.ts.~undo-tree~ +46 -0
- package/src/.velement.ts.~undo-tree~ +1739 -0
- package/src/brands.ts +2 -0
- package/src/brands.ts~ +0 -0
- package/src/context.ts +61 -0
- package/src/context.ts~ +0 -0
- package/src/index.ts +5 -0
- package/src/index.ts~ +4 -0
- package/src/mount.test.ts +405 -0
- package/src/mount.test.ts~ +375 -0
- package/src/mount.ts +332 -0
- package/src/mount.ts~ +306 -0
- package/src/node-like.ts~ +0 -0
- package/src/props.ts +99 -0
- package/src/props.ts~ +86 -0
- package/src/renderer.ts +37 -0
- package/src/renderer.ts~ +37 -0
- package/src/tree-node-like.ts +8 -0
- package/src/tree-node-like.ts~ +6 -0
- package/src/tree.ts +226 -0
- package/src/tree.ts~ +227 -0
- package/src/velement.ts +990 -0
- package/src/velement.ts~ +966 -0
- package/tsconfig.cjs.json +10 -0
- package/tsconfig.esm.json +10 -0
- package/tsconfig.json +23 -0
- package/tsconfig.json~ +23 -0
package/src/brands.ts
ADDED
package/src/brands.ts~
ADDED
|
File without changes
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { makeGlobalPrefixed, Stack } from "@derivesome/core";
|
|
2
|
+
|
|
3
|
+
export const TreeScope = makeGlobalPrefixed(
|
|
4
|
+
class TreeScope {
|
|
5
|
+
cleanups: Set<() => void> = new Set();
|
|
6
|
+
},
|
|
7
|
+
"TreeScope",
|
|
8
|
+
);
|
|
9
|
+
export type TreeScope = InstanceType<typeof TreeScope>;
|
|
10
|
+
|
|
11
|
+
export const TreeContext = makeGlobalPrefixed(
|
|
12
|
+
class TreeContext {
|
|
13
|
+
static scopes: Stack<TreeScope> = new Stack([new TreeScope()]);
|
|
14
|
+
|
|
15
|
+
static get scope(): TreeScope {
|
|
16
|
+
return this.scopes.current;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static push(): TreeScope {
|
|
20
|
+
const scope = new TreeScope();
|
|
21
|
+
this.scopes.push(scope);
|
|
22
|
+
return scope;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static pop() {
|
|
26
|
+
this.scopes.pop();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static scoped<T>(fn: (scope: TreeScope) => T): T {
|
|
30
|
+
const scope = this.push();
|
|
31
|
+
const ret = fn(scope);
|
|
32
|
+
this.pop();
|
|
33
|
+
return ret;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"TreeContext",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export type TreeContext = InstanceType<typeof TreeContext>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register a teardown callback for the currently-mounting component.
|
|
43
|
+
* Must be called synchronously during component setup.
|
|
44
|
+
*/
|
|
45
|
+
export function onCleanup(fn: () => void): void {
|
|
46
|
+
TreeContext.scope.cleanups.add(fn);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run `fn` inside a cleanup-collection scope.
|
|
51
|
+
* Returns `[result, dispose]` where `dispose()` calls every `onCleanup`
|
|
52
|
+
* registered during `fn`'s execution.
|
|
53
|
+
*/
|
|
54
|
+
export function withCleanupScope<T>(
|
|
55
|
+
fn: () => T,
|
|
56
|
+
): [result: T, dispose: () => void] {
|
|
57
|
+
return TreeContext.scoped<[result: T, dispose: () => void]>((scope) => {
|
|
58
|
+
const result = fn();
|
|
59
|
+
return [result, () => scope.cleanups.forEach((c) => c())];
|
|
60
|
+
});
|
|
61
|
+
}
|
package/src/context.ts~
ADDED
|
File without changes
|
package/src/index.ts
ADDED
package/src/index.ts~
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import * as F from "./velement";
|
|
2
|
+
import { Renderer } from "./renderer";
|
|
3
|
+
import { describe, expect, assert } from "vitest";
|
|
4
|
+
import { mount } from "./mount";
|
|
5
|
+
import { tn } from "./tree";
|
|
6
|
+
import { derived, ref } from "@derivesome/core";
|
|
7
|
+
|
|
8
|
+
type BrowserEnv = {
|
|
9
|
+
doc: F.VDocument;
|
|
10
|
+
root: F.VElement;
|
|
11
|
+
renderer: Renderer<F.VNode>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const createEnv = (): BrowserEnv => {
|
|
15
|
+
const doc = F.createDocument();
|
|
16
|
+
const root = doc.createElement("div");
|
|
17
|
+
|
|
18
|
+
const renderer: Renderer<F.VNode> = {
|
|
19
|
+
createComment: (content) => doc.createComment(content),
|
|
20
|
+
createText: (content) => doc.createTextNode(content),
|
|
21
|
+
createElement: (tag) => doc.createElement(tag),
|
|
22
|
+
createMarker: (name) => doc.createComment(name || "?"),
|
|
23
|
+
insertBefore: (parent, child, before) => parent.insertBefore(child, before),
|
|
24
|
+
removeChild: (parent, child) => parent.removeChild(child),
|
|
25
|
+
setProp: (node, key, value) =>
|
|
26
|
+
(node as F.VElement).setAttribute(key, value + ""),
|
|
27
|
+
removeProp: (node, key) => (node as F.VElement).removeAttribute(key),
|
|
28
|
+
setText: (node, text) => ((node as F.VText).data = text),
|
|
29
|
+
moveRange: (parent, startMarker, endMarker, before) => {
|
|
30
|
+
const list = parent.childNodes;
|
|
31
|
+
const startIdx = list.indexOf(startMarker);
|
|
32
|
+
const endIdx = list.indexOf(endMarker);
|
|
33
|
+
if (startIdx === -1 || endIdx === -1) return;
|
|
34
|
+
const range = list.splice(startIdx, endIdx - startIdx + 1);
|
|
35
|
+
const insertAt = before === null ? list.length : list.indexOf(before);
|
|
36
|
+
list.splice(insertAt, 0, ...range);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
doc,
|
|
42
|
+
renderer,
|
|
43
|
+
root,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
describe("mount", (it) => {
|
|
48
|
+
it("mounts simple text node", () => {
|
|
49
|
+
const env = createEnv();
|
|
50
|
+
mount(tn.value("foobar"), env.renderer, env.root);
|
|
51
|
+
|
|
52
|
+
expect(env.root.childNodes.length === 1);
|
|
53
|
+
const child = env.root.childNodes[0]!;
|
|
54
|
+
expect(child).toBeInstanceOf(F.VText);
|
|
55
|
+
expect(child).toMatchObject({ data: "foobar" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("mounts simple number node", () => {
|
|
59
|
+
const env = createEnv();
|
|
60
|
+
mount(tn.value(203), env.renderer, env.root);
|
|
61
|
+
|
|
62
|
+
expect(env.root.childNodes.length === 1);
|
|
63
|
+
const child = env.root.childNodes[0]!;
|
|
64
|
+
expect(child).toBeInstanceOf(F.VText);
|
|
65
|
+
expect(child).toMatchObject({ data: "203" });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("mounts simple true node", () => {
|
|
69
|
+
const env = createEnv();
|
|
70
|
+
mount(tn.value(true), env.renderer, env.root);
|
|
71
|
+
|
|
72
|
+
expect(env.root.childNodes.length === 1);
|
|
73
|
+
const child = env.root.childNodes[0]!;
|
|
74
|
+
expect(child).toBeInstanceOf(F.VText);
|
|
75
|
+
expect(child).toMatchObject({ data: "" });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("mounts simple false node", () => {
|
|
79
|
+
const env = createEnv();
|
|
80
|
+
mount(tn.value(false), env.renderer, env.root);
|
|
81
|
+
|
|
82
|
+
expect(env.root.childNodes.length === 1);
|
|
83
|
+
const child = env.root.childNodes[0]!;
|
|
84
|
+
expect(child).toBeInstanceOf(F.VText);
|
|
85
|
+
expect(child).toMatchObject({ data: "" });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("mounts reactive number and response to updates", () => {
|
|
89
|
+
const env = createEnv();
|
|
90
|
+
const count = ref<number>(0);
|
|
91
|
+
mount(tn.value(count), env.renderer, env.root);
|
|
92
|
+
|
|
93
|
+
expect(env.root.childNodes.length === 1);
|
|
94
|
+
expect(env.root.childNodes[0]!).toBeInstanceOf(F.VText);
|
|
95
|
+
expect(env.root.childNodes[0]!).toMatchObject({ data: "0" });
|
|
96
|
+
|
|
97
|
+
count.set((x) => x + 1);
|
|
98
|
+
|
|
99
|
+
expect(env.root.childNodes[0]!).toMatchObject({ data: "1" });
|
|
100
|
+
|
|
101
|
+
count.set((x) => x + 1);
|
|
102
|
+
|
|
103
|
+
expect(env.root.childNodes[0]!).toMatchObject({ data: "2" });
|
|
104
|
+
|
|
105
|
+
count.set((x) => x + 1);
|
|
106
|
+
|
|
107
|
+
expect(env.root.childNodes[0]!).toMatchObject({ data: "3" });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("mounts a list", () => {
|
|
111
|
+
const env = createEnv();
|
|
112
|
+
mount(
|
|
113
|
+
tn.element("ul", {
|
|
114
|
+
children: [
|
|
115
|
+
tn.element("li", { children: "item 1", key: 0 }),
|
|
116
|
+
tn.element("li", { children: "item 2", key: 1 }),
|
|
117
|
+
tn.element("li", { children: "item 3", key: 2 }),
|
|
118
|
+
tn.element("li", { children: "item 4", key: 3 }),
|
|
119
|
+
],
|
|
120
|
+
}),
|
|
121
|
+
env.renderer,
|
|
122
|
+
env.root,
|
|
123
|
+
);
|
|
124
|
+
expect(env.root.childNodes.length === 1);
|
|
125
|
+
const child = env.root.childNodes[0]!;
|
|
126
|
+
expect(child).toBeInstanceOf(F.VElement);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("mounts a reactive list", () => {
|
|
130
|
+
const env = createEnv();
|
|
131
|
+
|
|
132
|
+
let idCounter: number = 1;
|
|
133
|
+
type TodoItem = { title: string; id: number };
|
|
134
|
+
const todos = ref<TodoItem[]>([]);
|
|
135
|
+
|
|
136
|
+
const makeTodo = () =>
|
|
137
|
+
todos.set((items) => [
|
|
138
|
+
...items,
|
|
139
|
+
{ title: `Todo number ${idCounter}`, id: idCounter++ },
|
|
140
|
+
]);
|
|
141
|
+
const removeTodo = (id: number) =>
|
|
142
|
+
todos.set((items) => items.filter((x) => x.id !== id));
|
|
143
|
+
|
|
144
|
+
mount(
|
|
145
|
+
tn.element("ul", {
|
|
146
|
+
children: tn.list(todos, (todo) =>
|
|
147
|
+
tn.element("li", { children: todo.title }),
|
|
148
|
+
),
|
|
149
|
+
}),
|
|
150
|
+
env.renderer,
|
|
151
|
+
env.root,
|
|
152
|
+
);
|
|
153
|
+
expect(env.root.childNodes.length === 1);
|
|
154
|
+
const child = env.root.childNodes[0]!;
|
|
155
|
+
expect(child).toBeInstanceOf(F.VElement);
|
|
156
|
+
const ul = child as F.VElement;
|
|
157
|
+
|
|
158
|
+
expect(ul.childNodes.length === 0);
|
|
159
|
+
makeTodo();
|
|
160
|
+
expect(ul.childNodes.length === 1);
|
|
161
|
+
makeTodo();
|
|
162
|
+
expect(ul.childNodes.length === 2);
|
|
163
|
+
makeTodo();
|
|
164
|
+
expect(ul.childNodes.length === 3);
|
|
165
|
+
|
|
166
|
+
// The expected sequence of todo id's right now is [1, 2, 3] (since id's start at 1)
|
|
167
|
+
const expectedIds: number[] = [1, 2, 3];
|
|
168
|
+
for (let i = 0; i < ul.children.length; i++) {
|
|
169
|
+
const el = ul.children[i]!;
|
|
170
|
+
expect(el.textContent).toBe(`Todo number ${expectedIds[i]}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
removeTodo(2);
|
|
174
|
+
|
|
175
|
+
expect(ul.childNodes.length === 2);
|
|
176
|
+
|
|
177
|
+
// The expected sequence of todo id's right now is [1, 3] (since we removed 2)
|
|
178
|
+
const nextExpectedIds: number[] = [1, 3];
|
|
179
|
+
for (let i = 0; i < ul.children.length; i++) {
|
|
180
|
+
const el = ul.children[i]!;
|
|
181
|
+
expect(el.textContent).toBe(`Todo number ${nextExpectedIds[i]}`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("infers a reactive list from a derived and mounts", () => {
|
|
186
|
+
const env = createEnv();
|
|
187
|
+
|
|
188
|
+
let idCounter: number = 1;
|
|
189
|
+
type TodoItem = { title: string; id: number };
|
|
190
|
+
const todos = ref<TodoItem[]>([]);
|
|
191
|
+
|
|
192
|
+
const makeTodo = () =>
|
|
193
|
+
todos.set((items) => [
|
|
194
|
+
...items,
|
|
195
|
+
{ title: `Todo number ${idCounter}`, id: idCounter++ },
|
|
196
|
+
]);
|
|
197
|
+
const removeTodo = (id: number) =>
|
|
198
|
+
todos.set((items) => items.filter((x) => x.id !== id));
|
|
199
|
+
|
|
200
|
+
mount(
|
|
201
|
+
tn.element("ul", {
|
|
202
|
+
children: derived(() =>
|
|
203
|
+
todos.get().map((todo) => tn.element("li", { children: todo.title })),
|
|
204
|
+
),
|
|
205
|
+
}),
|
|
206
|
+
env.renderer,
|
|
207
|
+
env.root,
|
|
208
|
+
);
|
|
209
|
+
expect(env.root.childNodes.length === 1);
|
|
210
|
+
const child = env.root.childNodes[0]!;
|
|
211
|
+
expect(child).toBeInstanceOf(F.VElement);
|
|
212
|
+
const ul = child as F.VElement;
|
|
213
|
+
|
|
214
|
+
expect(ul.childNodes.length === 0);
|
|
215
|
+
makeTodo();
|
|
216
|
+
expect(ul.childNodes.length === 1);
|
|
217
|
+
makeTodo();
|
|
218
|
+
expect(ul.childNodes.length === 2);
|
|
219
|
+
makeTodo();
|
|
220
|
+
expect(ul.childNodes.length === 3);
|
|
221
|
+
|
|
222
|
+
// The expected sequence of todo id's right now is [1, 2, 3] (since id's start at 1)
|
|
223
|
+
const expectedIds: number[] = [1, 2, 3];
|
|
224
|
+
for (let i = 0; i < ul.children.length; i++) {
|
|
225
|
+
const el = ul.children[i]!;
|
|
226
|
+
expect(el.textContent).toBe(`Todo number ${expectedIds[i]}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
removeTodo(2);
|
|
230
|
+
|
|
231
|
+
expect(ul.childNodes.length === 2);
|
|
232
|
+
|
|
233
|
+
// The expected sequence of todo id's right now is [1, 3] (since we removed 2)
|
|
234
|
+
const nextExpectedIds: number[] = [1, 3];
|
|
235
|
+
for (let i = 0; i < ul.children.length; i++) {
|
|
236
|
+
const el = ul.children[i]!;
|
|
237
|
+
expect(el.textContent).toBe(`Todo number ${nextExpectedIds[i]}`);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("sets static element attributes", () => {
|
|
242
|
+
const env = createEnv();
|
|
243
|
+
mount(
|
|
244
|
+
tn.element("a", { href: "https://example.com", id: "link" }),
|
|
245
|
+
env.renderer,
|
|
246
|
+
env.root,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const el = env.root.childNodes[0]! as F.VElement;
|
|
250
|
+
expect(el).toBeInstanceOf(F.VElement);
|
|
251
|
+
expect(el.getAttribute("href")).toBe("https://example.com");
|
|
252
|
+
expect(el.getAttribute("id")).toBe("link");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("updates reactive element attribute when ref changes", () => {
|
|
256
|
+
const env = createEnv();
|
|
257
|
+
const disabled = ref<boolean | null>(null);
|
|
258
|
+
mount(tn.element("button", { disabled }), env.renderer, env.root);
|
|
259
|
+
|
|
260
|
+
const el = env.root.childNodes[0]! as F.VElement;
|
|
261
|
+
expect(el.hasAttribute("disabled")).toBe(false);
|
|
262
|
+
|
|
263
|
+
disabled.set(true);
|
|
264
|
+
expect(el.getAttribute("disabled")).toBe("true");
|
|
265
|
+
|
|
266
|
+
disabled.set(null);
|
|
267
|
+
expect(el.hasAttribute("disabled")).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("mounts nested elements", () => {
|
|
271
|
+
const env = createEnv();
|
|
272
|
+
mount(
|
|
273
|
+
tn.element("div", {
|
|
274
|
+
children: tn.element("p", {
|
|
275
|
+
children: tn.element("span", { children: "hello" }),
|
|
276
|
+
}),
|
|
277
|
+
}),
|
|
278
|
+
env.renderer,
|
|
279
|
+
env.root,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const div = env.root.childNodes[0]! as F.VElement;
|
|
283
|
+
expect(div).toBeInstanceOf(F.VElement);
|
|
284
|
+
const p = div.children[0]!;
|
|
285
|
+
expect(p.tagName).toBe("P");
|
|
286
|
+
const span = p.children[0]!;
|
|
287
|
+
expect(span.tagName).toBe("SPAN");
|
|
288
|
+
expect(span.textContent).toBe("hello");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("mounts a void node without adding children", () => {
|
|
292
|
+
const env = createEnv();
|
|
293
|
+
mount(tn.none(), env.renderer, env.root);
|
|
294
|
+
expect(env.root.childNodes.length).toBe(0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("mounts a function component", () => {
|
|
298
|
+
const env = createEnv();
|
|
299
|
+
const Greeting = (props: { name: string }) =>
|
|
300
|
+
tn.element("span", { children: `Hello, ${props.name}` });
|
|
301
|
+
|
|
302
|
+
mount(tn.fn(Greeting as any, { name: "world" }), env.renderer, env.root);
|
|
303
|
+
|
|
304
|
+
const span = env.root.childNodes[0]! as F.VElement;
|
|
305
|
+
expect(span).toBeInstanceOf(F.VElement);
|
|
306
|
+
expect(span.textContent).toBe("Hello, world");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("cleanup removes mounted nodes from the DOM", () => {
|
|
310
|
+
const env = createEnv();
|
|
311
|
+
const cleanup = mount(
|
|
312
|
+
tn.element("p", { children: "bye" }),
|
|
313
|
+
env.renderer,
|
|
314
|
+
env.root,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
expect(env.root.childNodes.length).toBe(1);
|
|
318
|
+
cleanup();
|
|
319
|
+
expect(env.root.childNodes.length).toBe(0);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("cleanup stops reactive updates", () => {
|
|
323
|
+
const env = createEnv();
|
|
324
|
+
const count = ref(0);
|
|
325
|
+
const cleanup = mount(tn.value(count), env.renderer, env.root);
|
|
326
|
+
|
|
327
|
+
const textNode = env.root.childNodes[0]! as F.VText;
|
|
328
|
+
expect(textNode.data).toBe("0");
|
|
329
|
+
|
|
330
|
+
count.set(1);
|
|
331
|
+
expect(textNode.data).toBe("1");
|
|
332
|
+
|
|
333
|
+
cleanup();
|
|
334
|
+
expect(env.root.childNodes.length).toBe(0);
|
|
335
|
+
|
|
336
|
+
// Updating the ref after cleanup should not throw and should not
|
|
337
|
+
// affect the (already removed) text node.
|
|
338
|
+
count.set(2);
|
|
339
|
+
expect(textNode.data).toBe("1");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("reorders list items when source order changes (explicit keys)", () => {
|
|
343
|
+
const env = createEnv();
|
|
344
|
+
type Item = { id: number; label: string };
|
|
345
|
+
const items = ref<Item[]>([
|
|
346
|
+
{ id: 1, label: "A" },
|
|
347
|
+
{ id: 2, label: "B" },
|
|
348
|
+
{ id: 3, label: "C" },
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
mount(
|
|
352
|
+
tn.element("ul", {
|
|
353
|
+
children: tn.list(items, (item) =>
|
|
354
|
+
tn.element("li", { children: item.label, key: item.id }),
|
|
355
|
+
),
|
|
356
|
+
}),
|
|
357
|
+
env.renderer,
|
|
358
|
+
env.root,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const ul = env.root.childNodes[0]! as F.VElement;
|
|
362
|
+
const labelsOf = () => ul.children.map((c) => c.textContent);
|
|
363
|
+
|
|
364
|
+
expect(labelsOf()).toEqual(["A", "B", "C"]);
|
|
365
|
+
|
|
366
|
+
// Reverse the order
|
|
367
|
+
items.set([
|
|
368
|
+
{ id: 3, label: "C" },
|
|
369
|
+
{ id: 1, label: "A" },
|
|
370
|
+
{ id: 2, label: "B" },
|
|
371
|
+
]);
|
|
372
|
+
|
|
373
|
+
expect(labelsOf()).toEqual(["C", "A", "B"]);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("preserves existing DOM nodes when prepending to a keyed list", () => {
|
|
377
|
+
const env = createEnv();
|
|
378
|
+
type Item = { id: number; label: string };
|
|
379
|
+
const items = ref<Item[]>([{ id: 2, label: "B" }]);
|
|
380
|
+
|
|
381
|
+
mount(
|
|
382
|
+
tn.element("ul", {
|
|
383
|
+
children: tn.list(items, (item) =>
|
|
384
|
+
tn.element("li", { children: item.label, key: item.id }),
|
|
385
|
+
),
|
|
386
|
+
}),
|
|
387
|
+
env.renderer,
|
|
388
|
+
env.root,
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const ul = env.root.childNodes[0]! as F.VElement;
|
|
392
|
+
const originalB = ul.children[0]!;
|
|
393
|
+
|
|
394
|
+
items.set([
|
|
395
|
+
{ id: 1, label: "A" },
|
|
396
|
+
{ id: 2, label: "B" },
|
|
397
|
+
]);
|
|
398
|
+
|
|
399
|
+
expect(ul.children.length).toBe(2);
|
|
400
|
+
expect(ul.children[0]!.textContent).toBe("A");
|
|
401
|
+
expect(ul.children[1]!.textContent).toBe("B");
|
|
402
|
+
// The B element should be the same DOM node (not recreated)
|
|
403
|
+
expect(ul.children[1]).toBe(originalB);
|
|
404
|
+
});
|
|
405
|
+
});
|