@ryupold/vode 0.10.0 → 0.11.0
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 +1 -1
- package/src/vode.ts +143 -163
- package/vode.mjs +116 -137
package/package.json
CHANGED
package/src/vode.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type Component<S> = (s: S) => ChildVode<S>;
|
|
|
12
12
|
export type Patch<S> =
|
|
13
13
|
| NoRenderPatch // ignored
|
|
14
14
|
| typeof EmptyPatch | DeepPartial<S> // render patches
|
|
15
|
-
| Promise<Patch<S>> | Effect<S
|
|
15
|
+
| Promise<Patch<S>> | Effect<S>; // effects resulting in patches
|
|
16
16
|
|
|
17
17
|
export const EmptyPatch = {} as const; // smallest patch to cause a render without any changes
|
|
18
18
|
export type NoRenderPatch = undefined | null | number | boolean | bigint | string | symbol | void;
|
|
@@ -28,9 +28,6 @@ export type Effect<S> =
|
|
|
28
28
|
|
|
29
29
|
export type EffectFunction<S> = (state: S, ...args: any[]) => Patch<S>;
|
|
30
30
|
|
|
31
|
-
export type Dispatch<S> = (action: Patch<S>) => void;
|
|
32
|
-
export type PatchableState<S> = S & { patch: Dispatch<Patch<S>> };
|
|
33
|
-
|
|
34
31
|
export type Props<S> = Partial<
|
|
35
32
|
Omit<HTMLElement,
|
|
36
33
|
keyof (DocumentFragment & ElementCSSInlineStyle & GlobalEventHandlers)> &
|
|
@@ -48,34 +45,42 @@ export type Props<S> = Partial<
|
|
|
48
45
|
export type MountFunction<S> =
|
|
49
46
|
| ((s: S, node: HTMLElement) => Patch<S>)
|
|
50
47
|
| ((s: S, node: SVGSVGElement) => Patch<S>)
|
|
51
|
-
| ((s: S, node: MathMLElement) => Patch<S>)
|
|
48
|
+
| ((s: S, node: MathMLElement) => Patch<S>);
|
|
52
49
|
|
|
53
50
|
export type ClassProp =
|
|
54
51
|
| "" | false | null | undefined // no class
|
|
55
52
|
| string // "class1 class2"
|
|
56
53
|
| string[] // ["class1", "class2"]
|
|
57
|
-
| Record<string, boolean | undefined | null
|
|
54
|
+
| Record<string, boolean | undefined | null>; // { class1: true, class2: false }
|
|
58
55
|
|
|
59
56
|
export type StyleProp = Record<number, never> & {
|
|
60
57
|
[K in keyof CSSStyleDeclaration]?: CSSStyleDeclaration[K] | null
|
|
61
|
-
}
|
|
58
|
+
};
|
|
62
59
|
|
|
63
60
|
export type EventsMap =
|
|
64
61
|
& { [K in keyof HTMLElementEventMap as `on${K}`]: HTMLElementEventMap[K] }
|
|
65
62
|
& { [K in keyof WindowEventMap as `on${K}`]: WindowEventMap[K] }
|
|
66
63
|
& { [K in keyof SVGElementEventMap as `on${K}`]: SVGElementEventMap[K] }
|
|
67
|
-
& { onsearch: Event }
|
|
64
|
+
& { onsearch: Event };
|
|
68
65
|
|
|
69
66
|
export type PropertyValue<S> = string | boolean | null | undefined | StyleProp | ClassProp | Patch<S> | void;
|
|
70
67
|
|
|
68
|
+
|
|
69
|
+
export type Dispatch<S> = (action: Patch<S>) => void;
|
|
70
|
+
export type PatchableState<S> = S & { patch: Dispatch<Patch<S>> };
|
|
71
|
+
|
|
71
72
|
export type ContainerNode<S> = HTMLElement & {
|
|
73
|
+
/** the `_vode` property is added to the container in `app()`.
|
|
74
|
+
* it contains all necessary stuff for the vode app to function.
|
|
75
|
+
* delete it to clear all resources of the vode app, or remove the container itself */
|
|
72
76
|
_vode: {
|
|
73
|
-
state: PatchableState<S>,
|
|
74
|
-
vode: AttachedVode<S>,
|
|
75
|
-
patch: Dispatch<S>,
|
|
76
|
-
render: () => void,
|
|
77
|
-
q: Patch<S>[]
|
|
78
|
-
isRendering: boolean,
|
|
77
|
+
state: PatchableState<S>, // can touch this, but let it be an object
|
|
78
|
+
vode: AttachedVode<S>, //don't touch this
|
|
79
|
+
patch: Dispatch<S>, // can't touch this
|
|
80
|
+
render: () => void, // can't touch this
|
|
81
|
+
q: Patch<S>[], // this will change in the future, so don't touch it
|
|
82
|
+
isRendering: boolean, // under no circumstances touch this
|
|
83
|
+
/** stats about the overall patches & last render time */
|
|
79
84
|
stats: {
|
|
80
85
|
patchCount: number,
|
|
81
86
|
liveEffectCount: number,
|
|
@@ -88,13 +93,18 @@ export type ContainerNode<S> = HTMLElement & {
|
|
|
88
93
|
}
|
|
89
94
|
};
|
|
90
95
|
|
|
96
|
+
/** create a state object used as initial state for `app()`. it is updated with `PatchableState.patch()` using `merge()` */
|
|
97
|
+
export function createState<S extends object | unknown>(state: S): PatchableState<S> { return state as PatchableState<S>; }
|
|
98
|
+
|
|
99
|
+
/** type safe way to create a patch. useful for type inference and autocompletion. */
|
|
100
|
+
export function createPatch<S extends object | unknown>(p: DeepPartial<S> | Effect<S> | NoRenderPatch): Patch<S> { return p; }
|
|
101
|
+
|
|
91
102
|
/** type-safe way to create a vode. useful for type inference and autocompletion.
|
|
92
103
|
*
|
|
93
|
-
*
|
|
94
|
-
* -
|
|
95
|
-
* - tag and
|
|
96
|
-
* -
|
|
97
|
-
* - identity: `vode(["div", ["span", "bar"]]) // => ["div", ["span", "bar"]]`
|
|
104
|
+
* - just a tag: `vode("div")` => `["div"]` --*rendered*-> `<div></div>`
|
|
105
|
+
* - tag and props: `vode("div", { class: "foo" })` => `["div", { class: "foo" }]` --*rendered*-> `<div class="foo"></div>`
|
|
106
|
+
* - tag, props and children: `vode("div", { class: "foo" }, ["span", "bar"])` => `["div", { class: "foo" }, ["span", "bar"]]` --*rendered*-> `<div class="foo"><span>bar</span></div>`
|
|
107
|
+
* - identity: `vode(["div", ["span", "bar"]])` => `["div", ["span", "bar"]]` --*rendered*-> `<div><span>bar</span></div>`
|
|
98
108
|
*/
|
|
99
109
|
export function vode<S extends object | unknown>(tag: Tag | Vode<S>, props?: Props<S> | ChildVode<S>, ...children: ChildVode<S>[]): Vode<S> {
|
|
100
110
|
if (Array.isArray(tag)) {
|
|
@@ -106,22 +116,14 @@ export function vode<S extends object | unknown>(tag: Tag | Vode<S>, props?: Pro
|
|
|
106
116
|
return [tag, ...children];
|
|
107
117
|
}
|
|
108
118
|
|
|
109
|
-
/**
|
|
110
|
-
export function createState<S>(state: S): PatchableState<S> { return state as PatchableState<S>; }
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
/** for a type safe way to create a deeply partial patch object or effect */
|
|
114
|
-
export function patch<S>(p: DeepPartial<S> | Effect<S> | NoRenderPatch): typeof p { return p; }
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* create a vode app inside a container element
|
|
119
|
+
/** create a vode app inside a container element
|
|
118
120
|
* @param container will use this container as root and places the result of the dom function and further renderings in it
|
|
119
|
-
* @param initialState
|
|
121
|
+
* @param initialState
|
|
120
122
|
* @param dom creates the initial dom from the state and is called on every render
|
|
121
123
|
* @param initialPatches variadic list of patches that are applied after the first render
|
|
122
124
|
* @returns a patch function that can be used to update the state
|
|
123
125
|
*/
|
|
124
|
-
export function app<S>(container: HTMLElement, initialState: Omit<S, "patch">, dom: Component<S>, ...initialPatches: Patch<S>[]) {
|
|
126
|
+
export function app<S extends object | unknown>(container: HTMLElement, initialState: Omit<S, "patch">, dom: Component<S>, ...initialPatches: Patch<S>[]) {
|
|
125
127
|
const _vode = {} as ContainerNode<S>["_vode"];
|
|
126
128
|
_vode.stats = { renderTime: 0, renderCount: 0, queueLengthBeforeRender: 0, queueLengthAfterRender: 0, liveEffectCount: 0, patchCount: 0, renderPatchCount: 0 };
|
|
127
129
|
|
|
@@ -205,7 +207,7 @@ export function app<S>(container: HTMLElement, initialState: Omit<S, "patch">, d
|
|
|
205
207
|
_vode.patch = (<PatchableState<S>>initialState).patch;
|
|
206
208
|
_vode.state = <PatchableState<S>>initialState;
|
|
207
209
|
_vode.q = [];
|
|
208
|
-
|
|
210
|
+
|
|
209
211
|
const root = container as ContainerNode<S>;
|
|
210
212
|
root._vode = _vode;
|
|
211
213
|
|
|
@@ -220,8 +222,23 @@ export function app<S>(container: HTMLElement, initialState: Omit<S, "patch">, d
|
|
|
220
222
|
return _vode.patch;
|
|
221
223
|
}
|
|
222
224
|
|
|
223
|
-
/**
|
|
224
|
-
|
|
225
|
+
/** memoizes the resulting component or props by comparing element by element (===) with the
|
|
226
|
+
* `compare` of the previous render. otherwise skips the render step (not calling `componentOrProps`)*/
|
|
227
|
+
export function memo<S>(compare: any[], componentOrProps: Component<S> | ((s: S) => Props<S>)): typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S> {
|
|
228
|
+
(<any>componentOrProps).__memo = compare;
|
|
229
|
+
return componentOrProps as typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S>;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** html tag of the vode or `#text` if it is a text node */
|
|
233
|
+
export function tag<S>(v: Vode<S> | TextVode | NoVode | AttachedVode<S>): Tag | "#text" | undefined {
|
|
234
|
+
return !!v ? (Array.isArray(v)
|
|
235
|
+
? v[0] : (typeof v === "string" || (<any>v).nodeType === Node.TEXT_NODE)
|
|
236
|
+
? "#text" : undefined) as Tag
|
|
237
|
+
: undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** get properties object of a vode, if there is any */
|
|
241
|
+
export function props<S>(vode: ChildVode<S> | AttachedVode<S>): Props<S> | undefined {
|
|
225
242
|
if (Array.isArray(vode)
|
|
226
243
|
&& vode.length > 1
|
|
227
244
|
&& vode[1]
|
|
@@ -238,6 +255,7 @@ export function props<S extends object | unknown>(vode: ChildVode<S> | AttachedV
|
|
|
238
255
|
return undefined;
|
|
239
256
|
}
|
|
240
257
|
|
|
258
|
+
/** merge `ClassProp`s regardless of structure */
|
|
241
259
|
export function mergeClass(a: ClassProp, b: ClassProp): ClassProp {
|
|
242
260
|
if (!a) return b;
|
|
243
261
|
if (!b) return a;
|
|
@@ -288,48 +306,14 @@ export function mergeClass(a: ClassProp, b: ClassProp): ClassProp {
|
|
|
288
306
|
throw new Error(`cannot merge classes of ${a} (${typeof a}) and ${b} (${typeof b})`);
|
|
289
307
|
}
|
|
290
308
|
|
|
291
|
-
export function patchProps<S extends object | unknown>(vode: Vode<S>, props: Props<S>): void {
|
|
292
|
-
if (!Array.isArray(vode)) return;
|
|
293
|
-
|
|
294
|
-
if (vode.length > 1) {
|
|
295
|
-
if (!Array.isArray(vode[1]) && typeof vode[1] === "object") {
|
|
296
|
-
vode[1] = merge(vode[1], props);
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (childCount(vode) > 0) {
|
|
301
|
-
(<FullVode<S>>vode).push(null);
|
|
302
|
-
}
|
|
303
|
-
for (let i = vode.length - 1; i > 0; i--) {
|
|
304
|
-
if (i > 1) vode[i] = vode[i - 1];
|
|
305
|
-
}
|
|
306
|
-
vode[1] = props;
|
|
307
|
-
} else {
|
|
308
|
-
(<FullVode<S>>vode).push(props);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
309
|
/** get a slice of all children of a vode, if there are any */
|
|
313
|
-
export function children<S
|
|
310
|
+
export function children<S>(vode: ChildVode<S> | AttachedVode<S>): ChildVode<S>[] | null {
|
|
314
311
|
const start = childrenStart(vode);
|
|
315
312
|
if (start > 0) {
|
|
316
313
|
return (<Vode<S>>vode).slice(start) as Vode<S>[];
|
|
317
314
|
}
|
|
318
315
|
|
|
319
|
-
return
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/** index in vode at which child-vodes start */
|
|
323
|
-
export function childrenStart<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): number {
|
|
324
|
-
return props(vode) ? 2 : 1;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/** html tag of the vode or #text if it is a text node */
|
|
328
|
-
export function tag<S extends object | unknown>(v: Vode<S> | TextVode | NoVode | AttachedVode<S>): Tag | "#text" | undefined {
|
|
329
|
-
return !!v ? (Array.isArray(v)
|
|
330
|
-
? v[0] : (typeof v === "string" || (<any>v).nodeType === Node.TEXT_NODE)
|
|
331
|
-
? "#text" : undefined) as Tag
|
|
332
|
-
: undefined;
|
|
316
|
+
return null;
|
|
333
317
|
}
|
|
334
318
|
|
|
335
319
|
export function childCount<S>(vode: Vode<S>) { return vode.length - childrenStart(vode); }
|
|
@@ -338,11 +322,13 @@ export function child<S>(vode: Vode<S>, index: number): ChildVode<S> | undefined
|
|
|
338
322
|
return vode[index + childrenStart(vode)] as ChildVode<S>;
|
|
339
323
|
}
|
|
340
324
|
|
|
341
|
-
/**
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
325
|
+
/** index in vode at which child-vodes start */
|
|
326
|
+
export function childrenStart<S>(vode: ChildVode<S> | AttachedVode<S>): number {
|
|
327
|
+
return props(vode) ? 2 : 1;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** @returns multiple merged objects as one, applying from left to right ({}, first, ...p) */
|
|
331
|
+
export function merge(first?: object | unknown, ...p: (object | unknown)[]): object {
|
|
346
332
|
first = mergeState({}, first);
|
|
347
333
|
for (const pp of p) {
|
|
348
334
|
if (!pp) continue;
|
|
@@ -351,66 +337,40 @@ export function merge(first?: any, ...p: any[]): any {
|
|
|
351
337
|
return first!;
|
|
352
338
|
}
|
|
353
339
|
|
|
354
|
-
function
|
|
355
|
-
if (
|
|
356
|
-
return classProp;
|
|
357
|
-
} else if (Array.isArray(classProp)) {
|
|
358
|
-
return classProp.map(classString).join(" ");
|
|
359
|
-
} else if (typeof classProp === "object") {
|
|
360
|
-
return Object.keys(classProp!).filter(k => classProp![k]).join(" ");
|
|
361
|
-
} else {
|
|
362
|
-
return "";
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function isNaturalVode(x: ChildVode<any>) {
|
|
367
|
-
return Array.isArray(x) && x.length > 0 && typeof x[0] === "string";
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function isTextVode(x: ChildVode<any>) {
|
|
371
|
-
return typeof x === "string" || (<Text><unknown>x)?.nodeType === Node.TEXT_NODE;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function unwrap<S>(c: Component<S> | ChildVode<S>, s: S): ChildVode<S> {
|
|
375
|
-
if (typeof c === "function") {
|
|
376
|
-
return unwrap(c(s), s);
|
|
377
|
-
} else {
|
|
378
|
-
return c;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/** memoization of the given component or props (compare array is compared element by element (===) with the previous render) */
|
|
383
|
-
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> {
|
|
384
|
-
(<any>componentOrProps).__memo = compare;
|
|
385
|
-
return componentOrProps as typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S>;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function remember<S>(state: S, present: any, past: any): ChildVode<S> | AttachedVode<S> {
|
|
389
|
-
if (typeof present !== "function")
|
|
390
|
-
return present;
|
|
391
|
-
|
|
392
|
-
const presentMemo = present?.__memo;
|
|
393
|
-
const pastMemo = past?.__memo;
|
|
340
|
+
function mergeState(target: any, source: any) {
|
|
341
|
+
if (!source) return target;
|
|
394
342
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
&&
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
343
|
+
for (const key in source) {
|
|
344
|
+
const value = source[key];
|
|
345
|
+
if (value && typeof value === "object") {
|
|
346
|
+
const targetValue = target[key];
|
|
347
|
+
if (targetValue) {
|
|
348
|
+
if (Array.isArray(value)) {
|
|
349
|
+
target[key] = [...value];
|
|
350
|
+
} else if (value instanceof Date && targetValue !== value) {
|
|
351
|
+
target[key] = new Date(value);
|
|
352
|
+
} else {
|
|
353
|
+
if (Array.isArray(targetValue)) target[key] = mergeState({}, value);
|
|
354
|
+
else if (typeof targetValue === "object") mergeState(target[key], value);
|
|
355
|
+
else target[key] = mergeState({}, value);
|
|
356
|
+
}
|
|
357
|
+
} else if (Array.isArray(value)) {
|
|
358
|
+
target[key] = [...value];
|
|
359
|
+
} else if (value instanceof Date) {
|
|
360
|
+
target[key] = new Date(value);
|
|
361
|
+
} else {
|
|
362
|
+
target[key] = mergeState({}, value);
|
|
404
363
|
}
|
|
405
364
|
}
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
365
|
+
else if (value === undefined) {
|
|
366
|
+
delete target[key];
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
target[key] = value;
|
|
370
|
+
}
|
|
411
371
|
}
|
|
412
|
-
return
|
|
413
|
-
}
|
|
372
|
+
return target;
|
|
373
|
+
};
|
|
414
374
|
|
|
415
375
|
function render<S>(state: S, patch: Dispatch<S>, parent: ChildNode, childIndex: number, oldVode: AttachedVode<S> | undefined, newVode: ChildVode<S>, svg?: boolean): AttachedVode<S> | undefined {
|
|
416
376
|
// unwrap component if it is memoized
|
|
@@ -567,6 +527,49 @@ function render<S>(state: S, patch: Dispatch<S>, parent: ChildNode, childIndex:
|
|
|
567
527
|
return undefined;
|
|
568
528
|
}
|
|
569
529
|
|
|
530
|
+
function isNaturalVode(x: ChildVode<any>) {
|
|
531
|
+
return Array.isArray(x) && x.length > 0 && typeof x[0] === "string";
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function isTextVode(x: ChildVode<any>) {
|
|
535
|
+
return typeof x === "string" || (<Text><unknown>x)?.nodeType === Node.TEXT_NODE;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function remember<S>(state: S, present: any, past: any): ChildVode<S> | AttachedVode<S> {
|
|
539
|
+
if (typeof present !== "function")
|
|
540
|
+
return present;
|
|
541
|
+
|
|
542
|
+
const presentMemo = present?.__memo;
|
|
543
|
+
const pastMemo = past?.__memo;
|
|
544
|
+
|
|
545
|
+
if (Array.isArray(presentMemo)
|
|
546
|
+
&& Array.isArray(pastMemo)
|
|
547
|
+
&& presentMemo.length === pastMemo.length
|
|
548
|
+
) {
|
|
549
|
+
let same = true;
|
|
550
|
+
for (let i = 0; i < presentMemo.length; i++) {
|
|
551
|
+
if (presentMemo[i] !== pastMemo[i]) {
|
|
552
|
+
same = false;
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (same) return past;
|
|
557
|
+
}
|
|
558
|
+
const newRender = unwrap(present, state);
|
|
559
|
+
if (typeof newRender === "object") {
|
|
560
|
+
(<any>newRender).__memo = present?.__memo;
|
|
561
|
+
}
|
|
562
|
+
return newRender;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function unwrap<S>(c: Component<S> | ChildVode<S>, s: S): ChildVode<S> {
|
|
566
|
+
if (typeof c === "function") {
|
|
567
|
+
return unwrap(c(s), s);
|
|
568
|
+
} else {
|
|
569
|
+
return c;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
570
573
|
function patchProperties<S>(patch: Dispatch<S>, node: ChildNode, oldProps?: Props<S>, newProps?: Props<S>, isSvg?: boolean) {
|
|
571
574
|
if (!newProps && !oldProps) return;
|
|
572
575
|
if (!oldProps) { // set new props
|
|
@@ -664,37 +667,14 @@ function patchProperty<S>(patch: Dispatch<S>, node: ChildNode, key: string | key
|
|
|
664
667
|
return newValue;
|
|
665
668
|
}
|
|
666
669
|
|
|
667
|
-
function
|
|
668
|
-
if (
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
target[key] = [...value];
|
|
677
|
-
} else if (value instanceof Date && targetValue !== value) {
|
|
678
|
-
target[key] = new Date(value);
|
|
679
|
-
} else {
|
|
680
|
-
if (Array.isArray(targetValue)) target[key] = mergeState({}, value);
|
|
681
|
-
else if (typeof targetValue === "object") mergeState(target[key], value);
|
|
682
|
-
else target[key] = mergeState({}, value);
|
|
683
|
-
}
|
|
684
|
-
} else if (Array.isArray(value)) {
|
|
685
|
-
target[key] = [...value];
|
|
686
|
-
} else if (value instanceof Date) {
|
|
687
|
-
target[key] = new Date(value);
|
|
688
|
-
} else {
|
|
689
|
-
target[key] = mergeState({}, value);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
else if (value === undefined) {
|
|
693
|
-
delete target[key];
|
|
694
|
-
}
|
|
695
|
-
else {
|
|
696
|
-
target[key] = value;
|
|
697
|
-
}
|
|
670
|
+
function classString(classProp: ClassProp): string {
|
|
671
|
+
if (typeof classProp === "string") {
|
|
672
|
+
return classProp;
|
|
673
|
+
} else if (Array.isArray(classProp)) {
|
|
674
|
+
return classProp.map(classString).join(" ");
|
|
675
|
+
} else if (typeof classProp === "object") {
|
|
676
|
+
return Object.keys(classProp!).filter(k => classProp![k]).join(" ");
|
|
677
|
+
} else {
|
|
678
|
+
return "";
|
|
698
679
|
}
|
|
699
|
-
|
|
700
|
-
};
|
|
680
|
+
}
|
package/vode.mjs
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// src/vode.ts
|
|
2
2
|
var EmptyPatch = {};
|
|
3
|
+
function createState(state) {
|
|
4
|
+
return state;
|
|
5
|
+
}
|
|
6
|
+
function createPatch(p) {
|
|
7
|
+
return p;
|
|
8
|
+
}
|
|
3
9
|
function vode(tag, props, ...children) {
|
|
4
10
|
if (Array.isArray(tag)) {
|
|
5
11
|
return tag;
|
|
@@ -9,12 +15,6 @@ function vode(tag, props, ...children) {
|
|
|
9
15
|
}
|
|
10
16
|
return [tag, ...children];
|
|
11
17
|
}
|
|
12
|
-
function createState(state) {
|
|
13
|
-
return state;
|
|
14
|
-
}
|
|
15
|
-
function patch(p) {
|
|
16
|
-
return p;
|
|
17
|
-
}
|
|
18
18
|
function app(container, initialState, dom, ...initialPatches) {
|
|
19
19
|
const _vode = {};
|
|
20
20
|
_vode.stats = { renderTime: 0, renderCount: 0, queueLengthBeforeRender: 0, queueLengthAfterRender: 0, liveEffectCount: 0, patchCount: 0, renderPatchCount: 0 };
|
|
@@ -83,10 +83,10 @@ function app(container, initialState, dom, ...initialPatches) {
|
|
|
83
83
|
try {
|
|
84
84
|
_vode.stats.queueLengthBeforeRender = _vode.q.length;
|
|
85
85
|
while (_vode.q.length > 0) {
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
86
|
+
const patch = _vode.q.shift();
|
|
87
|
+
if (patch === EmptyPatch)
|
|
88
88
|
continue;
|
|
89
|
-
mergeState(_vode.state,
|
|
89
|
+
mergeState(_vode.state, patch);
|
|
90
90
|
}
|
|
91
91
|
_vode.vode = render(_vode.state, _vode.patch, container, 0, _vode.vode, dom(_vode.state));
|
|
92
92
|
} finally {
|
|
@@ -113,6 +113,13 @@ function app(container, initialState, dom, ...initialPatches) {
|
|
|
113
113
|
}
|
|
114
114
|
return _vode.patch;
|
|
115
115
|
}
|
|
116
|
+
function memo(compare, componentOrProps) {
|
|
117
|
+
componentOrProps.__memo = compare;
|
|
118
|
+
return componentOrProps;
|
|
119
|
+
}
|
|
120
|
+
function tag(v) {
|
|
121
|
+
return v ? Array.isArray(v) ? v[0] : typeof v === "string" || v.nodeType === Node.TEXT_NODE ? "#text" : undefined : undefined;
|
|
122
|
+
}
|
|
116
123
|
function props(vode2) {
|
|
117
124
|
if (Array.isArray(vode2) && vode2.length > 1 && vode2[1] && !Array.isArray(vode2[1])) {
|
|
118
125
|
if (typeof vode2[1] === "object" && vode2[1].nodeType !== Node.TEXT_NODE) {
|
|
@@ -164,38 +171,12 @@ function mergeClass(a, b) {
|
|
|
164
171
|
}
|
|
165
172
|
throw new Error(`cannot merge classes of ${a} (${typeof a}) and ${b} (${typeof b})`);
|
|
166
173
|
}
|
|
167
|
-
function patchProps(vode2, props2) {
|
|
168
|
-
if (!Array.isArray(vode2))
|
|
169
|
-
return;
|
|
170
|
-
if (vode2.length > 1) {
|
|
171
|
-
if (!Array.isArray(vode2[1]) && typeof vode2[1] === "object") {
|
|
172
|
-
vode2[1] = merge(vode2[1], props2);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
if (childCount(vode2) > 0) {
|
|
176
|
-
vode2.push(null);
|
|
177
|
-
}
|
|
178
|
-
for (let i = vode2.length - 1;i > 0; i--) {
|
|
179
|
-
if (i > 1)
|
|
180
|
-
vode2[i] = vode2[i - 1];
|
|
181
|
-
}
|
|
182
|
-
vode2[1] = props2;
|
|
183
|
-
} else {
|
|
184
|
-
vode2.push(props2);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
174
|
function children(vode2) {
|
|
188
175
|
const start = childrenStart(vode2);
|
|
189
176
|
if (start > 0) {
|
|
190
177
|
return vode2.slice(start);
|
|
191
178
|
}
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
function childrenStart(vode2) {
|
|
195
|
-
return props(vode2) ? 2 : 1;
|
|
196
|
-
}
|
|
197
|
-
function tag(v) {
|
|
198
|
-
return v ? Array.isArray(v) ? v[0] : typeof v === "string" || v.nodeType === Node.TEXT_NODE ? "#text" : undefined : undefined;
|
|
179
|
+
return null;
|
|
199
180
|
}
|
|
200
181
|
function childCount(vode2) {
|
|
201
182
|
return vode2.length - childrenStart(vode2);
|
|
@@ -203,6 +184,9 @@ function childCount(vode2) {
|
|
|
203
184
|
function child(vode2, index) {
|
|
204
185
|
return vode2[index + childrenStart(vode2)];
|
|
205
186
|
}
|
|
187
|
+
function childrenStart(vode2) {
|
|
188
|
+
return props(vode2) ? 2 : 1;
|
|
189
|
+
}
|
|
206
190
|
function merge(first, ...p) {
|
|
207
191
|
first = mergeState({}, first);
|
|
208
192
|
for (const pp of p) {
|
|
@@ -212,57 +196,42 @@ function merge(first, ...p) {
|
|
|
212
196
|
}
|
|
213
197
|
return first;
|
|
214
198
|
}
|
|
215
|
-
function
|
|
216
|
-
if (
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return componentOrProps;
|
|
242
|
-
}
|
|
243
|
-
function remember(state, present, past) {
|
|
244
|
-
if (typeof present !== "function")
|
|
245
|
-
return present;
|
|
246
|
-
const presentMemo = present?.__memo;
|
|
247
|
-
const pastMemo = past?.__memo;
|
|
248
|
-
if (Array.isArray(presentMemo) && Array.isArray(pastMemo) && presentMemo.length === pastMemo.length) {
|
|
249
|
-
let same = true;
|
|
250
|
-
for (let i = 0;i < presentMemo.length; i++) {
|
|
251
|
-
if (presentMemo[i] !== pastMemo[i]) {
|
|
252
|
-
same = false;
|
|
253
|
-
break;
|
|
199
|
+
function mergeState(target, source) {
|
|
200
|
+
if (!source)
|
|
201
|
+
return target;
|
|
202
|
+
for (const key in source) {
|
|
203
|
+
const value = source[key];
|
|
204
|
+
if (value && typeof value === "object") {
|
|
205
|
+
const targetValue = target[key];
|
|
206
|
+
if (targetValue) {
|
|
207
|
+
if (Array.isArray(value)) {
|
|
208
|
+
target[key] = [...value];
|
|
209
|
+
} else if (value instanceof Date && targetValue !== value) {
|
|
210
|
+
target[key] = new Date(value);
|
|
211
|
+
} else {
|
|
212
|
+
if (Array.isArray(targetValue))
|
|
213
|
+
target[key] = mergeState({}, value);
|
|
214
|
+
else if (typeof targetValue === "object")
|
|
215
|
+
mergeState(target[key], value);
|
|
216
|
+
else
|
|
217
|
+
target[key] = mergeState({}, value);
|
|
218
|
+
}
|
|
219
|
+
} else if (Array.isArray(value)) {
|
|
220
|
+
target[key] = [...value];
|
|
221
|
+
} else if (value instanceof Date) {
|
|
222
|
+
target[key] = new Date(value);
|
|
223
|
+
} else {
|
|
224
|
+
target[key] = mergeState({}, value);
|
|
254
225
|
}
|
|
226
|
+
} else if (value === undefined) {
|
|
227
|
+
delete target[key];
|
|
228
|
+
} else {
|
|
229
|
+
target[key] = value;
|
|
255
230
|
}
|
|
256
|
-
if (same)
|
|
257
|
-
return past;
|
|
258
|
-
}
|
|
259
|
-
const newRender = unwrap(present, state);
|
|
260
|
-
if (typeof newRender === "object") {
|
|
261
|
-
newRender.__memo = present?.__memo;
|
|
262
231
|
}
|
|
263
|
-
return
|
|
232
|
+
return target;
|
|
264
233
|
}
|
|
265
|
-
function render(state,
|
|
234
|
+
function render(state, patch, parent, childIndex, oldVode, newVode, svg) {
|
|
266
235
|
newVode = remember(state, newVode, oldVode);
|
|
267
236
|
const isNoVode = !newVode || typeof newVode === "number" || typeof newVode === "boolean";
|
|
268
237
|
if (newVode === oldVode || !oldVode && isNoVode) {
|
|
@@ -271,7 +240,7 @@ function render(state, patch2, parent, childIndex, oldVode, newVode, svg) {
|
|
|
271
240
|
const oldIsText = oldVode?.nodeType === Node.TEXT_NODE;
|
|
272
241
|
const oldNode = oldIsText ? oldVode : oldVode?.node;
|
|
273
242
|
if (isNoVode) {
|
|
274
|
-
oldNode?.onUnmount &&
|
|
243
|
+
oldNode?.onUnmount && patch(oldNode.onUnmount(oldNode));
|
|
275
244
|
oldNode?.remove();
|
|
276
245
|
return;
|
|
277
246
|
}
|
|
@@ -294,7 +263,7 @@ function render(state, patch2, parent, childIndex, oldVode, newVode, svg) {
|
|
|
294
263
|
if (isText && (!oldNode || !oldIsText)) {
|
|
295
264
|
const text = document.createTextNode(newVode);
|
|
296
265
|
if (oldNode) {
|
|
297
|
-
oldNode.onUnmount &&
|
|
266
|
+
oldNode.onUnmount && patch(oldNode.onUnmount(oldNode));
|
|
298
267
|
oldNode.replaceWith(text);
|
|
299
268
|
} else {
|
|
300
269
|
if (parent.childNodes[childIndex]) {
|
|
@@ -314,9 +283,9 @@ function render(state, patch2, parent, childIndex, oldVode, newVode, svg) {
|
|
|
314
283
|
newvode[1] = remember(state, newvode[1], undefined);
|
|
315
284
|
}
|
|
316
285
|
const properties = props(newVode);
|
|
317
|
-
patchProperties(
|
|
286
|
+
patchProperties(patch, newNode, undefined, properties, svg);
|
|
318
287
|
if (oldNode) {
|
|
319
|
-
oldNode.onUnmount &&
|
|
288
|
+
oldNode.onUnmount && patch(oldNode.onUnmount(oldNode));
|
|
320
289
|
oldNode.replaceWith(newNode);
|
|
321
290
|
} else {
|
|
322
291
|
if (parent.childNodes[childIndex]) {
|
|
@@ -329,11 +298,11 @@ function render(state, patch2, parent, childIndex, oldVode, newVode, svg) {
|
|
|
329
298
|
if (newChildren) {
|
|
330
299
|
for (let i = 0;i < newChildren.length; i++) {
|
|
331
300
|
const child2 = newChildren[i];
|
|
332
|
-
const attached = render(state,
|
|
301
|
+
const attached = render(state, patch, newNode, i, undefined, child2, svg);
|
|
333
302
|
newVode[properties ? i + 2 : i + 1] = attached;
|
|
334
303
|
}
|
|
335
304
|
}
|
|
336
|
-
newNode.onMount &&
|
|
305
|
+
newNode.onMount && patch(newNode.onMount(newNode));
|
|
337
306
|
return newVode;
|
|
338
307
|
}
|
|
339
308
|
if (!oldIsText && isNode && oldVode[0] === newVode[0]) {
|
|
@@ -347,12 +316,12 @@ function render(state, patch2, parent, childIndex, oldVode, newVode, svg) {
|
|
|
347
316
|
newvode[1] = remember(state, newvode[1], oldvode[1]);
|
|
348
317
|
if (prev !== newvode[1]) {
|
|
349
318
|
const properties = props(newVode);
|
|
350
|
-
patchProperties(
|
|
319
|
+
patchProperties(patch, oldNode, props(oldVode), properties, svg);
|
|
351
320
|
hasProps = !!properties;
|
|
352
321
|
}
|
|
353
322
|
} else {
|
|
354
323
|
const properties = props(newVode);
|
|
355
|
-
patchProperties(
|
|
324
|
+
patchProperties(patch, oldNode, props(oldVode), properties, svg);
|
|
356
325
|
hasProps = !!properties;
|
|
357
326
|
}
|
|
358
327
|
const newKids = children(newVode);
|
|
@@ -361,7 +330,7 @@ function render(state, patch2, parent, childIndex, oldVode, newVode, svg) {
|
|
|
361
330
|
for (let i = 0;i < newKids.length; i++) {
|
|
362
331
|
const child2 = newKids[i];
|
|
363
332
|
const oldChild = oldKids && oldKids[i];
|
|
364
|
-
const attached = render(state,
|
|
333
|
+
const attached = render(state, patch, oldNode, i, oldChild, child2, svg);
|
|
365
334
|
if (attached) {
|
|
366
335
|
newVode[hasProps ? i + 2 : i + 1] = attached;
|
|
367
336
|
}
|
|
@@ -383,13 +352,48 @@ function render(state, patch2, parent, childIndex, oldVode, newVode, svg) {
|
|
|
383
352
|
}
|
|
384
353
|
return;
|
|
385
354
|
}
|
|
386
|
-
function
|
|
355
|
+
function isNaturalVode(x) {
|
|
356
|
+
return Array.isArray(x) && x.length > 0 && typeof x[0] === "string";
|
|
357
|
+
}
|
|
358
|
+
function isTextVode(x) {
|
|
359
|
+
return typeof x === "string" || x?.nodeType === Node.TEXT_NODE;
|
|
360
|
+
}
|
|
361
|
+
function remember(state, present, past) {
|
|
362
|
+
if (typeof present !== "function")
|
|
363
|
+
return present;
|
|
364
|
+
const presentMemo = present?.__memo;
|
|
365
|
+
const pastMemo = past?.__memo;
|
|
366
|
+
if (Array.isArray(presentMemo) && Array.isArray(pastMemo) && presentMemo.length === pastMemo.length) {
|
|
367
|
+
let same = true;
|
|
368
|
+
for (let i = 0;i < presentMemo.length; i++) {
|
|
369
|
+
if (presentMemo[i] !== pastMemo[i]) {
|
|
370
|
+
same = false;
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (same)
|
|
375
|
+
return past;
|
|
376
|
+
}
|
|
377
|
+
const newRender = unwrap(present, state);
|
|
378
|
+
if (typeof newRender === "object") {
|
|
379
|
+
newRender.__memo = present?.__memo;
|
|
380
|
+
}
|
|
381
|
+
return newRender;
|
|
382
|
+
}
|
|
383
|
+
function unwrap(c, s) {
|
|
384
|
+
if (typeof c === "function") {
|
|
385
|
+
return unwrap(c(s), s);
|
|
386
|
+
} else {
|
|
387
|
+
return c;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function patchProperties(patch, node, oldProps, newProps, isSvg) {
|
|
387
391
|
if (!newProps && !oldProps)
|
|
388
392
|
return;
|
|
389
393
|
if (!oldProps) {
|
|
390
394
|
for (const key in newProps) {
|
|
391
395
|
const newValue = newProps[key];
|
|
392
|
-
newProps[key] = patchProperty(
|
|
396
|
+
newProps[key] = patchProperty(patch, node, key, undefined, newValue, isSvg);
|
|
393
397
|
}
|
|
394
398
|
} else if (newProps) {
|
|
395
399
|
const combinedKeys = new Set([...Object.keys(oldProps), ...Object.keys(newProps)]);
|
|
@@ -399,21 +403,21 @@ function patchProperties(patch2, node, oldProps, newProps, isSvg) {
|
|
|
399
403
|
if (key[0] === "o" && key[1] === "n") {
|
|
400
404
|
const oldEvent = node["__" + key];
|
|
401
405
|
if (oldEvent && oldEvent !== newValue || !oldEvent && oldValue !== newValue) {
|
|
402
|
-
newProps[key] = patchProperty(
|
|
406
|
+
newProps[key] = patchProperty(patch, node, key, oldValue, newValue, isSvg);
|
|
403
407
|
}
|
|
404
408
|
node["__" + key] = newValue;
|
|
405
409
|
} else if (oldValue !== newValue) {
|
|
406
|
-
newProps[key] = patchProperty(
|
|
410
|
+
newProps[key] = patchProperty(patch, node, key, oldValue, newValue, isSvg);
|
|
407
411
|
}
|
|
408
412
|
}
|
|
409
413
|
} else {
|
|
410
414
|
for (const key in oldProps) {
|
|
411
415
|
const oldValue = oldProps[key];
|
|
412
|
-
oldProps[key] = patchProperty(
|
|
416
|
+
oldProps[key] = patchProperty(patch, node, key, oldValue, undefined, isSvg);
|
|
413
417
|
}
|
|
414
418
|
}
|
|
415
419
|
}
|
|
416
|
-
function patchProperty(
|
|
420
|
+
function patchProperty(patch, node, key, oldValue, newValue, isSvg) {
|
|
417
421
|
if (key === "style") {
|
|
418
422
|
if (!newValue) {
|
|
419
423
|
node.style.cssText = "";
|
|
@@ -451,17 +455,17 @@ function patchProperty(patch2, node, key, oldValue, newValue, isSvg) {
|
|
|
451
455
|
let eventHandler = null;
|
|
452
456
|
if (typeof newValue === "function") {
|
|
453
457
|
const action = newValue;
|
|
454
|
-
eventHandler = (evt) =>
|
|
458
|
+
eventHandler = (evt) => patch([action, evt]);
|
|
455
459
|
} else if (Array.isArray(newValue)) {
|
|
456
460
|
const arr = newValue;
|
|
457
461
|
const action = newValue[0];
|
|
458
462
|
if (arr.length > 1) {
|
|
459
|
-
eventHandler = () =>
|
|
463
|
+
eventHandler = () => patch([action, ...arr.slice(1)]);
|
|
460
464
|
} else {
|
|
461
|
-
eventHandler = (evt) =>
|
|
465
|
+
eventHandler = (evt) => patch([action, evt]);
|
|
462
466
|
}
|
|
463
467
|
} else if (typeof newValue === "object") {
|
|
464
|
-
eventHandler = () =>
|
|
468
|
+
eventHandler = () => patch(newValue);
|
|
465
469
|
}
|
|
466
470
|
node[key] = eventHandler;
|
|
467
471
|
} else {
|
|
@@ -474,40 +478,16 @@ function patchProperty(patch2, node, key, oldValue, newValue, isSvg) {
|
|
|
474
478
|
}
|
|
475
479
|
return newValue;
|
|
476
480
|
}
|
|
477
|
-
function
|
|
478
|
-
if (
|
|
479
|
-
return
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
target[key] = [...value];
|
|
487
|
-
} else if (value instanceof Date && targetValue !== value) {
|
|
488
|
-
target[key] = new Date(value);
|
|
489
|
-
} else {
|
|
490
|
-
if (Array.isArray(targetValue))
|
|
491
|
-
target[key] = mergeState({}, value);
|
|
492
|
-
else if (typeof targetValue === "object")
|
|
493
|
-
mergeState(target[key], value);
|
|
494
|
-
else
|
|
495
|
-
target[key] = mergeState({}, value);
|
|
496
|
-
}
|
|
497
|
-
} else if (Array.isArray(value)) {
|
|
498
|
-
target[key] = [...value];
|
|
499
|
-
} else if (value instanceof Date) {
|
|
500
|
-
target[key] = new Date(value);
|
|
501
|
-
} else {
|
|
502
|
-
target[key] = mergeState({}, value);
|
|
503
|
-
}
|
|
504
|
-
} else if (value === undefined) {
|
|
505
|
-
delete target[key];
|
|
506
|
-
} else {
|
|
507
|
-
target[key] = value;
|
|
508
|
-
}
|
|
481
|
+
function classString(classProp) {
|
|
482
|
+
if (typeof classProp === "string") {
|
|
483
|
+
return classProp;
|
|
484
|
+
} else if (Array.isArray(classProp)) {
|
|
485
|
+
return classProp.map(classString).join(" ");
|
|
486
|
+
} else if (typeof classProp === "object") {
|
|
487
|
+
return Object.keys(classProp).filter((k) => classProp[k]).join(" ");
|
|
488
|
+
} else {
|
|
489
|
+
return "";
|
|
509
490
|
}
|
|
510
|
-
return target;
|
|
511
491
|
}
|
|
512
492
|
// src/vode-tags.ts
|
|
513
493
|
var A = "a";
|
|
@@ -747,13 +727,12 @@ export {
|
|
|
747
727
|
vode,
|
|
748
728
|
tag,
|
|
749
729
|
props,
|
|
750
|
-
patchProps,
|
|
751
|
-
patch,
|
|
752
730
|
mergeClass,
|
|
753
731
|
merge,
|
|
754
732
|
memo,
|
|
755
733
|
htmlToVode,
|
|
756
734
|
createState,
|
|
735
|
+
createPatch,
|
|
757
736
|
childrenStart,
|
|
758
737
|
children,
|
|
759
738
|
childCount,
|