@ryupold/vode 0.10.0 → 0.12.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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/vode.ts +159 -185
  3. package/vode.mjs +130 -152
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryupold/vode",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Small web framework for minimal websites",
5
5
  "author": "Michael Scherbakow (ryupold)",
6
6
  "license": "MIT",
package/src/vode.ts CHANGED
@@ -11,10 +11,9 @@ export type Component<S> = (s: S) => ChildVode<S>;
11
11
 
12
12
  export type Patch<S> =
13
13
  | NoRenderPatch // ignored
14
- | typeof EmptyPatch | DeepPartial<S> // render patches
15
- | Promise<Patch<S>> | Effect<S> // effects resulting in patches
14
+ | {} | DeepPartial<S> // render patches
15
+ | Promise<Patch<S>> | Effect<S>; // effects resulting in patches
16
16
 
17
- export const EmptyPatch = {} as const; // smallest patch to cause a render without any changes
18
17
  export type NoRenderPatch = undefined | null | number | boolean | bigint | string | symbol | void;
19
18
 
20
19
  export type DeepPartial<S> = { [P in keyof S]?: S[P] extends Array<infer I> ? Array<Patch<I>> : Patch<S[P]> };
@@ -28,9 +27,6 @@ export type Effect<S> =
28
27
 
29
28
  export type EffectFunction<S> = (state: S, ...args: any[]) => Patch<S>;
30
29
 
31
- export type Dispatch<S> = (action: Patch<S>) => void;
32
- export type PatchableState<S> = S & { patch: Dispatch<Patch<S>> };
33
-
34
30
  export type Props<S> = Partial<
35
31
  Omit<HTMLElement,
36
32
  keyof (DocumentFragment & ElementCSSInlineStyle & GlobalEventHandlers)> &
@@ -48,34 +44,42 @@ export type Props<S> = Partial<
48
44
  export type MountFunction<S> =
49
45
  | ((s: S, node: HTMLElement) => Patch<S>)
50
46
  | ((s: S, node: SVGSVGElement) => Patch<S>)
51
- | ((s: S, node: MathMLElement) => Patch<S>)
47
+ | ((s: S, node: MathMLElement) => Patch<S>);
52
48
 
53
49
  export type ClassProp =
54
50
  | "" | false | null | undefined // no class
55
51
  | string // "class1 class2"
56
52
  | string[] // ["class1", "class2"]
57
- | Record<string, boolean | undefined | null> // { class1: true, class2: false }
53
+ | Record<string, boolean | undefined | null>; // { class1: true, class2: false }
58
54
 
59
55
  export type StyleProp = Record<number, never> & {
60
56
  [K in keyof CSSStyleDeclaration]?: CSSStyleDeclaration[K] | null
61
- }
57
+ };
62
58
 
63
59
  export type EventsMap =
64
60
  & { [K in keyof HTMLElementEventMap as `on${K}`]: HTMLElementEventMap[K] }
65
61
  & { [K in keyof WindowEventMap as `on${K}`]: WindowEventMap[K] }
66
62
  & { [K in keyof SVGElementEventMap as `on${K}`]: SVGElementEventMap[K] }
67
- & { onsearch: Event }
63
+ & { onsearch: Event };
68
64
 
69
65
  export type PropertyValue<S> = string | boolean | null | undefined | StyleProp | ClassProp | Patch<S> | void;
70
66
 
67
+
68
+ export type Dispatch<S> = (action: Patch<S>) => void;
69
+ export type PatchableState<S> = S & { patch: Dispatch<Patch<S>> };
70
+
71
71
  export type ContainerNode<S> = HTMLElement & {
72
+ /** the `_vode` property is added to the container in `app()`.
73
+ * it contains all necessary stuff for the vode app to function.
74
+ * delete it to clear all resources of the vode app, or remove the container itself */
72
75
  _vode: {
73
- state: PatchableState<S>,
74
- vode: AttachedVode<S>,
75
- patch: Dispatch<S>,
76
- render: () => void,
77
- q: Patch<S>[]
78
- isRendering: boolean,
76
+ state: PatchableState<S>, // can touch this, but let it be an object
77
+ vode: AttachedVode<S>, //don't touch this
78
+ patch: Dispatch<S>, // can't touch this
79
+ render: () => void, // can't touch this
80
+ q: object | null, // next patch aggregate to be applied
81
+ isRendering: boolean, // under no circumstances touch this
82
+ /** stats about the overall patches & last render time */
79
83
  stats: {
80
84
  patchCount: number,
81
85
  liveEffectCount: number,
@@ -88,40 +92,38 @@ export type ContainerNode<S> = HTMLElement & {
88
92
  }
89
93
  };
90
94
 
95
+ /** create a state object used as initial state for `app()`. it is updated with `PatchableState.patch()` using `merge()` */
96
+ export function createState<S extends object | unknown>(state: S): PatchableState<S> { return state as PatchableState<S>; }
97
+
98
+ /** type safe way to create a patch. useful for type inference and autocompletion. */
99
+ export function createPatch<S extends object | unknown>(p: DeepPartial<S> | Effect<S> | NoRenderPatch): typeof p { return p; }
100
+
91
101
  /** type-safe way to create a vode. useful for type inference and autocompletion.
92
102
  *
93
- * overloads:
94
- * - just a tag: `vode("div") // => ["div"]`
95
- * - tag and props: `vode("div", { class: "foo" }) // => ["div", { class: "foo" }]`
96
- * - tag, props and children: `vode("div", { class: "foo" }, ["span", "bar"]) // => ["div", { class: "foo" }, ["span", "bar"]]`
97
- * - identity: `vode(["div", ["span", "bar"]]) // => ["div", ["span", "bar"]]`
103
+ * - just a tag: `vode("div")` => `["div"]` --*rendered*-> `<div></div>`
104
+ * - tag and props: `vode("div", { class: "foo" })` => `["div", { class: "foo" }]` --*rendered*-> `<div class="foo"></div>`
105
+ * - tag, props and children: `vode("div", { class: "foo" }, ["span", "bar"])` => `["div", { class: "foo" }, ["span", "bar"]]` --*rendered*-> `<div class="foo"><span>bar</span></div>`
106
+ * - identity: `vode(["div", ["span", "bar"]])` => `["div", ["span", "bar"]]` --*rendered*-> `<div><span>bar</span></div>`
98
107
  */
99
108
  export function vode<S extends object | unknown>(tag: Tag | Vode<S>, props?: Props<S> | ChildVode<S>, ...children: ChildVode<S>[]): Vode<S> {
100
- if (Array.isArray(tag)) {
101
- return tag;
102
- }
103
- if (props) {
104
- return [tag, props as Props<S>, ...children];
105
- }
106
- return [tag, ...children];
109
+ if(!tag) throw new Error("tag must be a string or vode");
110
+ if (Array.isArray(tag)) return tag;
111
+ else if (props) return [tag, props as Props<S>, ...children];
112
+ else return [tag, ...children];
107
113
  }
108
114
 
109
- /** pass an object whose type determines the initial state */
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
115
+ /** create a vode app inside a container element
118
116
  * @param container will use this container as root and places the result of the dom function and further renderings in it
119
- * @param initialState @see createState
117
+ * @param initialState
120
118
  * @param dom creates the initial dom from the state and is called on every render
121
119
  * @param initialPatches variadic list of patches that are applied after the first render
122
120
  * @returns a patch function that can be used to update the state
123
121
  */
124
- export function app<S>(container: HTMLElement, initialState: Omit<S, "patch">, dom: Component<S>, ...initialPatches: Patch<S>[]) {
122
+ export function app<S extends object | unknown>(container: HTMLElement, initialState: Omit<S, "patch">, dom: Component<S>, ...initialPatches: Patch<S>[]) {
123
+ if (!container) throw new Error("container must be a valid HTMLElement");
124
+ if (!initialState || typeof initialState !== "object") throw new Error("initialState must be an object");
125
+ if (typeof dom !== "function") throw new Error("dom must be a function that returns a vode");
126
+
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
 
@@ -169,7 +171,7 @@ export function app<S>(container: HTMLElement, initialState: Omit<S, "patch">, d
169
171
  _vode.patch!((<EffectFunction<S>>action)(_vode.state));
170
172
  } else {
171
173
  _vode.stats.renderPatchCount++;
172
- _vode.q!.push(<Patch<S>>action);
174
+ _vode.q = mergeState(_vode.q || {}, action);
173
175
  if (!_vode.isRendering) _vode.render!();
174
176
  }
175
177
  }
@@ -178,25 +180,19 @@ export function app<S>(container: HTMLElement, initialState: Omit<S, "patch">, d
178
180
  Object.defineProperty(_vode, "render", {
179
181
  enumerable: false, configurable: true,
180
182
  writable: false, value: () => requestAnimationFrame(() => {
181
- if (_vode.isRendering || _vode.q!.length === 0) return;
183
+ if (_vode.isRendering || !_vode.q) return;
182
184
  _vode.isRendering = true;
183
185
  const sw = Date.now();
184
186
  try {
185
- _vode.stats.queueLengthBeforeRender = _vode.q!.length;
186
-
187
- while (_vode.q!.length > 0) {
188
- const patch = _vode.q!.shift();
189
- if (patch === EmptyPatch) continue;
190
- mergeState(_vode.state, patch);
191
- }
187
+ _vode.state = mergeState(_vode.state, _vode.q);
188
+ _vode.q = null;
192
189
  _vode.vode = render(_vode.state, _vode.patch, container, 0, _vode.vode, dom(_vode.state))!;
193
190
  } finally {
194
191
  _vode.isRendering = false;
195
192
  _vode.stats.renderCount++;
196
193
  _vode.stats.renderTime = Date.now() - sw;
197
- _vode.stats.queueLengthAfterRender = _vode.q!.length;
198
- if (_vode.q!.length > 0) {
199
- _vode.render!();
194
+ if (_vode.q) {
195
+ _vode.render();
200
196
  }
201
197
  }
202
198
  })
@@ -204,8 +200,8 @@ export function app<S>(container: HTMLElement, initialState: Omit<S, "patch">, d
204
200
 
205
201
  _vode.patch = (<PatchableState<S>>initialState).patch;
206
202
  _vode.state = <PatchableState<S>>initialState;
207
- _vode.q = [];
208
-
203
+ _vode.q = null;
204
+
209
205
  const root = container as ContainerNode<S>;
210
206
  root._vode = _vode;
211
207
 
@@ -220,8 +216,23 @@ export function app<S>(container: HTMLElement, initialState: Omit<S, "patch">, d
220
216
  return _vode.patch;
221
217
  }
222
218
 
223
- /** get properties of a vode, if there are any */
224
- export function props<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): Props<S> | undefined {
219
+ /** memoizes the resulting component or props by comparing element by element (===) with the
220
+ * `compare` of the previous render. otherwise skips the render step (not calling `componentOrProps`)*/
221
+ 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> {
222
+ (<any>componentOrProps).__memo = compare;
223
+ return componentOrProps as typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S>;
224
+ }
225
+
226
+ /** html tag of the vode or `#text` if it is a text node */
227
+ export function tag<S>(v: Vode<S> | TextVode | NoVode | AttachedVode<S>): Tag | "#text" | undefined {
228
+ return !!v ? (Array.isArray(v)
229
+ ? v[0] : (typeof v === "string" || (<any>v).nodeType === Node.TEXT_NODE)
230
+ ? "#text" : undefined) as Tag
231
+ : undefined;
232
+ }
233
+
234
+ /** get properties object of a vode, if there is any */
235
+ export function props<S>(vode: ChildVode<S> | AttachedVode<S>): Props<S> | undefined {
225
236
  if (Array.isArray(vode)
226
237
  && vode.length > 1
227
238
  && vode[1]
@@ -238,6 +249,7 @@ export function props<S extends object | unknown>(vode: ChildVode<S> | AttachedV
238
249
  return undefined;
239
250
  }
240
251
 
252
+ /** merge `ClassProp`s regardless of structure */
241
253
  export function mergeClass(a: ClassProp, b: ClassProp): ClassProp {
242
254
  if (!a) return b;
243
255
  if (!b) return a;
@@ -288,48 +300,14 @@ export function mergeClass(a: ClassProp, b: ClassProp): ClassProp {
288
300
  throw new Error(`cannot merge classes of ${a} (${typeof a}) and ${b} (${typeof b})`);
289
301
  }
290
302
 
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
303
  /** get a slice of all children of a vode, if there are any */
313
- export function children<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): ChildVode<S>[] | undefined {
304
+ export function children<S>(vode: ChildVode<S> | AttachedVode<S>): ChildVode<S>[] | null {
314
305
  const start = childrenStart(vode);
315
306
  if (start > 0) {
316
307
  return (<Vode<S>>vode).slice(start) as Vode<S>[];
317
308
  }
318
309
 
319
- return undefined;
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;
310
+ return null;
333
311
  }
334
312
 
335
313
  export function childCount<S>(vode: Vode<S>) { return vode.length - childrenStart(vode); }
@@ -338,11 +316,13 @@ export function child<S>(vode: Vode<S>, index: number): ChildVode<S> | undefined
338
316
  return vode[index + childrenStart(vode)] as ChildVode<S>;
339
317
  }
340
318
 
341
- /** merge multiple objects into one, applying from left to right
342
- * @param first object to merge
343
- * @returns merged object
344
- */
345
- export function merge(first?: any, ...p: any[]): any {
319
+ /** index in vode at which child-vodes start */
320
+ export function childrenStart<S>(vode: ChildVode<S> | AttachedVode<S>): number {
321
+ return props(vode) ? 2 : 1;
322
+ }
323
+
324
+ /** @returns multiple merged objects as one, applying from left to right ({}, first, ...p) */
325
+ export function merge(first?: object | unknown, ...p: (object | unknown)[]): object {
346
326
  first = mergeState({}, first);
347
327
  for (const pp of p) {
348
328
  if (!pp) continue;
@@ -351,66 +331,40 @@ export function merge(first?: any, ...p: any[]): any {
351
331
  return first!;
352
332
  }
353
333
 
354
- function classString(classProp: ClassProp): string {
355
- if (typeof classProp === "string") {
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;
334
+ function mergeState(target: any, source: any) {
335
+ if (!source) return target;
394
336
 
395
- if (Array.isArray(presentMemo)
396
- && Array.isArray(pastMemo)
397
- && presentMemo.length === pastMemo.length
398
- ) {
399
- let same = true;
400
- for (let i = 0; i < presentMemo.length; i++) {
401
- if (presentMemo[i] !== pastMemo[i]) {
402
- same = false;
403
- break;
337
+ for (const key in source) {
338
+ const value = source[key];
339
+ if (value && typeof value === "object") {
340
+ const targetValue = target[key];
341
+ if (targetValue) {
342
+ if (Array.isArray(value)) {
343
+ target[key] = [...value];
344
+ } else if (value instanceof Date && targetValue !== value) {
345
+ target[key] = new Date(value);
346
+ } else {
347
+ if (Array.isArray(targetValue)) target[key] = mergeState({}, value);
348
+ else if (typeof targetValue === "object") mergeState(target[key], value);
349
+ else target[key] = mergeState({}, value);
350
+ }
351
+ } else if (Array.isArray(value)) {
352
+ target[key] = [...value];
353
+ } else if (value instanceof Date) {
354
+ target[key] = new Date(value);
355
+ } else {
356
+ target[key] = mergeState({}, value);
404
357
  }
405
358
  }
406
- if (same) return past;
407
- }
408
- const newRender = unwrap(present, state);
409
- if (typeof newRender === "object") {
410
- (<any>newRender).__memo = present?.__memo;
359
+ else if (value === undefined) {
360
+ delete target[key];
361
+ }
362
+ else {
363
+ target[key] = value;
364
+ }
411
365
  }
412
- return newRender;
413
- }
366
+ return target;
367
+ };
414
368
 
415
369
  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
370
  // unwrap component if it is memoized
@@ -567,6 +521,49 @@ function render<S>(state: S, patch: Dispatch<S>, parent: ChildNode, childIndex:
567
521
  return undefined;
568
522
  }
569
523
 
524
+ function isNaturalVode(x: ChildVode<any>) {
525
+ return Array.isArray(x) && x.length > 0 && typeof x[0] === "string";
526
+ }
527
+
528
+ function isTextVode(x: ChildVode<any>) {
529
+ return typeof x === "string" || (<Text><unknown>x)?.nodeType === Node.TEXT_NODE;
530
+ }
531
+
532
+ function remember<S>(state: S, present: any, past: any): ChildVode<S> | AttachedVode<S> {
533
+ if (typeof present !== "function")
534
+ return present;
535
+
536
+ const presentMemo = present?.__memo;
537
+ const pastMemo = past?.__memo;
538
+
539
+ if (Array.isArray(presentMemo)
540
+ && Array.isArray(pastMemo)
541
+ && presentMemo.length === pastMemo.length
542
+ ) {
543
+ let same = true;
544
+ for (let i = 0; i < presentMemo.length; i++) {
545
+ if (presentMemo[i] !== pastMemo[i]) {
546
+ same = false;
547
+ break;
548
+ }
549
+ }
550
+ if (same) return past;
551
+ }
552
+ const newRender = unwrap(present, state);
553
+ if (typeof newRender === "object") {
554
+ (<any>newRender).__memo = present?.__memo;
555
+ }
556
+ return newRender;
557
+ }
558
+
559
+ function unwrap<S>(c: Component<S> | ChildVode<S>, s: S): ChildVode<S> {
560
+ if (typeof c === "function") {
561
+ return unwrap(c(s), s);
562
+ } else {
563
+ return c;
564
+ }
565
+ }
566
+
570
567
  function patchProperties<S>(patch: Dispatch<S>, node: ChildNode, oldProps?: Props<S>, newProps?: Props<S>, isSvg?: boolean) {
571
568
  if (!newProps && !oldProps) return;
572
569
  if (!oldProps) { // set new props
@@ -664,37 +661,14 @@ function patchProperty<S>(patch: Dispatch<S>, node: ChildNode, key: string | key
664
661
  return newValue;
665
662
  }
666
663
 
667
- function mergeState(target: any, source: any) {
668
- if (!source) return target;
669
-
670
- for (const key in source) {
671
- const value = source[key];
672
- if (value && typeof value === "object") {
673
- const targetValue = target[key];
674
- if (targetValue) {
675
- if (Array.isArray(value)) {
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
- }
664
+ function classString(classProp: ClassProp): string {
665
+ if (typeof classProp === "string") {
666
+ return classProp;
667
+ } else if (Array.isArray(classProp)) {
668
+ return classProp.map(classString).join(" ");
669
+ } else if (typeof classProp === "object") {
670
+ return Object.keys(classProp!).filter(k => classProp![k]).join(" ");
671
+ } else {
672
+ return "";
698
673
  }
699
- return target;
700
- };
674
+ }
package/vode.mjs CHANGED
@@ -1,21 +1,27 @@
1
1
  // src/vode.ts
2
- var EmptyPatch = {};
3
- function vode(tag, props, ...children) {
4
- if (Array.isArray(tag)) {
5
- return tag;
6
- }
7
- if (props) {
8
- return [tag, props, ...children];
9
- }
10
- return [tag, ...children];
11
- }
12
2
  function createState(state) {
13
3
  return state;
14
4
  }
15
- function patch(p) {
5
+ function createPatch(p) {
16
6
  return p;
17
7
  }
8
+ function vode(tag, props, ...children) {
9
+ if (!tag)
10
+ throw new Error("tag must be a string or vode");
11
+ if (Array.isArray(tag))
12
+ return tag;
13
+ else if (props)
14
+ return [tag, props, ...children];
15
+ else
16
+ return [tag, ...children];
17
+ }
18
18
  function app(container, initialState, dom, ...initialPatches) {
19
+ if (!container)
20
+ throw new Error("container must be a valid HTMLElement");
21
+ if (!initialState || typeof initialState !== "object")
22
+ throw new Error("initialState must be an object");
23
+ if (typeof dom !== "function")
24
+ throw new Error("dom must be a function that returns a vode");
19
25
  const _vode = {};
20
26
  _vode.stats = { renderTime: 0, renderCount: 0, queueLengthBeforeRender: 0, queueLengthAfterRender: 0, liveEffectCount: 0, patchCount: 0, renderPatchCount: 0 };
21
27
  Object.defineProperty(initialState, "patch", {
@@ -65,7 +71,7 @@ function app(container, initialState, dom, ...initialPatches) {
65
71
  _vode.patch(action(_vode.state));
66
72
  } else {
67
73
  _vode.stats.renderPatchCount++;
68
- _vode.q.push(action);
74
+ _vode.q = mergeState(_vode.q || {}, action);
69
75
  if (!_vode.isRendering)
70
76
  _vode.render();
71
77
  }
@@ -76,25 +82,19 @@ function app(container, initialState, dom, ...initialPatches) {
76
82
  configurable: true,
77
83
  writable: false,
78
84
  value: () => requestAnimationFrame(() => {
79
- if (_vode.isRendering || _vode.q.length === 0)
85
+ if (_vode.isRendering || !_vode.q)
80
86
  return;
81
87
  _vode.isRendering = true;
82
88
  const sw = Date.now();
83
89
  try {
84
- _vode.stats.queueLengthBeforeRender = _vode.q.length;
85
- while (_vode.q.length > 0) {
86
- const patch2 = _vode.q.shift();
87
- if (patch2 === EmptyPatch)
88
- continue;
89
- mergeState(_vode.state, patch2);
90
- }
90
+ _vode.state = mergeState(_vode.state, _vode.q);
91
+ _vode.q = null;
91
92
  _vode.vode = render(_vode.state, _vode.patch, container, 0, _vode.vode, dom(_vode.state));
92
93
  } finally {
93
94
  _vode.isRendering = false;
94
95
  _vode.stats.renderCount++;
95
96
  _vode.stats.renderTime = Date.now() - sw;
96
- _vode.stats.queueLengthAfterRender = _vode.q.length;
97
- if (_vode.q.length > 0) {
97
+ if (_vode.q) {
98
98
  _vode.render();
99
99
  }
100
100
  }
@@ -102,7 +102,7 @@ function app(container, initialState, dom, ...initialPatches) {
102
102
  });
103
103
  _vode.patch = initialState.patch;
104
104
  _vode.state = initialState;
105
- _vode.q = [];
105
+ _vode.q = null;
106
106
  const root = container;
107
107
  root._vode = _vode;
108
108
  const initialVode = dom(initialState);
@@ -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 classString(classProp) {
216
- if (typeof classProp === "string") {
217
- return classProp;
218
- } else if (Array.isArray(classProp)) {
219
- return classProp.map(classString).join(" ");
220
- } else if (typeof classProp === "object") {
221
- return Object.keys(classProp).filter((k) => classProp[k]).join(" ");
222
- } else {
223
- return "";
224
- }
225
- }
226
- function isNaturalVode(x) {
227
- return Array.isArray(x) && x.length > 0 && typeof x[0] === "string";
228
- }
229
- function isTextVode(x) {
230
- return typeof x === "string" || x?.nodeType === Node.TEXT_NODE;
231
- }
232
- function unwrap(c, s) {
233
- if (typeof c === "function") {
234
- return unwrap(c(s), s);
235
- } else {
236
- return c;
237
- }
238
- }
239
- function memo(compare, componentOrProps) {
240
- componentOrProps.__memo = compare;
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
231
  }
259
- const newRender = unwrap(present, state);
260
- if (typeof newRender === "object") {
261
- newRender.__memo = present?.__memo;
262
- }
263
- return newRender;
232
+ return target;
264
233
  }
265
- function render(state, patch2, parent, childIndex, oldVode, newVode, svg) {
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 && patch2(oldNode.onUnmount(oldNode));
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 && patch2(oldNode.onUnmount(oldNode));
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(patch2, newNode, undefined, properties, svg);
286
+ patchProperties(patch, newNode, undefined, properties, svg);
318
287
  if (oldNode) {
319
- oldNode.onUnmount && patch2(oldNode.onUnmount(oldNode));
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, patch2, newNode, i, undefined, child2, svg);
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 && patch2(newNode.onMount(newNode));
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(patch2, oldNode, props(oldVode), properties, svg);
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(patch2, oldNode, props(oldVode), properties, svg);
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, patch2, oldNode, i, oldChild, child2, svg);
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 patchProperties(patch2, node, oldProps, newProps, isSvg) {
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(patch2, node, key, undefined, newValue, isSvg);
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(patch2, node, key, oldValue, newValue, isSvg);
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(patch2, node, key, oldValue, newValue, isSvg);
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(patch2, node, key, oldValue, undefined, isSvg);
416
+ oldProps[key] = patchProperty(patch, node, key, oldValue, undefined, isSvg);
413
417
  }
414
418
  }
415
419
  }
416
- function patchProperty(patch2, node, key, oldValue, newValue, isSvg) {
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) => patch2([action, 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 = () => patch2([action, ...arr.slice(1)]);
463
+ eventHandler = () => patch([action, ...arr.slice(1)]);
460
464
  } else {
461
- eventHandler = (evt) => patch2([action, evt]);
465
+ eventHandler = (evt) => patch([action, evt]);
462
466
  }
463
467
  } else if (typeof newValue === "object") {
464
- eventHandler = () => patch2(newValue);
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 mergeState(target, source) {
478
- if (!source)
479
- return target;
480
- for (const key in source) {
481
- const value = source[key];
482
- if (value && typeof value === "object") {
483
- const targetValue = target[key];
484
- if (targetValue) {
485
- if (Array.isArray(value)) {
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,
@@ -915,7 +894,6 @@ export {
915
894
  FECOMPONENTTRANSFER,
916
895
  FECOLORMATRIX,
917
896
  FEBLEND,
918
- EmptyPatch,
919
897
  EMBED,
920
898
  EM,
921
899
  ELLIPSE,