@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/.github/workflows/npm-publish.yml +1 -2
- package/README.md +5 -1
- package/dist/vode.js +62 -41
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +395 -381
- package/index.ts +1 -0
- package/package.json +7 -8
- package/src/merge-style.ts +23 -0
- package/src/vode.ts +53 -44
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryupold/vode",
|
|
3
|
-
"version": "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": "
|
|
30
|
-
"build-min": "
|
|
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": "
|
|
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
|
-
"
|
|
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>(
|
|
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"]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
285
|
+
patchableState.patch(effect);
|
|
282
286
|
}
|
|
283
287
|
|
|
284
|
-
return
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
693
|
-
else patchProperty(s,
|
|
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,
|
|
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,
|
|
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,
|
|
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;
|