@ryupold/vode 0.9.4 → 0.9.6
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/.github/workflows/npm-publish.yml +33 -25
- package/LICENSE +21 -21
- package/README.md +126 -126
- package/index.ts +2 -2
- package/package.json +2 -2
- package/src/html.ts +37 -37
- package/src/vode-tags.ts +206 -206
- package/src/vode.ts +693 -693
- package/tsconfig.json +30 -30
- package/version.txt +0 -0
package/src/vode.ts
CHANGED
|
@@ -1,694 +1,694 @@
|
|
|
1
|
-
export type Vode<S> = FullVode<S> | JustTagVode | NoPropsVode<S>;
|
|
2
|
-
export type ChildVode<S> = Vode<S> | TextVode | NoVode | Component<S>;
|
|
3
|
-
export type FullVode<S> = [tag: Tag, props: Props<S>, ...children: ChildVode<S>[]];
|
|
4
|
-
export type NoPropsVode<S> = [tag: Tag, ...children: ChildVode<S>[]] | string[];
|
|
5
|
-
export type JustTagVode = [tag: Tag];
|
|
6
|
-
export type TextVode = string;
|
|
7
|
-
export type NoVode = undefined | null | number | boolean | bigint | void;
|
|
8
|
-
export type AttachedVode<S> = Vode<S> & { node: ChildNode, id?: string } | Text & { node?: never, id?: never };
|
|
9
|
-
export type Tag = keyof (HTMLElementTagNameMap & SVGElementTagNameMap & MathMLElementTagNameMap);
|
|
10
|
-
export type Component<S> = (s: S) => ChildVode<S>;
|
|
11
|
-
|
|
12
|
-
export type Patch<S> =
|
|
13
|
-
| NoRenderPatch // ignored
|
|
14
|
-
| typeof EmptyPatch | DeepPartial<S> // render patches
|
|
15
|
-
| Promise<Patch<S>> | Effect<S> // effects resulting in patches
|
|
16
|
-
|
|
17
|
-
export const EmptyPatch = {} as const; // smallest patch to cause a render without any changes
|
|
18
|
-
export type NoRenderPatch = undefined | null | number | boolean | bigint | string | symbol | void;
|
|
19
|
-
|
|
20
|
-
export type DeepPartial<S> = { [P in keyof S]?: S[P] extends Array<infer I> ? Array<Patch<I>> : Patch<S[P]> };
|
|
21
|
-
|
|
22
|
-
export type Effect<S> =
|
|
23
|
-
| (() => Patch<S>)
|
|
24
|
-
| EffectFunction<S>
|
|
25
|
-
| [effect: EffectFunction<S>, ...args: any[]]
|
|
26
|
-
| Generator<Patch<S>, unknown, void>
|
|
27
|
-
| AsyncGenerator<Patch<S>, unknown, void>;
|
|
28
|
-
|
|
29
|
-
export type EffectFunction<S> = (state: S, ...args: any[]) => Patch<S>;
|
|
30
|
-
|
|
31
|
-
export type Dispatch<S> = (action: Patch<S>) => void;
|
|
32
|
-
export type PatchableState<S> = S & { patch: Dispatch<Patch<S>> };
|
|
33
|
-
|
|
34
|
-
export type Props<S> = Partial<
|
|
35
|
-
Omit<HTMLElement,
|
|
36
|
-
keyof (DocumentFragment & ElementCSSInlineStyle & GlobalEventHandlers)> &
|
|
37
|
-
{ [K in keyof EventsMap]: Patch<S> } // all on* events
|
|
38
|
-
> & {
|
|
39
|
-
[_: string]: unknown,
|
|
40
|
-
class?: ClassProp,
|
|
41
|
-
style?: StyleProp,
|
|
42
|
-
/** called after the element was attached */
|
|
43
|
-
onMount?: MountFunction<S>,
|
|
44
|
-
/** called before the element is detached */
|
|
45
|
-
onUnmount?: MountFunction<S>,
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export type MountFunction<S> =
|
|
49
|
-
| ((s: S, node: HTMLElement) => Patch<S>)
|
|
50
|
-
| ((s: S, node: SVGSVGElement) => Patch<S>)
|
|
51
|
-
| ((s: S, node: MathMLElement) => Patch<S>)
|
|
52
|
-
|
|
53
|
-
export type ClassProp =
|
|
54
|
-
| "" | false | null | undefined // no class
|
|
55
|
-
| string // "class1 class2"
|
|
56
|
-
| string[] // ["class1", "class2"]
|
|
57
|
-
| Record<string, boolean | undefined | null> // { class1: true, class2: false }
|
|
58
|
-
|
|
59
|
-
export type StyleProp = Record<number, never> & {
|
|
60
|
-
[K in keyof CSSStyleDeclaration]?: CSSStyleDeclaration[K] | null
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export type EventsMap =
|
|
64
|
-
& { [K in keyof HTMLElementEventMap as `on${K}`]: HTMLElementEventMap[K] }
|
|
65
|
-
& { [K in keyof WindowEventMap as `on${K}`]: WindowEventMap[K] }
|
|
66
|
-
& { [K in keyof SVGElementEventMap as `on${K}`]: SVGElementEventMap[K] }
|
|
67
|
-
& { onsearch: Event }
|
|
68
|
-
|
|
69
|
-
export type PropertyValue<S> = string | boolean | null | undefined | StyleProp | ClassProp | Patch<S> | void;
|
|
70
|
-
|
|
71
|
-
export type ContainerNode<S> = HTMLElement & {
|
|
72
|
-
state: PatchableState<S>,
|
|
73
|
-
vode: AttachedVode<S>,
|
|
74
|
-
patch: Dispatch<S>,
|
|
75
|
-
render: () => void,
|
|
76
|
-
q: Patch<S>[]
|
|
77
|
-
isRendering: boolean,
|
|
78
|
-
stats: {
|
|
79
|
-
patchCount: number,
|
|
80
|
-
liveEffectCount: number,
|
|
81
|
-
renderPatchCount: number,
|
|
82
|
-
renderCount: number,
|
|
83
|
-
renderTime: number,
|
|
84
|
-
queueLengthBeforeRender: number,
|
|
85
|
-
queueLengthAfterRender: number,
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
/** type-safe way to create a vode. useful for type inference and autocompletion.
|
|
90
|
-
*
|
|
91
|
-
* overloads:
|
|
92
|
-
* - just a tag: `vode("div") // => ["div"]`
|
|
93
|
-
* - tag and props: `vode("div", { class: "foo" }) // => ["div", { class: "foo" }]`
|
|
94
|
-
* - tag, props and children: `vode("div", { class: "foo" }, ["span", "bar"]) // => ["div", { class: "foo" }, ["span", "bar"]]`
|
|
95
|
-
* - identity: `vode(["div", ["span", "bar"]]) // => ["div", ["span", "bar"]]`
|
|
96
|
-
*/
|
|
97
|
-
export function vode<S extends object | unknown>(tag: Tag | Vode<S>, props?: Props<S> | ChildVode<S>, ...children: ChildVode<S>[]): Vode<S> {
|
|
98
|
-
if (Array.isArray(tag)) {
|
|
99
|
-
return tag;
|
|
100
|
-
}
|
|
101
|
-
if (props) {
|
|
102
|
-
return [tag, props as Props<S>, ...children];
|
|
103
|
-
}
|
|
104
|
-
return [tag, ...children];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** pass an object whose type determines the initial state */
|
|
108
|
-
export function createState<S>(state: S): PatchableState<S> { return state as PatchableState<S>; }
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
/** for a type safe way to create a deeply partial patch object or effect */
|
|
112
|
-
export function patch<S>(p: DeepPartial<S> | Effect<S> | NoRenderPatch): typeof p { return p; }
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* create a vode app inside a container element
|
|
116
|
-
* @param container will use this container as root and places the result of the dom function and further renderings in it
|
|
117
|
-
* @param initialState @see createState
|
|
118
|
-
* @param dom creates the initial dom from the state and is called on every render
|
|
119
|
-
* @param initialPatches variadic list of patches that are applied after the first render
|
|
120
|
-
* @returns a patch function that can be used to update the state
|
|
121
|
-
*/
|
|
122
|
-
export function app<S>(container: HTMLElement, initialState: Omit<S, "patch">, dom: Component<S>, ...initialPatches: Patch<S>[]) {
|
|
123
|
-
const root = container as ContainerNode<S>;
|
|
124
|
-
root.stats = { renderTime: 0, renderCount: 0, queueLengthBeforeRender: 0, queueLengthAfterRender: 0, liveEffectCount: 0, patchCount: 0, renderPatchCount: 0 };
|
|
125
|
-
|
|
126
|
-
Object.defineProperty(initialState, "patch", {
|
|
127
|
-
enumerable: false, configurable: true,
|
|
128
|
-
writable: false, value: async (action: Patch<S>) => {
|
|
129
|
-
if (!action || (typeof action !== "function" && typeof action !== "object")) return;
|
|
130
|
-
root.stats.patchCount++;
|
|
131
|
-
|
|
132
|
-
if ((action as AsyncGenerator<Patch<S>, unknown, void>)?.next) {
|
|
133
|
-
const generator = action as AsyncGenerator<Patch<S>, unknown, void>;
|
|
134
|
-
root.stats.liveEffectCount++;
|
|
135
|
-
try {
|
|
136
|
-
let v = await generator.next();
|
|
137
|
-
while (v.done === false) {
|
|
138
|
-
root.stats.liveEffectCount++;
|
|
139
|
-
try {
|
|
140
|
-
root.patch!(v.value);
|
|
141
|
-
v = await generator.next();
|
|
142
|
-
} finally {
|
|
143
|
-
root.stats.liveEffectCount--;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
root.patch!(v.value as Patch<S>);
|
|
147
|
-
} finally {
|
|
148
|
-
root.stats.liveEffectCount--;
|
|
149
|
-
}
|
|
150
|
-
} else if ((action as Promise<S>).then) {
|
|
151
|
-
root.stats.liveEffectCount++;
|
|
152
|
-
try {
|
|
153
|
-
const nextState = await (action as Promise<S>);
|
|
154
|
-
root.patch!(<Patch<S>>nextState);
|
|
155
|
-
} finally {
|
|
156
|
-
root.stats.liveEffectCount--;
|
|
157
|
-
}
|
|
158
|
-
} else if (Array.isArray(action)) {
|
|
159
|
-
if (typeof action[0] === "function") {
|
|
160
|
-
if (action.length > 1)
|
|
161
|
-
root.patch!(action[0](root.state!, ...(action as any[]).slice(1)));
|
|
162
|
-
else root.patch!(action[0](root.state!));
|
|
163
|
-
} else {
|
|
164
|
-
root.stats.patchCount--;
|
|
165
|
-
}
|
|
166
|
-
} else if (typeof action === "function") {
|
|
167
|
-
root.patch!((<EffectFunction<S>>action)(root.state));
|
|
168
|
-
} else {
|
|
169
|
-
root.stats.renderPatchCount++;
|
|
170
|
-
root.q!.push(<Patch<S>>action);
|
|
171
|
-
if (!root.isRendering) root.render!();
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
Object.defineProperty(root, "render", {
|
|
177
|
-
enumerable: false, configurable: true,
|
|
178
|
-
writable: false, value: () => requestAnimationFrame(() => {
|
|
179
|
-
if (root.isRendering || root.q!.length === 0) return;
|
|
180
|
-
root.isRendering = true;
|
|
181
|
-
const sw = Date.now();
|
|
182
|
-
try {
|
|
183
|
-
root.stats.queueLengthBeforeRender = root.q!.length;
|
|
184
|
-
|
|
185
|
-
while (root.q!.length > 0) {
|
|
186
|
-
const patch = root.q!.shift();
|
|
187
|
-
if(patch === EmptyPatch) continue;
|
|
188
|
-
mergeState(root.state, patch);
|
|
189
|
-
}
|
|
190
|
-
root.vode = render(root.state, root.patch, container, 0, root.vode, dom(root.state))!;
|
|
191
|
-
} finally {
|
|
192
|
-
root.isRendering = false;
|
|
193
|
-
root.stats.renderCount++;
|
|
194
|
-
root.stats.renderTime = Date.now() - sw;
|
|
195
|
-
root.stats.queueLengthAfterRender = root.q!.length;
|
|
196
|
-
if (root.q!.length > 0) {
|
|
197
|
-
root.render!();
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
})
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
root.patch = (<PatchableState<S>>initialState).patch;
|
|
204
|
-
root.state = <PatchableState<S>>initialState;
|
|
205
|
-
root.q = [];
|
|
206
|
-
const initialVode = dom(<S>initialState);
|
|
207
|
-
root.vode = <AttachedVode<S>>initialVode;
|
|
208
|
-
root.vode = render(<S>initialState, root.patch!, container, 0, undefined, initialVode)!;
|
|
209
|
-
|
|
210
|
-
for (const effect of initialPatches) {
|
|
211
|
-
root.patch!(effect);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return root.patch;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** get properties of a vode, if there are any */
|
|
218
|
-
export function props<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): Props<S> | undefined {
|
|
219
|
-
if (Array.isArray(vode)
|
|
220
|
-
&& vode.length > 1
|
|
221
|
-
&& vode[1]
|
|
222
|
-
&& !Array.isArray(vode[1])
|
|
223
|
-
) {
|
|
224
|
-
if (
|
|
225
|
-
typeof vode[1] === "object"
|
|
226
|
-
&& (vode[1] as unknown as Node).nodeType !== Node.TEXT_NODE
|
|
227
|
-
) {
|
|
228
|
-
return vode[1];
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return undefined;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export function mergeClass(a: ClassProp, b: ClassProp): ClassProp {
|
|
236
|
-
if (!a) return b;
|
|
237
|
-
if (!b) return a;
|
|
238
|
-
|
|
239
|
-
if (typeof a === "string" && typeof b === "string") {
|
|
240
|
-
const aSplit = a.split(" ");
|
|
241
|
-
const bSplit = b.split(" ");
|
|
242
|
-
const classSet = new Set([...aSplit, ...bSplit]);
|
|
243
|
-
return Array.from(classSet).join(" ").trim();
|
|
244
|
-
}
|
|
245
|
-
else if (typeof a === "string" && Array.isArray(b)) {
|
|
246
|
-
const classSet = new Set([...b, ...a.split(" ")]);
|
|
247
|
-
return Array.from(classSet).join(" ").trim();
|
|
248
|
-
}
|
|
249
|
-
else if (Array.isArray(a) && typeof b === "string") {
|
|
250
|
-
const classSet = new Set([...a, ...b.split(" ")]);
|
|
251
|
-
return Array.from(classSet).join(" ").trim();
|
|
252
|
-
}
|
|
253
|
-
else if (Array.isArray(a) && Array.isArray(b)) {
|
|
254
|
-
const classSet = new Set([...a, ...b]);
|
|
255
|
-
return Array.from(classSet).join(" ").trim();
|
|
256
|
-
}
|
|
257
|
-
else if (typeof a === "string" && typeof b === "object") {
|
|
258
|
-
return { [a]: true, ...b };
|
|
259
|
-
}
|
|
260
|
-
else if (typeof a === "object" && typeof b === "string") {
|
|
261
|
-
return { ...a, [b]: true };
|
|
262
|
-
}
|
|
263
|
-
else if (typeof a === "object" && typeof b === "object") {
|
|
264
|
-
return { ...a, ...b };
|
|
265
|
-
} else if (typeof a === "object" && Array.isArray(b)) {
|
|
266
|
-
const aa = { ...a };
|
|
267
|
-
for (const item of b as string[]) {
|
|
268
|
-
(<Record<string, boolean | null | undefined>>aa)[item] = true;
|
|
269
|
-
}
|
|
270
|
-
return aa;
|
|
271
|
-
} else if (Array.isArray(a) && typeof b === "object") {
|
|
272
|
-
const aa: Record<string, any> = {};
|
|
273
|
-
for (const item of a as string[]) {
|
|
274
|
-
aa[item] = true;
|
|
275
|
-
}
|
|
276
|
-
for (const bKey of (<Record<string, any>>b).keys) {
|
|
277
|
-
aa[bKey] = (<Record<string, boolean | null | undefined>>b)[bKey];
|
|
278
|
-
}
|
|
279
|
-
return b;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
throw new Error(`cannot merge classes of ${a} (${typeof a}) and ${b} (${typeof b})`);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export function patchProps<S extends object | unknown>(vode: Vode<S>, props: Props<S>): void {
|
|
286
|
-
if (!Array.isArray(vode)) return;
|
|
287
|
-
|
|
288
|
-
if (vode.length > 1) {
|
|
289
|
-
if (!Array.isArray(vode[1]) && typeof vode[1] === "object") {
|
|
290
|
-
vode[1] = merge(vode[1], props);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (childCount(vode) > 0) {
|
|
295
|
-
(<FullVode<S>>vode).push(null);
|
|
296
|
-
}
|
|
297
|
-
for (let i = vode.length - 1; i > 0; i--) {
|
|
298
|
-
if (i > 1) vode[i] = vode[i - 1];
|
|
299
|
-
}
|
|
300
|
-
vode[1] = props;
|
|
301
|
-
} else {
|
|
302
|
-
(<FullVode<S>>vode).push(props);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/** get a slice of all children of a vode, if there are any */
|
|
307
|
-
export function children<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): ChildVode<S>[] | undefined {
|
|
308
|
-
const start = childrenStart(vode);
|
|
309
|
-
if (start > 0) {
|
|
310
|
-
return (<Vode<S>>vode).slice(start) as Vode<S>[];
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return undefined;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/** index in vode at which child-vodes start */
|
|
317
|
-
export function childrenStart<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): number {
|
|
318
|
-
return props(vode) ? 2 : 1;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/** html tag of the vode or #text if it is a text node */
|
|
322
|
-
export function tag<S extends object | unknown>(v: Vode<S> | TextVode | NoVode | AttachedVode<S>): Tag | "#text" | undefined {
|
|
323
|
-
return !!v ? (Array.isArray(v)
|
|
324
|
-
? v[0] : (typeof v === "string" || (<any>v).nodeType === Node.TEXT_NODE)
|
|
325
|
-
? "#text" : undefined) as Tag
|
|
326
|
-
: undefined;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
export function childCount<S>(vode: Vode<S>) { return vode.length - childrenStart(vode); }
|
|
330
|
-
|
|
331
|
-
export function child<S>(vode: Vode<S>, index: number): ChildVode<S> | undefined {
|
|
332
|
-
return vode[index + childrenStart(vode)] as ChildVode<S>;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/** merge multiple objects into one, applying from left to right
|
|
336
|
-
* @param first object to merge
|
|
337
|
-
* @returns merged object
|
|
338
|
-
*/
|
|
339
|
-
export function merge(first?: any, ...p: any[]): any {
|
|
340
|
-
first = mergeState({}, first);
|
|
341
|
-
for (const pp of p) {
|
|
342
|
-
if (!pp) continue;
|
|
343
|
-
first = mergeState(first, pp);
|
|
344
|
-
}
|
|
345
|
-
return first!;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function classString(classProp: ClassProp): string {
|
|
349
|
-
if (typeof classProp === "string") {
|
|
350
|
-
return classProp;
|
|
351
|
-
} else if (Array.isArray(classProp)) {
|
|
352
|
-
return classProp.map(classString).join(" ");
|
|
353
|
-
} else if (typeof classProp === "object") {
|
|
354
|
-
return Object.keys(classProp!).filter(k => classProp![k]).join(" ");
|
|
355
|
-
} else {
|
|
356
|
-
return "";
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function isNaturalVode(x: ChildVode<any>) {
|
|
361
|
-
return Array.isArray(x) && x.length > 0 && typeof x[0] === "string";
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function isTextVode(x: ChildVode<any>) {
|
|
365
|
-
return typeof x === "string" || (<Text><unknown>x)?.nodeType === Node.TEXT_NODE;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function unwrap<S>(c: Component<S> | ChildVode<S>, s: S): ChildVode<S> {
|
|
369
|
-
if (typeof c === "function") {
|
|
370
|
-
return unwrap(c(s), s);
|
|
371
|
-
} else {
|
|
372
|
-
return c;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/** memoization of the given component or props (compare array is compared element by element (===) with the previous render) */
|
|
377
|
-
export function memo<S extends object | unknown>(compare: any[], componentOrProps: Component<S> | ((s: S) => Props<S>)): typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S> {
|
|
378
|
-
(<any>componentOrProps).__memo = compare;
|
|
379
|
-
return componentOrProps as typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S>;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function remember<S>(state: S, present: any, past: any): ChildVode<S> | AttachedVode<S> {
|
|
383
|
-
if (typeof present !== "function")
|
|
384
|
-
return present;
|
|
385
|
-
|
|
386
|
-
const presentMemo = present?.__memo;
|
|
387
|
-
const pastMemo = past?.__memo;
|
|
388
|
-
|
|
389
|
-
if (Array.isArray(presentMemo)
|
|
390
|
-
&& Array.isArray(pastMemo)
|
|
391
|
-
&& presentMemo.length === pastMemo.length
|
|
392
|
-
) {
|
|
393
|
-
let same = true;
|
|
394
|
-
for (let i = 0; i < presentMemo.length; i++) {
|
|
395
|
-
if (presentMemo[i] !== pastMemo[i]) {
|
|
396
|
-
same = false;
|
|
397
|
-
break;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
if (same) return past;
|
|
401
|
-
}
|
|
402
|
-
const newRender = unwrap(present, state);
|
|
403
|
-
if (typeof newRender === "object") {
|
|
404
|
-
(<any>newRender).__memo = present?.__memo;
|
|
405
|
-
}
|
|
406
|
-
return newRender;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function render<S>(state: S, patch: Dispatch<S>, parent: ChildNode, childIndex: number, oldVode: AttachedVode<S> | undefined, newVode: ChildVode<S>, svg?: boolean): AttachedVode<S> | undefined {
|
|
410
|
-
// unwrap component if it is memoized
|
|
411
|
-
newVode = remember(state, newVode, oldVode) as ChildVode<S>;
|
|
412
|
-
|
|
413
|
-
const isNoVode = !newVode || typeof newVode === "number" || typeof newVode === "boolean";
|
|
414
|
-
if (newVode === oldVode || (!oldVode && isNoVode)) {
|
|
415
|
-
return oldVode;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const oldIsText = (oldVode as Text)?.nodeType === Node.TEXT_NODE;
|
|
419
|
-
const oldNode: ChildNode | undefined = oldIsText ? oldVode as Text : oldVode?.node;
|
|
420
|
-
|
|
421
|
-
// falsy|text|element(A) -> undefined
|
|
422
|
-
if (isNoVode) {
|
|
423
|
-
(<any>oldNode)?.onUnmount && patch((<any>oldNode).onUnmount(oldNode));
|
|
424
|
-
oldNode?.remove();
|
|
425
|
-
return undefined;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const isText = !isNoVode && isTextVode(newVode);
|
|
429
|
-
const isNode = !isNoVode && isNaturalVode(newVode);
|
|
430
|
-
const alreadyAttached = !!newVode && typeof newVode !== "string" && !!((<any>newVode)?.node || (<any>newVode)?.nodeType === Node.TEXT_NODE);
|
|
431
|
-
|
|
432
|
-
if (!isText && !isNode && !alreadyAttached && !oldVode) {
|
|
433
|
-
throw new Error("Invalid vode: " + typeof newVode + " " + JSON.stringify(newVode));
|
|
434
|
-
}
|
|
435
|
-
else if (alreadyAttached && isText) {
|
|
436
|
-
newVode = (<Text><any>newVode).wholeText;
|
|
437
|
-
}
|
|
438
|
-
else if (alreadyAttached && isNode) {
|
|
439
|
-
newVode = [...<Vode<S>>newVode];
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// text -> text
|
|
443
|
-
if (oldIsText && isText) {
|
|
444
|
-
if ((<Text>oldNode).nodeValue !== <string>newVode) {
|
|
445
|
-
(<Text>oldNode).nodeValue = <string>newVode;
|
|
446
|
-
}
|
|
447
|
-
return oldVode;
|
|
448
|
-
}
|
|
449
|
-
// falsy|element -> text
|
|
450
|
-
if (isText && (!oldNode || !oldIsText)) {
|
|
451
|
-
const text = document.createTextNode(newVode as string)
|
|
452
|
-
if (oldNode) {
|
|
453
|
-
(<any>oldNode).onUnmount && patch((<any>oldNode).onUnmount(oldNode));
|
|
454
|
-
oldNode.replaceWith(text);
|
|
455
|
-
} else {
|
|
456
|
-
if (parent.childNodes[childIndex]) {
|
|
457
|
-
parent.insertBefore(text, parent.childNodes[childIndex]);
|
|
458
|
-
} else {
|
|
459
|
-
parent.appendChild(text);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
return text as Text;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// falsy|text|element(A) -> element(B)
|
|
467
|
-
if (
|
|
468
|
-
(isNode && (!oldNode || oldIsText || (<Vode<S>>oldVode)[0] !== (<Vode<S>>newVode)[0]))
|
|
469
|
-
) {
|
|
470
|
-
svg = svg || (<Vode<S>>newVode)[0] === "svg";
|
|
471
|
-
const newNode: ChildNode = svg
|
|
472
|
-
? document.createElementNS("http://www.w3.org/2000/svg", (<Vode<S>>newVode)[0])
|
|
473
|
-
: document.createElement((<Vode<S>>newVode)[0]);
|
|
474
|
-
(<AttachedVode<S>>newVode).node = newNode;
|
|
475
|
-
|
|
476
|
-
const newvode = <Vode<S>>newVode;
|
|
477
|
-
if (1 in newvode) {
|
|
478
|
-
newvode[1] = remember(state, newvode[1], undefined) as Vode<S>;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const properties = props(newVode);
|
|
482
|
-
patchProperties(patch, newNode, undefined, properties, svg);
|
|
483
|
-
|
|
484
|
-
if (oldNode) {
|
|
485
|
-
(<any>oldNode).onUnmount && patch((<any>oldNode).onUnmount(oldNode));
|
|
486
|
-
oldNode.replaceWith(newNode);
|
|
487
|
-
} else {
|
|
488
|
-
if (parent.childNodes[childIndex]) {
|
|
489
|
-
parent.insertBefore(newNode, parent.childNodes[childIndex]);
|
|
490
|
-
} else {
|
|
491
|
-
parent.appendChild(newNode);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const newChildren = children(newVode);
|
|
496
|
-
if (newChildren) {
|
|
497
|
-
for (let i = 0; i < newChildren.length; i++) {
|
|
498
|
-
const child = newChildren[i];
|
|
499
|
-
const attached = render(state, patch, newNode, i, undefined, child, svg);
|
|
500
|
-
(<Vode<S>>newVode!)[properties ? i + 2 : i + 1] = <Vode<S>>attached;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
(<any>newNode).onMount && patch((<any>newNode).onMount(newNode));
|
|
505
|
-
return <AttachedVode<S>>newVode;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
//element(A) -> element(A)
|
|
509
|
-
if (!oldIsText && isNode && (<Vode<S>>oldVode)[0] === (<Vode<S>>newVode)[0]) {
|
|
510
|
-
svg = svg || (<Vode<S>>newVode)[0] === "svg";
|
|
511
|
-
(<AttachedVode<S>>newVode).node = oldNode;
|
|
512
|
-
|
|
513
|
-
const newvode = <Vode<S>>newVode;
|
|
514
|
-
const oldvode = <Vode<S>>oldVode;
|
|
515
|
-
|
|
516
|
-
let hasProps = false;
|
|
517
|
-
if ((<any>newvode[1])?.__memo) {
|
|
518
|
-
const prev = newvode[1] as any;
|
|
519
|
-
newvode[1] = remember(state, newvode[1], oldvode[1]) as Vode<S>;
|
|
520
|
-
if (prev !== newvode[1]) {
|
|
521
|
-
const properties = props(newVode);
|
|
522
|
-
patchProperties(patch, oldNode!, props(oldVode), properties, svg);
|
|
523
|
-
hasProps = !!properties;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
else {
|
|
527
|
-
const properties = props(newVode);
|
|
528
|
-
patchProperties(patch, oldNode!, props(oldVode), properties, svg);
|
|
529
|
-
hasProps = !!properties;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const newKids = children(newVode);
|
|
533
|
-
const oldKids = children(oldVode) as AttachedVode<S>[];
|
|
534
|
-
if (newKids) {
|
|
535
|
-
for (let i = 0; i < newKids.length; i++) {
|
|
536
|
-
const child = newKids[i];
|
|
537
|
-
const oldChild = oldKids && oldKids[i];
|
|
538
|
-
|
|
539
|
-
const attached = render(state, patch, oldNode!, i, oldChild, child, svg);
|
|
540
|
-
if (attached) {
|
|
541
|
-
(<Vode<S>>newVode)[hasProps ? i + 2 : i + 1] = <Vode<S>>attached;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
for (let i = newKids.length; oldKids && i < oldKids.length; i++) {
|
|
545
|
-
if (oldKids[i]?.node)
|
|
546
|
-
oldKids[i].node!.remove();
|
|
547
|
-
else if ((oldKids[i] as Text)?.nodeType === Node.TEXT_NODE)
|
|
548
|
-
(oldKids[i] as Text).remove();
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
for (let i = newKids?.length || 0; i < oldKids?.length || 0; i++) {
|
|
553
|
-
if (oldKids[i]?.node)
|
|
554
|
-
oldKids[i].node!.remove();
|
|
555
|
-
else if ((oldKids[i] as Text)?.nodeType === Node.TEXT_NODE)
|
|
556
|
-
(oldKids[i] as Text).remove();
|
|
557
|
-
}
|
|
558
|
-
return <AttachedVode<S>>newVode;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
return undefined;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function patchProperties<S>(patch: Dispatch<S>, node: ChildNode, oldProps?: Props<S>, newProps?: Props<S>, isSvg?: boolean) {
|
|
565
|
-
if (!newProps && !oldProps) return;
|
|
566
|
-
if (!oldProps) { // set new props
|
|
567
|
-
for (const key in newProps) {
|
|
568
|
-
const newValue = newProps[key as keyof Props<S>] as PropertyValue<S>;
|
|
569
|
-
newProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, undefined, newValue, isSvg);
|
|
570
|
-
}
|
|
571
|
-
} else if (newProps) { // clear old props and set new in one loop
|
|
572
|
-
const combinedKeys = new Set([...Object.keys(oldProps), ...Object.keys(newProps)]);
|
|
573
|
-
for (const key of combinedKeys) {
|
|
574
|
-
const oldValue = oldProps[key as keyof Props<S>] as PropertyValue<S>;
|
|
575
|
-
const newValue = newProps[key as keyof Props<S>] as PropertyValue<S>;
|
|
576
|
-
if (key[0] === "o" && key[1] === "n") {
|
|
577
|
-
const oldEvent = (<any>node)["__" + key];
|
|
578
|
-
if ((oldEvent && oldEvent !== newValue) || (!oldEvent && oldValue !== newValue)) {
|
|
579
|
-
newProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, oldValue, newValue, isSvg);
|
|
580
|
-
}
|
|
581
|
-
(<any>node)["__" + key] = newValue;
|
|
582
|
-
}
|
|
583
|
-
else if (oldValue !== newValue) {
|
|
584
|
-
newProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, oldValue, newValue, isSvg);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
} else { //delete all old props, cause there are no new props
|
|
588
|
-
for (const key in oldProps) {
|
|
589
|
-
const oldValue = oldProps[key as keyof Props<S>] as PropertyValue<S>;
|
|
590
|
-
oldProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, oldValue, undefined, isSvg);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function patchProperty<S>(patch: Dispatch<S>, node: ChildNode, key: string | keyof ElementEventMap, oldValue?: PropertyValue<S>, newValue?: PropertyValue<S>, isSvg?: boolean) {
|
|
596
|
-
if (key === "style") {
|
|
597
|
-
if (!newValue) {
|
|
598
|
-
(node as HTMLElement).style.cssText = "";
|
|
599
|
-
} else if (oldValue) {
|
|
600
|
-
for (let k in { ...(oldValue as Props<S>), ...(newValue as Props<S>) }) {
|
|
601
|
-
if (!oldValue || newValue[k as keyof PropertyValue<S>] !== oldValue[k as keyof PropertyValue<S>]) {
|
|
602
|
-
(node as HTMLElement).style[k as keyof PropertyValue<S>] = newValue[k as keyof PropertyValue<S>];
|
|
603
|
-
}
|
|
604
|
-
else if (oldValue[k as keyof PropertyValue<S>] && !newValue[k as keyof PropertyValue<S>]) {
|
|
605
|
-
(<any>(node as HTMLElement).style)[k as keyof PropertyValue<S>] = undefined;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
} else {
|
|
609
|
-
for (let k in (newValue as Props<S>)) {
|
|
610
|
-
(node as HTMLElement).style[k as keyof PropertyValue<S>] = newValue[k as keyof PropertyValue<S>];
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
} else if (key === "class") {
|
|
614
|
-
if (isSvg) {
|
|
615
|
-
if (newValue) {
|
|
616
|
-
const newClass = classString(newValue as ClassProp);
|
|
617
|
-
(<SVGSVGElement>node).classList.value = newClass;
|
|
618
|
-
} else {
|
|
619
|
-
(<SVGSVGElement>node).classList.value = '';
|
|
620
|
-
}
|
|
621
|
-
} else {
|
|
622
|
-
if (newValue) {
|
|
623
|
-
const newClass = classString(newValue as ClassProp);
|
|
624
|
-
(<HTMLElement>node).className = newClass;
|
|
625
|
-
} else {
|
|
626
|
-
(<HTMLElement>node).className = '';
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
} else if (key[0] === "o" && key[1] === "n") {
|
|
630
|
-
if (newValue) {
|
|
631
|
-
let eventHandler: Function | null = null;
|
|
632
|
-
if (typeof newValue === "function") {
|
|
633
|
-
const action = newValue as EffectFunction<S>;
|
|
634
|
-
eventHandler = (evt: Event) => patch([action, evt]);
|
|
635
|
-
} else if (Array.isArray(newValue)) {
|
|
636
|
-
const arr = (newValue as Array<any>);
|
|
637
|
-
const action = newValue[0] as EffectFunction<S>;
|
|
638
|
-
if (arr.length > 1) {
|
|
639
|
-
eventHandler = () => patch([action, ...arr.slice(1)]);
|
|
640
|
-
}
|
|
641
|
-
else {
|
|
642
|
-
eventHandler = (evt: Event) => patch([action, evt]);
|
|
643
|
-
}
|
|
644
|
-
} else if (typeof newValue === "object") {
|
|
645
|
-
eventHandler = () => patch(newValue as Patch<S>);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
(<any>node)[key] = eventHandler;
|
|
649
|
-
} else {
|
|
650
|
-
(<any>node)[key] = null;
|
|
651
|
-
}
|
|
652
|
-
} else if (newValue !== null && newValue !== undefined && newValue !== false) {
|
|
653
|
-
(<HTMLElement>node).setAttribute(key, <string>newValue);
|
|
654
|
-
} else {
|
|
655
|
-
(<HTMLElement>node).removeAttribute(key);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
return newValue;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function mergeState(target: any, source: any) {
|
|
662
|
-
if (!source) return target;
|
|
663
|
-
|
|
664
|
-
for (const key in source) {
|
|
665
|
-
const value = source[key];
|
|
666
|
-
if (value && typeof value === "object") {
|
|
667
|
-
const targetValue = target[key];
|
|
668
|
-
if (targetValue) {
|
|
669
|
-
if (Array.isArray(value)) {
|
|
670
|
-
target[key] = [...value];
|
|
671
|
-
} else if (value instanceof Date && targetValue !== value) {
|
|
672
|
-
target[key] = new Date(value);
|
|
673
|
-
} else {
|
|
674
|
-
if (Array.isArray(targetValue)) target[key] = mergeState({}, value);
|
|
675
|
-
else if (typeof targetValue === "object") mergeState(target[key], value);
|
|
676
|
-
else target[key] = mergeState({}, value);
|
|
677
|
-
}
|
|
678
|
-
} else if (Array.isArray(value)) {
|
|
679
|
-
target[key] = [...value];
|
|
680
|
-
} else if (value instanceof Date) {
|
|
681
|
-
target[key] = new Date(value);
|
|
682
|
-
} else {
|
|
683
|
-
target[key] = mergeState({}, value);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
else if (value === undefined) {
|
|
687
|
-
delete target[key];
|
|
688
|
-
}
|
|
689
|
-
else {
|
|
690
|
-
target[key] = value;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
return target;
|
|
1
|
+
export type Vode<S> = FullVode<S> | JustTagVode | NoPropsVode<S>;
|
|
2
|
+
export type ChildVode<S> = Vode<S> | TextVode | NoVode | Component<S>;
|
|
3
|
+
export type FullVode<S> = [tag: Tag, props: Props<S>, ...children: ChildVode<S>[]];
|
|
4
|
+
export type NoPropsVode<S> = [tag: Tag, ...children: ChildVode<S>[]] | string[];
|
|
5
|
+
export type JustTagVode = [tag: Tag];
|
|
6
|
+
export type TextVode = string;
|
|
7
|
+
export type NoVode = undefined | null | number | boolean | bigint | void;
|
|
8
|
+
export type AttachedVode<S> = Vode<S> & { node: ChildNode, id?: string } | Text & { node?: never, id?: never };
|
|
9
|
+
export type Tag = keyof (HTMLElementTagNameMap & SVGElementTagNameMap & MathMLElementTagNameMap);
|
|
10
|
+
export type Component<S> = (s: S) => ChildVode<S>;
|
|
11
|
+
|
|
12
|
+
export type Patch<S> =
|
|
13
|
+
| NoRenderPatch // ignored
|
|
14
|
+
| typeof EmptyPatch | DeepPartial<S> // render patches
|
|
15
|
+
| Promise<Patch<S>> | Effect<S> // effects resulting in patches
|
|
16
|
+
|
|
17
|
+
export const EmptyPatch = {} as const; // smallest patch to cause a render without any changes
|
|
18
|
+
export type NoRenderPatch = undefined | null | number | boolean | bigint | string | symbol | void;
|
|
19
|
+
|
|
20
|
+
export type DeepPartial<S> = { [P in keyof S]?: S[P] extends Array<infer I> ? Array<Patch<I>> : Patch<S[P]> };
|
|
21
|
+
|
|
22
|
+
export type Effect<S> =
|
|
23
|
+
| (() => Patch<S>)
|
|
24
|
+
| EffectFunction<S>
|
|
25
|
+
| [effect: EffectFunction<S>, ...args: any[]]
|
|
26
|
+
| Generator<Patch<S>, unknown, void>
|
|
27
|
+
| AsyncGenerator<Patch<S>, unknown, void>;
|
|
28
|
+
|
|
29
|
+
export type EffectFunction<S> = (state: S, ...args: any[]) => Patch<S>;
|
|
30
|
+
|
|
31
|
+
export type Dispatch<S> = (action: Patch<S>) => void;
|
|
32
|
+
export type PatchableState<S> = S & { patch: Dispatch<Patch<S>> };
|
|
33
|
+
|
|
34
|
+
export type Props<S> = Partial<
|
|
35
|
+
Omit<HTMLElement,
|
|
36
|
+
keyof (DocumentFragment & ElementCSSInlineStyle & GlobalEventHandlers)> &
|
|
37
|
+
{ [K in keyof EventsMap]: Patch<S> } // all on* events
|
|
38
|
+
> & {
|
|
39
|
+
[_: string]: unknown,
|
|
40
|
+
class?: ClassProp,
|
|
41
|
+
style?: StyleProp,
|
|
42
|
+
/** called after the element was attached */
|
|
43
|
+
onMount?: MountFunction<S>,
|
|
44
|
+
/** called before the element is detached */
|
|
45
|
+
onUnmount?: MountFunction<S>,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type MountFunction<S> =
|
|
49
|
+
| ((s: S, node: HTMLElement) => Patch<S>)
|
|
50
|
+
| ((s: S, node: SVGSVGElement) => Patch<S>)
|
|
51
|
+
| ((s: S, node: MathMLElement) => Patch<S>)
|
|
52
|
+
|
|
53
|
+
export type ClassProp =
|
|
54
|
+
| "" | false | null | undefined // no class
|
|
55
|
+
| string // "class1 class2"
|
|
56
|
+
| string[] // ["class1", "class2"]
|
|
57
|
+
| Record<string, boolean | undefined | null> // { class1: true, class2: false }
|
|
58
|
+
|
|
59
|
+
export type StyleProp = Record<number, never> & {
|
|
60
|
+
[K in keyof CSSStyleDeclaration]?: CSSStyleDeclaration[K] | null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type EventsMap =
|
|
64
|
+
& { [K in keyof HTMLElementEventMap as `on${K}`]: HTMLElementEventMap[K] }
|
|
65
|
+
& { [K in keyof WindowEventMap as `on${K}`]: WindowEventMap[K] }
|
|
66
|
+
& { [K in keyof SVGElementEventMap as `on${K}`]: SVGElementEventMap[K] }
|
|
67
|
+
& { onsearch: Event }
|
|
68
|
+
|
|
69
|
+
export type PropertyValue<S> = string | boolean | null | undefined | StyleProp | ClassProp | Patch<S> | void;
|
|
70
|
+
|
|
71
|
+
export type ContainerNode<S> = HTMLElement & {
|
|
72
|
+
state: PatchableState<S>,
|
|
73
|
+
vode: AttachedVode<S>,
|
|
74
|
+
patch: Dispatch<S>,
|
|
75
|
+
render: () => void,
|
|
76
|
+
q: Patch<S>[]
|
|
77
|
+
isRendering: boolean,
|
|
78
|
+
stats: {
|
|
79
|
+
patchCount: number,
|
|
80
|
+
liveEffectCount: number,
|
|
81
|
+
renderPatchCount: number,
|
|
82
|
+
renderCount: number,
|
|
83
|
+
renderTime: number,
|
|
84
|
+
queueLengthBeforeRender: number,
|
|
85
|
+
queueLengthAfterRender: number,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** type-safe way to create a vode. useful for type inference and autocompletion.
|
|
90
|
+
*
|
|
91
|
+
* overloads:
|
|
92
|
+
* - just a tag: `vode("div") // => ["div"]`
|
|
93
|
+
* - tag and props: `vode("div", { class: "foo" }) // => ["div", { class: "foo" }]`
|
|
94
|
+
* - tag, props and children: `vode("div", { class: "foo" }, ["span", "bar"]) // => ["div", { class: "foo" }, ["span", "bar"]]`
|
|
95
|
+
* - identity: `vode(["div", ["span", "bar"]]) // => ["div", ["span", "bar"]]`
|
|
96
|
+
*/
|
|
97
|
+
export function vode<S extends object | unknown>(tag: Tag | Vode<S>, props?: Props<S> | ChildVode<S>, ...children: ChildVode<S>[]): Vode<S> {
|
|
98
|
+
if (Array.isArray(tag)) {
|
|
99
|
+
return tag;
|
|
100
|
+
}
|
|
101
|
+
if (props) {
|
|
102
|
+
return [tag, props as Props<S>, ...children];
|
|
103
|
+
}
|
|
104
|
+
return [tag, ...children];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** pass an object whose type determines the initial state */
|
|
108
|
+
export function createState<S>(state: S): PatchableState<S> { return state as PatchableState<S>; }
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
/** for a type safe way to create a deeply partial patch object or effect */
|
|
112
|
+
export function patch<S>(p: DeepPartial<S> | Effect<S> | NoRenderPatch): typeof p { return p; }
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* create a vode app inside a container element
|
|
116
|
+
* @param container will use this container as root and places the result of the dom function and further renderings in it
|
|
117
|
+
* @param initialState @see createState
|
|
118
|
+
* @param dom creates the initial dom from the state and is called on every render
|
|
119
|
+
* @param initialPatches variadic list of patches that are applied after the first render
|
|
120
|
+
* @returns a patch function that can be used to update the state
|
|
121
|
+
*/
|
|
122
|
+
export function app<S>(container: HTMLElement, initialState: Omit<S, "patch">, dom: Component<S>, ...initialPatches: Patch<S>[]) {
|
|
123
|
+
const root = container as ContainerNode<S>;
|
|
124
|
+
root.stats = { renderTime: 0, renderCount: 0, queueLengthBeforeRender: 0, queueLengthAfterRender: 0, liveEffectCount: 0, patchCount: 0, renderPatchCount: 0 };
|
|
125
|
+
|
|
126
|
+
Object.defineProperty(initialState, "patch", {
|
|
127
|
+
enumerable: false, configurable: true,
|
|
128
|
+
writable: false, value: async (action: Patch<S>) => {
|
|
129
|
+
if (!action || (typeof action !== "function" && typeof action !== "object")) return;
|
|
130
|
+
root.stats.patchCount++;
|
|
131
|
+
|
|
132
|
+
if ((action as AsyncGenerator<Patch<S>, unknown, void>)?.next) {
|
|
133
|
+
const generator = action as AsyncGenerator<Patch<S>, unknown, void>;
|
|
134
|
+
root.stats.liveEffectCount++;
|
|
135
|
+
try {
|
|
136
|
+
let v = await generator.next();
|
|
137
|
+
while (v.done === false) {
|
|
138
|
+
root.stats.liveEffectCount++;
|
|
139
|
+
try {
|
|
140
|
+
root.patch!(v.value);
|
|
141
|
+
v = await generator.next();
|
|
142
|
+
} finally {
|
|
143
|
+
root.stats.liveEffectCount--;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
root.patch!(v.value as Patch<S>);
|
|
147
|
+
} finally {
|
|
148
|
+
root.stats.liveEffectCount--;
|
|
149
|
+
}
|
|
150
|
+
} else if ((action as Promise<S>).then) {
|
|
151
|
+
root.stats.liveEffectCount++;
|
|
152
|
+
try {
|
|
153
|
+
const nextState = await (action as Promise<S>);
|
|
154
|
+
root.patch!(<Patch<S>>nextState);
|
|
155
|
+
} finally {
|
|
156
|
+
root.stats.liveEffectCount--;
|
|
157
|
+
}
|
|
158
|
+
} else if (Array.isArray(action)) {
|
|
159
|
+
if (typeof action[0] === "function") {
|
|
160
|
+
if (action.length > 1)
|
|
161
|
+
root.patch!(action[0](root.state!, ...(action as any[]).slice(1)));
|
|
162
|
+
else root.patch!(action[0](root.state!));
|
|
163
|
+
} else {
|
|
164
|
+
root.stats.patchCount--;
|
|
165
|
+
}
|
|
166
|
+
} else if (typeof action === "function") {
|
|
167
|
+
root.patch!((<EffectFunction<S>>action)(root.state));
|
|
168
|
+
} else {
|
|
169
|
+
root.stats.renderPatchCount++;
|
|
170
|
+
root.q!.push(<Patch<S>>action);
|
|
171
|
+
if (!root.isRendering) root.render!();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
Object.defineProperty(root, "render", {
|
|
177
|
+
enumerable: false, configurable: true,
|
|
178
|
+
writable: false, value: () => requestAnimationFrame(() => {
|
|
179
|
+
if (root.isRendering || root.q!.length === 0) return;
|
|
180
|
+
root.isRendering = true;
|
|
181
|
+
const sw = Date.now();
|
|
182
|
+
try {
|
|
183
|
+
root.stats.queueLengthBeforeRender = root.q!.length;
|
|
184
|
+
|
|
185
|
+
while (root.q!.length > 0) {
|
|
186
|
+
const patch = root.q!.shift();
|
|
187
|
+
if(patch === EmptyPatch) continue;
|
|
188
|
+
mergeState(root.state, patch);
|
|
189
|
+
}
|
|
190
|
+
root.vode = render(root.state, root.patch, container, 0, root.vode, dom(root.state))!;
|
|
191
|
+
} finally {
|
|
192
|
+
root.isRendering = false;
|
|
193
|
+
root.stats.renderCount++;
|
|
194
|
+
root.stats.renderTime = Date.now() - sw;
|
|
195
|
+
root.stats.queueLengthAfterRender = root.q!.length;
|
|
196
|
+
if (root.q!.length > 0) {
|
|
197
|
+
root.render!();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
root.patch = (<PatchableState<S>>initialState).patch;
|
|
204
|
+
root.state = <PatchableState<S>>initialState;
|
|
205
|
+
root.q = [];
|
|
206
|
+
const initialVode = dom(<S>initialState);
|
|
207
|
+
root.vode = <AttachedVode<S>>initialVode;
|
|
208
|
+
root.vode = render(<S>initialState, root.patch!, container, 0, undefined, initialVode)!;
|
|
209
|
+
|
|
210
|
+
for (const effect of initialPatches) {
|
|
211
|
+
root.patch!(effect);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return root.patch;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** get properties of a vode, if there are any */
|
|
218
|
+
export function props<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): Props<S> | undefined {
|
|
219
|
+
if (Array.isArray(vode)
|
|
220
|
+
&& vode.length > 1
|
|
221
|
+
&& vode[1]
|
|
222
|
+
&& !Array.isArray(vode[1])
|
|
223
|
+
) {
|
|
224
|
+
if (
|
|
225
|
+
typeof vode[1] === "object"
|
|
226
|
+
&& (vode[1] as unknown as Node).nodeType !== Node.TEXT_NODE
|
|
227
|
+
) {
|
|
228
|
+
return vode[1];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function mergeClass(a: ClassProp, b: ClassProp): ClassProp {
|
|
236
|
+
if (!a) return b;
|
|
237
|
+
if (!b) return a;
|
|
238
|
+
|
|
239
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
240
|
+
const aSplit = a.split(" ");
|
|
241
|
+
const bSplit = b.split(" ");
|
|
242
|
+
const classSet = new Set([...aSplit, ...bSplit]);
|
|
243
|
+
return Array.from(classSet).join(" ").trim();
|
|
244
|
+
}
|
|
245
|
+
else if (typeof a === "string" && Array.isArray(b)) {
|
|
246
|
+
const classSet = new Set([...b, ...a.split(" ")]);
|
|
247
|
+
return Array.from(classSet).join(" ").trim();
|
|
248
|
+
}
|
|
249
|
+
else if (Array.isArray(a) && typeof b === "string") {
|
|
250
|
+
const classSet = new Set([...a, ...b.split(" ")]);
|
|
251
|
+
return Array.from(classSet).join(" ").trim();
|
|
252
|
+
}
|
|
253
|
+
else if (Array.isArray(a) && Array.isArray(b)) {
|
|
254
|
+
const classSet = new Set([...a, ...b]);
|
|
255
|
+
return Array.from(classSet).join(" ").trim();
|
|
256
|
+
}
|
|
257
|
+
else if (typeof a === "string" && typeof b === "object") {
|
|
258
|
+
return { [a]: true, ...b };
|
|
259
|
+
}
|
|
260
|
+
else if (typeof a === "object" && typeof b === "string") {
|
|
261
|
+
return { ...a, [b]: true };
|
|
262
|
+
}
|
|
263
|
+
else if (typeof a === "object" && typeof b === "object") {
|
|
264
|
+
return { ...a, ...b };
|
|
265
|
+
} else if (typeof a === "object" && Array.isArray(b)) {
|
|
266
|
+
const aa = { ...a };
|
|
267
|
+
for (const item of b as string[]) {
|
|
268
|
+
(<Record<string, boolean | null | undefined>>aa)[item] = true;
|
|
269
|
+
}
|
|
270
|
+
return aa;
|
|
271
|
+
} else if (Array.isArray(a) && typeof b === "object") {
|
|
272
|
+
const aa: Record<string, any> = {};
|
|
273
|
+
for (const item of a as string[]) {
|
|
274
|
+
aa[item] = true;
|
|
275
|
+
}
|
|
276
|
+
for (const bKey of (<Record<string, any>>b).keys) {
|
|
277
|
+
aa[bKey] = (<Record<string, boolean | null | undefined>>b)[bKey];
|
|
278
|
+
}
|
|
279
|
+
return b;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
throw new Error(`cannot merge classes of ${a} (${typeof a}) and ${b} (${typeof b})`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function patchProps<S extends object | unknown>(vode: Vode<S>, props: Props<S>): void {
|
|
286
|
+
if (!Array.isArray(vode)) return;
|
|
287
|
+
|
|
288
|
+
if (vode.length > 1) {
|
|
289
|
+
if (!Array.isArray(vode[1]) && typeof vode[1] === "object") {
|
|
290
|
+
vode[1] = merge(vode[1], props);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (childCount(vode) > 0) {
|
|
295
|
+
(<FullVode<S>>vode).push(null);
|
|
296
|
+
}
|
|
297
|
+
for (let i = vode.length - 1; i > 0; i--) {
|
|
298
|
+
if (i > 1) vode[i] = vode[i - 1];
|
|
299
|
+
}
|
|
300
|
+
vode[1] = props;
|
|
301
|
+
} else {
|
|
302
|
+
(<FullVode<S>>vode).push(props);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** get a slice of all children of a vode, if there are any */
|
|
307
|
+
export function children<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): ChildVode<S>[] | undefined {
|
|
308
|
+
const start = childrenStart(vode);
|
|
309
|
+
if (start > 0) {
|
|
310
|
+
return (<Vode<S>>vode).slice(start) as Vode<S>[];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** index in vode at which child-vodes start */
|
|
317
|
+
export function childrenStart<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): number {
|
|
318
|
+
return props(vode) ? 2 : 1;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** html tag of the vode or #text if it is a text node */
|
|
322
|
+
export function tag<S extends object | unknown>(v: Vode<S> | TextVode | NoVode | AttachedVode<S>): Tag | "#text" | undefined {
|
|
323
|
+
return !!v ? (Array.isArray(v)
|
|
324
|
+
? v[0] : (typeof v === "string" || (<any>v).nodeType === Node.TEXT_NODE)
|
|
325
|
+
? "#text" : undefined) as Tag
|
|
326
|
+
: undefined;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function childCount<S>(vode: Vode<S>) { return vode.length - childrenStart(vode); }
|
|
330
|
+
|
|
331
|
+
export function child<S>(vode: Vode<S>, index: number): ChildVode<S> | undefined {
|
|
332
|
+
return vode[index + childrenStart(vode)] as ChildVode<S>;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** merge multiple objects into one, applying from left to right
|
|
336
|
+
* @param first object to merge
|
|
337
|
+
* @returns merged object
|
|
338
|
+
*/
|
|
339
|
+
export function merge(first?: any, ...p: any[]): any {
|
|
340
|
+
first = mergeState({}, first);
|
|
341
|
+
for (const pp of p) {
|
|
342
|
+
if (!pp) continue;
|
|
343
|
+
first = mergeState(first, pp);
|
|
344
|
+
}
|
|
345
|
+
return first!;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function classString(classProp: ClassProp): string {
|
|
349
|
+
if (typeof classProp === "string") {
|
|
350
|
+
return classProp;
|
|
351
|
+
} else if (Array.isArray(classProp)) {
|
|
352
|
+
return classProp.map(classString).join(" ");
|
|
353
|
+
} else if (typeof classProp === "object") {
|
|
354
|
+
return Object.keys(classProp!).filter(k => classProp![k]).join(" ");
|
|
355
|
+
} else {
|
|
356
|
+
return "";
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function isNaturalVode(x: ChildVode<any>) {
|
|
361
|
+
return Array.isArray(x) && x.length > 0 && typeof x[0] === "string";
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function isTextVode(x: ChildVode<any>) {
|
|
365
|
+
return typeof x === "string" || (<Text><unknown>x)?.nodeType === Node.TEXT_NODE;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function unwrap<S>(c: Component<S> | ChildVode<S>, s: S): ChildVode<S> {
|
|
369
|
+
if (typeof c === "function") {
|
|
370
|
+
return unwrap(c(s), s);
|
|
371
|
+
} else {
|
|
372
|
+
return c;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** memoization of the given component or props (compare array is compared element by element (===) with the previous render) */
|
|
377
|
+
export function memo<S extends object | unknown>(compare: any[], componentOrProps: Component<S> | ((s: S) => Props<S>)): typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S> {
|
|
378
|
+
(<any>componentOrProps).__memo = compare;
|
|
379
|
+
return componentOrProps as typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S>;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function remember<S>(state: S, present: any, past: any): ChildVode<S> | AttachedVode<S> {
|
|
383
|
+
if (typeof present !== "function")
|
|
384
|
+
return present;
|
|
385
|
+
|
|
386
|
+
const presentMemo = present?.__memo;
|
|
387
|
+
const pastMemo = past?.__memo;
|
|
388
|
+
|
|
389
|
+
if (Array.isArray(presentMemo)
|
|
390
|
+
&& Array.isArray(pastMemo)
|
|
391
|
+
&& presentMemo.length === pastMemo.length
|
|
392
|
+
) {
|
|
393
|
+
let same = true;
|
|
394
|
+
for (let i = 0; i < presentMemo.length; i++) {
|
|
395
|
+
if (presentMemo[i] !== pastMemo[i]) {
|
|
396
|
+
same = false;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (same) return past;
|
|
401
|
+
}
|
|
402
|
+
const newRender = unwrap(present, state);
|
|
403
|
+
if (typeof newRender === "object") {
|
|
404
|
+
(<any>newRender).__memo = present?.__memo;
|
|
405
|
+
}
|
|
406
|
+
return newRender;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function render<S>(state: S, patch: Dispatch<S>, parent: ChildNode, childIndex: number, oldVode: AttachedVode<S> | undefined, newVode: ChildVode<S>, svg?: boolean): AttachedVode<S> | undefined {
|
|
410
|
+
// unwrap component if it is memoized
|
|
411
|
+
newVode = remember(state, newVode, oldVode) as ChildVode<S>;
|
|
412
|
+
|
|
413
|
+
const isNoVode = !newVode || typeof newVode === "number" || typeof newVode === "boolean";
|
|
414
|
+
if (newVode === oldVode || (!oldVode && isNoVode)) {
|
|
415
|
+
return oldVode;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const oldIsText = (oldVode as Text)?.nodeType === Node.TEXT_NODE;
|
|
419
|
+
const oldNode: ChildNode | undefined = oldIsText ? oldVode as Text : oldVode?.node;
|
|
420
|
+
|
|
421
|
+
// falsy|text|element(A) -> undefined
|
|
422
|
+
if (isNoVode) {
|
|
423
|
+
(<any>oldNode)?.onUnmount && patch((<any>oldNode).onUnmount(oldNode));
|
|
424
|
+
oldNode?.remove();
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const isText = !isNoVode && isTextVode(newVode);
|
|
429
|
+
const isNode = !isNoVode && isNaturalVode(newVode);
|
|
430
|
+
const alreadyAttached = !!newVode && typeof newVode !== "string" && !!((<any>newVode)?.node || (<any>newVode)?.nodeType === Node.TEXT_NODE);
|
|
431
|
+
|
|
432
|
+
if (!isText && !isNode && !alreadyAttached && !oldVode) {
|
|
433
|
+
throw new Error("Invalid vode: " + typeof newVode + " " + JSON.stringify(newVode));
|
|
434
|
+
}
|
|
435
|
+
else if (alreadyAttached && isText) {
|
|
436
|
+
newVode = (<Text><any>newVode).wholeText;
|
|
437
|
+
}
|
|
438
|
+
else if (alreadyAttached && isNode) {
|
|
439
|
+
newVode = [...<Vode<S>>newVode];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// text -> text
|
|
443
|
+
if (oldIsText && isText) {
|
|
444
|
+
if ((<Text>oldNode).nodeValue !== <string>newVode) {
|
|
445
|
+
(<Text>oldNode).nodeValue = <string>newVode;
|
|
446
|
+
}
|
|
447
|
+
return oldVode;
|
|
448
|
+
}
|
|
449
|
+
// falsy|element -> text
|
|
450
|
+
if (isText && (!oldNode || !oldIsText)) {
|
|
451
|
+
const text = document.createTextNode(newVode as string)
|
|
452
|
+
if (oldNode) {
|
|
453
|
+
(<any>oldNode).onUnmount && patch((<any>oldNode).onUnmount(oldNode));
|
|
454
|
+
oldNode.replaceWith(text);
|
|
455
|
+
} else {
|
|
456
|
+
if (parent.childNodes[childIndex]) {
|
|
457
|
+
parent.insertBefore(text, parent.childNodes[childIndex]);
|
|
458
|
+
} else {
|
|
459
|
+
parent.appendChild(text);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return text as Text;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// falsy|text|element(A) -> element(B)
|
|
467
|
+
if (
|
|
468
|
+
(isNode && (!oldNode || oldIsText || (<Vode<S>>oldVode)[0] !== (<Vode<S>>newVode)[0]))
|
|
469
|
+
) {
|
|
470
|
+
svg = svg || (<Vode<S>>newVode)[0] === "svg";
|
|
471
|
+
const newNode: ChildNode = svg
|
|
472
|
+
? document.createElementNS("http://www.w3.org/2000/svg", (<Vode<S>>newVode)[0])
|
|
473
|
+
: document.createElement((<Vode<S>>newVode)[0]);
|
|
474
|
+
(<AttachedVode<S>>newVode).node = newNode;
|
|
475
|
+
|
|
476
|
+
const newvode = <Vode<S>>newVode;
|
|
477
|
+
if (1 in newvode) {
|
|
478
|
+
newvode[1] = remember(state, newvode[1], undefined) as Vode<S>;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const properties = props(newVode);
|
|
482
|
+
patchProperties(patch, newNode, undefined, properties, svg);
|
|
483
|
+
|
|
484
|
+
if (oldNode) {
|
|
485
|
+
(<any>oldNode).onUnmount && patch((<any>oldNode).onUnmount(oldNode));
|
|
486
|
+
oldNode.replaceWith(newNode);
|
|
487
|
+
} else {
|
|
488
|
+
if (parent.childNodes[childIndex]) {
|
|
489
|
+
parent.insertBefore(newNode, parent.childNodes[childIndex]);
|
|
490
|
+
} else {
|
|
491
|
+
parent.appendChild(newNode);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const newChildren = children(newVode);
|
|
496
|
+
if (newChildren) {
|
|
497
|
+
for (let i = 0; i < newChildren.length; i++) {
|
|
498
|
+
const child = newChildren[i];
|
|
499
|
+
const attached = render(state, patch, newNode, i, undefined, child, svg);
|
|
500
|
+
(<Vode<S>>newVode!)[properties ? i + 2 : i + 1] = <Vode<S>>attached;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
(<any>newNode).onMount && patch((<any>newNode).onMount(newNode));
|
|
505
|
+
return <AttachedVode<S>>newVode;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
//element(A) -> element(A)
|
|
509
|
+
if (!oldIsText && isNode && (<Vode<S>>oldVode)[0] === (<Vode<S>>newVode)[0]) {
|
|
510
|
+
svg = svg || (<Vode<S>>newVode)[0] === "svg";
|
|
511
|
+
(<AttachedVode<S>>newVode).node = oldNode;
|
|
512
|
+
|
|
513
|
+
const newvode = <Vode<S>>newVode;
|
|
514
|
+
const oldvode = <Vode<S>>oldVode;
|
|
515
|
+
|
|
516
|
+
let hasProps = false;
|
|
517
|
+
if ((<any>newvode[1])?.__memo) {
|
|
518
|
+
const prev = newvode[1] as any;
|
|
519
|
+
newvode[1] = remember(state, newvode[1], oldvode[1]) as Vode<S>;
|
|
520
|
+
if (prev !== newvode[1]) {
|
|
521
|
+
const properties = props(newVode);
|
|
522
|
+
patchProperties(patch, oldNode!, props(oldVode), properties, svg);
|
|
523
|
+
hasProps = !!properties;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
const properties = props(newVode);
|
|
528
|
+
patchProperties(patch, oldNode!, props(oldVode), properties, svg);
|
|
529
|
+
hasProps = !!properties;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const newKids = children(newVode);
|
|
533
|
+
const oldKids = children(oldVode) as AttachedVode<S>[];
|
|
534
|
+
if (newKids) {
|
|
535
|
+
for (let i = 0; i < newKids.length; i++) {
|
|
536
|
+
const child = newKids[i];
|
|
537
|
+
const oldChild = oldKids && oldKids[i];
|
|
538
|
+
|
|
539
|
+
const attached = render(state, patch, oldNode!, i, oldChild, child, svg);
|
|
540
|
+
if (attached) {
|
|
541
|
+
(<Vode<S>>newVode)[hasProps ? i + 2 : i + 1] = <Vode<S>>attached;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
for (let i = newKids.length; oldKids && i < oldKids.length; i++) {
|
|
545
|
+
if (oldKids[i]?.node)
|
|
546
|
+
oldKids[i].node!.remove();
|
|
547
|
+
else if ((oldKids[i] as Text)?.nodeType === Node.TEXT_NODE)
|
|
548
|
+
(oldKids[i] as Text).remove();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
for (let i = newKids?.length || 0; i < oldKids?.length || 0; i++) {
|
|
553
|
+
if (oldKids[i]?.node)
|
|
554
|
+
oldKids[i].node!.remove();
|
|
555
|
+
else if ((oldKids[i] as Text)?.nodeType === Node.TEXT_NODE)
|
|
556
|
+
(oldKids[i] as Text).remove();
|
|
557
|
+
}
|
|
558
|
+
return <AttachedVode<S>>newVode;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function patchProperties<S>(patch: Dispatch<S>, node: ChildNode, oldProps?: Props<S>, newProps?: Props<S>, isSvg?: boolean) {
|
|
565
|
+
if (!newProps && !oldProps) return;
|
|
566
|
+
if (!oldProps) { // set new props
|
|
567
|
+
for (const key in newProps) {
|
|
568
|
+
const newValue = newProps[key as keyof Props<S>] as PropertyValue<S>;
|
|
569
|
+
newProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, undefined, newValue, isSvg);
|
|
570
|
+
}
|
|
571
|
+
} else if (newProps) { // clear old props and set new in one loop
|
|
572
|
+
const combinedKeys = new Set([...Object.keys(oldProps), ...Object.keys(newProps)]);
|
|
573
|
+
for (const key of combinedKeys) {
|
|
574
|
+
const oldValue = oldProps[key as keyof Props<S>] as PropertyValue<S>;
|
|
575
|
+
const newValue = newProps[key as keyof Props<S>] as PropertyValue<S>;
|
|
576
|
+
if (key[0] === "o" && key[1] === "n") {
|
|
577
|
+
const oldEvent = (<any>node)["__" + key];
|
|
578
|
+
if ((oldEvent && oldEvent !== newValue) || (!oldEvent && oldValue !== newValue)) {
|
|
579
|
+
newProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, oldValue, newValue, isSvg);
|
|
580
|
+
}
|
|
581
|
+
(<any>node)["__" + key] = newValue;
|
|
582
|
+
}
|
|
583
|
+
else if (oldValue !== newValue) {
|
|
584
|
+
newProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, oldValue, newValue, isSvg);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
} else { //delete all old props, cause there are no new props
|
|
588
|
+
for (const key in oldProps) {
|
|
589
|
+
const oldValue = oldProps[key as keyof Props<S>] as PropertyValue<S>;
|
|
590
|
+
oldProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, oldValue, undefined, isSvg);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function patchProperty<S>(patch: Dispatch<S>, node: ChildNode, key: string | keyof ElementEventMap, oldValue?: PropertyValue<S>, newValue?: PropertyValue<S>, isSvg?: boolean) {
|
|
596
|
+
if (key === "style") {
|
|
597
|
+
if (!newValue) {
|
|
598
|
+
(node as HTMLElement).style.cssText = "";
|
|
599
|
+
} else if (oldValue) {
|
|
600
|
+
for (let k in { ...(oldValue as Props<S>), ...(newValue as Props<S>) }) {
|
|
601
|
+
if (!oldValue || newValue[k as keyof PropertyValue<S>] !== oldValue[k as keyof PropertyValue<S>]) {
|
|
602
|
+
(node as HTMLElement).style[k as keyof PropertyValue<S>] = newValue[k as keyof PropertyValue<S>];
|
|
603
|
+
}
|
|
604
|
+
else if (oldValue[k as keyof PropertyValue<S>] && !newValue[k as keyof PropertyValue<S>]) {
|
|
605
|
+
(<any>(node as HTMLElement).style)[k as keyof PropertyValue<S>] = undefined;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
for (let k in (newValue as Props<S>)) {
|
|
610
|
+
(node as HTMLElement).style[k as keyof PropertyValue<S>] = newValue[k as keyof PropertyValue<S>];
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
} else if (key === "class") {
|
|
614
|
+
if (isSvg) {
|
|
615
|
+
if (newValue) {
|
|
616
|
+
const newClass = classString(newValue as ClassProp);
|
|
617
|
+
(<SVGSVGElement>node).classList.value = newClass;
|
|
618
|
+
} else {
|
|
619
|
+
(<SVGSVGElement>node).classList.value = '';
|
|
620
|
+
}
|
|
621
|
+
} else {
|
|
622
|
+
if (newValue) {
|
|
623
|
+
const newClass = classString(newValue as ClassProp);
|
|
624
|
+
(<HTMLElement>node).className = newClass;
|
|
625
|
+
} else {
|
|
626
|
+
(<HTMLElement>node).className = '';
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
} else if (key[0] === "o" && key[1] === "n") {
|
|
630
|
+
if (newValue) {
|
|
631
|
+
let eventHandler: Function | null = null;
|
|
632
|
+
if (typeof newValue === "function") {
|
|
633
|
+
const action = newValue as EffectFunction<S>;
|
|
634
|
+
eventHandler = (evt: Event) => patch([action, evt]);
|
|
635
|
+
} else if (Array.isArray(newValue)) {
|
|
636
|
+
const arr = (newValue as Array<any>);
|
|
637
|
+
const action = newValue[0] as EffectFunction<S>;
|
|
638
|
+
if (arr.length > 1) {
|
|
639
|
+
eventHandler = () => patch([action, ...arr.slice(1)]);
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
eventHandler = (evt: Event) => patch([action, evt]);
|
|
643
|
+
}
|
|
644
|
+
} else if (typeof newValue === "object") {
|
|
645
|
+
eventHandler = () => patch(newValue as Patch<S>);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
(<any>node)[key] = eventHandler;
|
|
649
|
+
} else {
|
|
650
|
+
(<any>node)[key] = null;
|
|
651
|
+
}
|
|
652
|
+
} else if (newValue !== null && newValue !== undefined && newValue !== false) {
|
|
653
|
+
(<HTMLElement>node).setAttribute(key, <string>newValue);
|
|
654
|
+
} else {
|
|
655
|
+
(<HTMLElement>node).removeAttribute(key);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return newValue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function mergeState(target: any, source: any) {
|
|
662
|
+
if (!source) return target;
|
|
663
|
+
|
|
664
|
+
for (const key in source) {
|
|
665
|
+
const value = source[key];
|
|
666
|
+
if (value && typeof value === "object") {
|
|
667
|
+
const targetValue = target[key];
|
|
668
|
+
if (targetValue) {
|
|
669
|
+
if (Array.isArray(value)) {
|
|
670
|
+
target[key] = [...value];
|
|
671
|
+
} else if (value instanceof Date && targetValue !== value) {
|
|
672
|
+
target[key] = new Date(value);
|
|
673
|
+
} else {
|
|
674
|
+
if (Array.isArray(targetValue)) target[key] = mergeState({}, value);
|
|
675
|
+
else if (typeof targetValue === "object") mergeState(target[key], value);
|
|
676
|
+
else target[key] = mergeState({}, value);
|
|
677
|
+
}
|
|
678
|
+
} else if (Array.isArray(value)) {
|
|
679
|
+
target[key] = [...value];
|
|
680
|
+
} else if (value instanceof Date) {
|
|
681
|
+
target[key] = new Date(value);
|
|
682
|
+
} else {
|
|
683
|
+
target[key] = mergeState({}, value);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
else if (value === undefined) {
|
|
687
|
+
delete target[key];
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
target[key] = value;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return target;
|
|
694
694
|
};
|