@ryupold/vode 1.5.1 → 1.6.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/index.ts CHANGED
@@ -3,4 +3,5 @@ export * from "./src/vode.js";
3
3
  // utilities
4
4
  export * from "./src/vode-tags.js";
5
5
  export * from "./src/merge-class.js";
6
+ export * from "./src/merge-style.js";
6
7
  export * from "./src/state-context.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryupold/vode",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "a minimalist web framework",
5
5
  "author": "Michael Scherbakow (ryupold)",
6
6
  "license": "MIT",
@@ -26,18 +26,17 @@
26
26
  "homepage": "https://github.com/ryupold/vode#readme",
27
27
  "module": "index.ts",
28
28
  "scripts": {
29
- "build": "bun build index.ts --outfile dist/vode.mjs",
30
- "build-min": "bun build index.ts --outfile dist/vode.min.mjs --minify",
31
- "build-classic": "esbuild index.ts --outfile=dist/vode.js --bundle --global-name=V",
32
- "build-classic-min": "esbuild index.ts --outfile=dist/vode.min.js --bundle --global-name=V --minify",
33
- "release": "bun run build && bun run build-min && bun run build-classic && bun run build-classic-min",
29
+ "build": "esbuild index.ts --bundle --format=esm --outfile=dist/vode.mjs ",
30
+ "build-min": "esbuild index.ts --bundle --format=esm --minify --outfile=dist/vode.min.mjs",
31
+ "build-classic": "esbuild index.ts --outfile=dist/vode.js --bundle --format=iife --global-name=V",
32
+ "build-classic-min": "esbuild index.ts --outfile=dist/vode.min.js --bundle --format=iife --global-name=V --minify",
33
+ "release": "npm run build && npm run build-min && npm run build-classic && npm run build-classic-min",
34
34
  "publish": "npm publish --access public",
35
35
  "clean": "tsc -b --clean",
36
36
  "watch": "tsc -b -w"
37
37
  },
38
38
  "devDependencies": {
39
- "bun": "1.3.1",
40
- "esbuild": "0.25.12",
39
+ "esbuild": "0.27.0",
41
40
  "typescript": "5.9.3"
42
41
  }
43
42
  }
@@ -0,0 +1,23 @@
1
+ import { StyleProp } from "./vode.js";
2
+
3
+ const tempDivForStyling = document.createElement('div');
4
+
5
+ /** merge `StyleProps`s regardless of type
6
+ * @returns {string} merged StyleProp */
7
+ export function mergeStyle(...props: StyleProp[]): StyleProp {
8
+ try{
9
+ const merged = tempDivForStyling.style;
10
+ for (const style of props) {
11
+ if (typeof style === 'object' && style !== null) {
12
+ for (const key in style) {
13
+ merged[key] = style[key];
14
+ }
15
+ } else if (typeof style === 'string') {
16
+ merged.cssText += ';' + style;
17
+ }
18
+ }
19
+ return merged.cssText;
20
+ } finally {
21
+ tempDivForStyling.style.cssText = '';
22
+ }
23
+ }
package/src/vode.ts CHANGED
@@ -86,7 +86,6 @@ export type ContainerNode<S = PatchableState> = HTMLElement & {
86
86
  _vode: {
87
87
  state: PatchableState<S>,
88
88
  vode: AttachedVode<S>,
89
- patch: Dispatch<S>,
90
89
  renderSync: () => void,
91
90
  renderAsync: () => Promise<unknown>,
92
91
  syncRenderer: (cb: () => void) => void,
@@ -131,18 +130,25 @@ export function vode<S = PatchableState>(tag: Tag | Vode<S>, props?: Props<S> |
131
130
  * @param initialPatches variadic list of patches that are applied after the first render
132
131
  * @returns a patch function that can be used to update the state
133
132
  */
134
- export function app<S = PatchableState>(container: Element, state: Omit<S, "patch">, dom: (s: S) => Vode<S>, ...initialPatches: Patch<S>[]) {
133
+ export function app<S extends PatchableState = PatchableState>(
134
+ container: Element,
135
+ state: Omit<S, "patch">,
136
+ dom: (s: S) => Vode<S>,
137
+ ...initialPatches: Patch<S>[]
138
+ ): Dispatch<S> {
135
139
  if (!container?.parentElement) throw new Error("first argument to app() must be a valid HTMLElement inside the <html></html> document");
136
140
  if (!state || typeof state !== "object") throw new Error("second argument to app() must be a state object");
137
141
  if (typeof dom !== "function") throw new Error("third argument to app() must be a function that returns a vode");
138
142
 
139
- const _vode = {} as ContainerNode<S>["_vode"] & { patch: (action: Patch<S>, animate?: boolean) => void };
143
+ const _vode = {} as ContainerNode<S>["_vode"];
140
144
  _vode.syncRenderer = globals.requestAnimationFrame;
141
145
  _vode.asyncRenderer = globals.startViewTransition;
142
146
  _vode.qSync = null;
143
147
  _vode.qAsync = null;
144
148
  _vode.stats = { lastSyncRenderTime: 0, lastAsyncRenderTime: 0, syncRenderCount: 0, asyncRenderCount: 0, liveEffectCount: 0, patchCount: 0, syncRenderPatchCount: 0, asyncRenderPatchCount: 0 };
145
149
 
150
+ const patchableState = state as PatchableState<S> & { patch: (action: Patch<S>, animate?: boolean) => void };
151
+
146
152
  Object.defineProperty(state, "patch", {
147
153
  enumerable: false, configurable: true,
148
154
  writable: false, value: async (action: Patch<S>, isAsync?: boolean) => {
@@ -157,28 +163,28 @@ export function app<S = PatchableState>(container: Element, state: Omit<S, "patc
157
163
  while (v.done === false) {
158
164
  _vode.stats.liveEffectCount++;
159
165
  try {
160
- _vode.patch!(v.value, isAsync);
166
+ patchableState.patch(v.value, isAsync);
161
167
  v = await generator.next();
162
168
  } finally {
163
169
  _vode.stats.liveEffectCount--;
164
170
  }
165
171
  }
166
- _vode.patch!(v.value as Patch<S>, isAsync);
172
+ patchableState.patch(v.value as Patch<S>, isAsync);
167
173
  } finally {
168
174
  _vode.stats.liveEffectCount--;
169
175
  }
170
176
  } else if ((action as Promise<S>).then) {
171
177
  _vode.stats.liveEffectCount++;
172
178
  try {
173
- const nextState = await (action as Promise<S>);
174
- _vode.patch!(<Patch<S>>nextState, isAsync);
179
+ const resolvedPatch = await (action as Promise<S>);
180
+ patchableState.patch(<Patch<S>>resolvedPatch, isAsync);
175
181
  } finally {
176
182
  _vode.stats.liveEffectCount--;
177
183
  }
178
184
  } else if (Array.isArray(action)) {
179
185
  if (action.length > 0) {
180
186
  for (const p of action) {
181
- _vode.patch(p, !document.hidden && !!_vode.asyncRenderer);
187
+ patchableState.patch(p, !document.hidden && !!_vode.asyncRenderer);
182
188
  }
183
189
  } else { //when [] is patched: 1. skip current animation 2. merge all queued async patches into synced queue
184
190
  _vode.qSync = mergeState(_vode.qSync || {}, _vode.qAsync, false);
@@ -188,7 +194,7 @@ export function app<S = PatchableState>(container: Element, state: Omit<S, "patc
188
194
  _vode.renderSync();
189
195
  }
190
196
  } else if (typeof action === "function") {
191
- _vode.patch!((<(s: S) => unknown>action)(_vode.state), isAsync);
197
+ patchableState.patch((<(s: S) => unknown>action)(_vode.state), isAsync);
192
198
  } else {
193
199
  if (isAsync) {
194
200
  _vode.stats.asyncRenderPatchCount++;
@@ -206,7 +212,7 @@ export function app<S = PatchableState>(container: Element, state: Omit<S, "patc
206
212
  function renderDom(isAsync: boolean) {
207
213
  const sw = Date.now();
208
214
  const vom = dom(_vode.state);
209
- _vode.vode = render(_vode.state, _vode.patch, container.parentElement as Element, 0, _vode.vode, vom)!;
215
+ _vode.vode = render<S>(_vode.state, container.parentElement as Element, 0, _vode.vode, vom)!;
210
216
 
211
217
  if ((<ContainerNode<S>>container).tagName.toUpperCase() !== (vom[0] as Tag).toUpperCase()) { //the tag name was changed during render -> update reference to vode-app-root
212
218
  container = _vode.vode.node as Element;
@@ -262,15 +268,13 @@ export function app<S = PatchableState>(container: Element, state: Omit<S, "patc
262
268
  }
263
269
  });
264
270
 
265
- _vode.patch = (<PatchableState<S>>state).patch;
266
- _vode.state = <PatchableState<S>>state;
271
+ _vode.state = patchableState;
267
272
 
268
273
  const root = container as ContainerNode<S>;
269
274
  root._vode = _vode;
270
275
 
271
276
  _vode.vode = render(
272
277
  <S>state,
273
- _vode.patch!,
274
278
  container.parentElement,
275
279
  Array.from(container.parentElement.children).indexOf(container),
276
280
  hydrate<S>(container, true) as AttachedVode<S>,
@@ -278,10 +282,10 @@ export function app<S = PatchableState>(container: Element, state: Omit<S, "patc
278
282
  )!;
279
283
 
280
284
  for (const effect of initialPatches) {
281
- _vode.patch!(effect);
285
+ patchableState.patch(effect);
282
286
  }
283
287
 
284
- return _vode.patch;
288
+ return (action: Patch<S>) => patchableState.patch(action);
285
289
  }
286
290
 
287
291
  /** unregister vode app from container and free resources
@@ -467,7 +471,7 @@ function mergeState(target: any, source: any, allowDeletion: boolean) {
467
471
  return target;
468
472
  };
469
473
 
470
- function render<S>(state: S, patch: Dispatch<S>, parent: Element, childIndex: number, oldVode: AttachedVode<S> | undefined, newVode: ChildVode<S>, xmlns?: string): AttachedVode<S> | undefined {
474
+ function render<S extends PatchableState>(state: S, parent: Element, childIndex: number, oldVode: AttachedVode<S> | undefined, newVode: ChildVode<S>, xmlns?: string): AttachedVode<S> | undefined {
471
475
  try {
472
476
  // unwrap component if it is memoized
473
477
  newVode = remember(state, newVode, oldVode) as ChildVode<S>;
@@ -482,7 +486,7 @@ function render<S>(state: S, patch: Dispatch<S>, parent: Element, childIndex: nu
482
486
 
483
487
  // falsy|text|element(A) -> undefined
484
488
  if (isNoVode) {
485
- (<any>oldNode)?.onUnmount && patch((<any>oldNode).onUnmount(oldNode));
489
+ (<any>oldNode)?.onUnmount && state.patch((<any>oldNode).onUnmount(oldNode));
486
490
  oldNode?.remove();
487
491
  return undefined;
488
492
  }
@@ -512,7 +516,7 @@ function render<S>(state: S, patch: Dispatch<S>, parent: Element, childIndex: nu
512
516
  if (isText && (!oldNode || !oldIsText)) {
513
517
  const text = document.createTextNode(newVode as string)
514
518
  if (oldNode) {
515
- (<any>oldNode).onUnmount && patch((<any>oldNode).onUnmount(oldNode));
519
+ (<any>oldNode).onUnmount && state.patch((<any>oldNode).onUnmount(oldNode));
516
520
  oldNode.replaceWith(text);
517
521
  } else {
518
522
  if (parent.childNodes[childIndex]) {
@@ -541,10 +545,15 @@ function render<S>(state: S, patch: Dispatch<S>, parent: Element, childIndex: nu
541
545
  : document.createElement((<Vode<S>>newVode)[0]);
542
546
  (<AttachedVode<S>>newVode).node = newNode;
543
547
 
544
- patchProperties(state, patch, newNode, undefined, properties);
548
+ patchProperties(state, newNode, undefined, properties);
549
+
550
+ if (!!properties && 'catch' in properties) {
551
+ (<any>newVode).node['catch'] = null;
552
+ (<any>newVode).node.removeAttribute('catch');
553
+ }
545
554
 
546
555
  if (oldNode) {
547
- (<any>oldNode).onUnmount && patch((<any>oldNode).onUnmount(oldNode));
556
+ (<any>oldNode).onUnmount && state.patch((<any>oldNode).onUnmount(oldNode));
548
557
  oldNode.replaceWith(newNode);
549
558
  } else {
550
559
  if (parent.childNodes[childIndex]) {
@@ -558,12 +567,12 @@ function render<S>(state: S, patch: Dispatch<S>, parent: Element, childIndex: nu
558
567
  if (newChildren) {
559
568
  for (let i = 0; i < newChildren.length; i++) {
560
569
  const child = newChildren[i];
561
- const attached = render(state, patch, newNode as Element, i, undefined, child, xmlns);
570
+ const attached = render(state, newNode as Element, i, undefined, child, xmlns);
562
571
  (<Vode<S>>newVode!)[properties ? i + 2 : i + 1] = <Vode<S>>attached;
563
572
  }
564
573
  }
565
574
 
566
- (<any>newNode).onMount && patch((<any>newNode).onMount(newNode));
575
+ (<any>newNode).onMount && state.patch((<any>newNode).onMount(newNode));
567
576
  return <AttachedVode<S>>newVode;
568
577
  }
569
578
 
@@ -574,24 +583,24 @@ function render<S>(state: S, patch: Dispatch<S>, parent: Element, childIndex: nu
574
583
  const newvode = <Vode<S>>newVode;
575
584
  const oldvode = <Vode<S>>oldVode;
576
585
 
577
- let hasProps = false;
586
+ const properties = props(newVode);
587
+ let hasProps = !!properties;
588
+ const oldProps = props(oldVode);
589
+
578
590
  if ((<any>newvode[1])?.__memo) {
579
591
  const prev = newvode[1] as any;
580
592
  newvode[1] = remember(state, newvode[1], oldvode[1]) as Vode<S>;
581
593
  if (prev !== newvode[1]) {
582
- const properties = props(newVode);
583
- patchProperties(state, patch, oldNode!, props(oldVode), properties);
584
- hasProps = !!properties;
594
+ patchProperties(state, oldNode!, oldProps, properties);
585
595
  }
586
596
  }
587
597
  else {
588
- const properties = props(newVode);
589
- patchProperties(state, patch, oldNode!, props(oldVode), properties);
590
- hasProps = !!properties;
591
- if (hasProps && 'catch' in (properties!)) { //hold catch information only in vdom
592
- (<any>newVode).node['catch'] = null;
593
- (<any>newVode).node.removeAttribute('catch');
594
- }
598
+ patchProperties(state, oldNode!, oldProps, properties);
599
+ }
600
+
601
+ if (!!properties?.catch && oldProps?.catch !== properties.catch) {
602
+ (<any>newVode).node['catch'] = null;
603
+ (<any>newVode).node.removeAttribute('catch');
595
604
  }
596
605
 
597
606
  const newKids = children(newVode);
@@ -601,7 +610,7 @@ function render<S>(state: S, patch: Dispatch<S>, parent: Element, childIndex: nu
601
610
  const child = newKids[i];
602
611
  const oldChild = oldKids && oldKids[i];
603
612
 
604
- const attached = render(state, patch, oldNode as Element, i, oldChild, child, xmlns);
613
+ const attached = render(state, oldNode as Element, i, oldChild, child, xmlns);
605
614
  if (attached) {
606
615
  (<Vode<S>>newVode)[hasProps ? i + 2 : i + 1] = <Vode<S>>attached;
607
616
  }
@@ -611,7 +620,7 @@ function render<S>(state: S, patch: Dispatch<S>, parent: Element, childIndex: nu
611
620
  if (oldKids) {
612
621
  const newKidsCount = newKids ? newKids.length : 0;
613
622
  for (let i = oldKids.length - 1; i >= newKidsCount; i--) {
614
- render(state, patch, oldNode as Element, i, oldKids[i], undefined, xmlns);
623
+ render(state, oldNode as Element, i, oldKids[i], undefined, xmlns);
615
624
  }
616
625
  }
617
626
 
@@ -624,7 +633,7 @@ function render<S>(state: S, patch: Dispatch<S>, parent: Element, childIndex: nu
624
633
  ? (<(s: S, error: any) => ChildVode<S>>catchVode)(state, error)
625
634
  : catchVode;
626
635
 
627
- return render(state, patch, parent, childIndex,
636
+ return render(state, parent, childIndex,
628
637
  hydrate(((<AttachedVode<S>>newVode)?.node || oldVode?.node) as Element, true) as AttachedVode<S>,
629
638
  handledVode,
630
639
  xmlns);
@@ -679,7 +688,7 @@ function unwrap<S>(c: Component<S> | ChildVode<S>, s: S): ChildVode<S> {
679
688
  }
680
689
  }
681
690
 
682
- function patchProperties<S>(s: S, patch: Dispatch<S>, node: ChildNode, oldProps?: Props<S>, newProps?: Props<S>) {
691
+ function patchProperties<S extends PatchableState>(s: S, node: ChildNode, oldProps?: Props<S>, newProps?: Props<S>) {
683
692
  if (!newProps && !oldProps) return;
684
693
 
685
694
  // match existing properties
@@ -689,8 +698,8 @@ function patchProperties<S>(s: S, patch: Dispatch<S>, node: ChildNode, oldProps?
689
698
  const newValue = newProps?.[key as keyof Props<S>] as PropertyValue<S>;
690
699
 
691
700
  if (oldValue !== newValue) {
692
- if (newProps) newProps[key as keyof Props<S>] = patchProperty(s, patch, node, key, oldValue, newValue);
693
- else patchProperty(s, patch, node, key, oldValue, undefined);
701
+ if (newProps) newProps[key as keyof Props<S>] = patchProperty(s, node, key, oldValue, newValue);
702
+ else patchProperty(s, node, key, oldValue, undefined);
694
703
  }
695
704
  }
696
705
  }
@@ -700,7 +709,7 @@ function patchProperties<S>(s: S, patch: Dispatch<S>, node: ChildNode, oldProps?
700
709
  for (const key in newProps) {
701
710
  if (!(key in oldProps)) {
702
711
  const newValue = newProps[key as keyof Props<S>] as PropertyValue<S>;
703
- newProps[key as keyof Props<S>] = patchProperty(s, patch, <Element>node, key, undefined, newValue);
712
+ newProps[key as keyof Props<S>] = patchProperty(s, <Element>node, key, undefined, newValue);
704
713
  }
705
714
  }
706
715
  }
@@ -708,12 +717,12 @@ function patchProperties<S>(s: S, patch: Dispatch<S>, node: ChildNode, oldProps?
708
717
  else if (newProps) {
709
718
  for (const key in newProps) {
710
719
  const newValue = newProps[key as keyof Props<S>] as PropertyValue<S>;
711
- newProps[key as keyof Props<S>] = patchProperty(s, patch, <Element>node, key, undefined, newValue);
720
+ newProps[key as keyof Props<S>] = patchProperty(s, <Element>node, key, undefined, newValue);
712
721
  }
713
722
  }
714
723
  }
715
724
 
716
- function patchProperty<S>(s: S, patch: Dispatch<S>, node: ChildNode, key: string | keyof ElementEventMap, oldValue?: PropertyValue<S>, newValue?: PropertyValue<S>) {
725
+ function patchProperty<S extends PatchableState>(s: S, node: ChildNode, key: string | keyof ElementEventMap, oldValue?: PropertyValue<S>, newValue?: PropertyValue<S>) {
717
726
  if (key === "style") {
718
727
  if (!newValue) {
719
728
  (node as HTMLElement).style.cssText = "";
@@ -749,9 +758,9 @@ function patchProperty<S>(s: S, patch: Dispatch<S>, node: ChildNode, key: string
749
758
  let eventHandler: Function | null = null;
750
759
  if (typeof newValue === "function") {
751
760
  const action = newValue as EventFunction<S>;
752
- eventHandler = (evt: Event) => patch(action(s, evt));
761
+ eventHandler = (evt: Event) => s.patch(action(s, evt));
753
762
  } else if (typeof newValue === "object") {
754
- eventHandler = () => patch(newValue as Patch<S>);
763
+ eventHandler = () => s.patch(newValue as Patch<S>);
755
764
  }
756
765
 
757
766
  (<any>node)[key] = eventHandler;