@ryupold/vode 0.9.6 → 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/.github/workflows/npm-publish.yml +1 -5
- package/README.md +7 -0
- package/package.json +3 -2
- package/src/vode.ts +198 -212
- package/vode.mjs +941 -0
|
@@ -6,17 +6,13 @@ on:
|
|
|
6
6
|
- 'main'
|
|
7
7
|
|
|
8
8
|
jobs:
|
|
9
|
-
build:
|
|
9
|
+
test-build-and-publish-to-npm:
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
11
|
permissions:
|
|
12
12
|
contents: read
|
|
13
13
|
id-token: write
|
|
14
14
|
steps:
|
|
15
15
|
- uses: actions/checkout@v4
|
|
16
|
-
- uses: actions/setup-node@v4
|
|
17
|
-
with:
|
|
18
|
-
node-version: '22.x'
|
|
19
|
-
registry-url: 'https://registry.npmjs.org'
|
|
20
16
|
- uses: oven-sh/setup-bun@v2
|
|
21
17
|
- run: bun run build
|
|
22
18
|
- run: |
|
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryupold/vode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Small web framework for minimal websites",
|
|
5
5
|
"author": "Michael Scherbakow (ryupold)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"homepage": "https://github.com/ryupold/vode#readme",
|
|
23
23
|
"module": "index.ts",
|
|
24
24
|
"scripts": {
|
|
25
|
-
"build": "bun build index.ts --outfile
|
|
25
|
+
"build": "bun build index.ts --outfile vode.mjs",
|
|
26
|
+
"build-min": "bun build index.ts --outfile vode.min.mjs --minify",
|
|
26
27
|
"pack": "rm *.tgz && bun run build && bun pm pack",
|
|
27
28
|
"publish": "bun publish --provenance --access public",
|
|
28
29
|
"clean": "tsc -b --clean",
|
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,51 +45,66 @@ 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 & {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 */
|
|
76
|
+
_vode: {
|
|
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 */
|
|
84
|
+
stats: {
|
|
85
|
+
patchCount: number,
|
|
86
|
+
liveEffectCount: number,
|
|
87
|
+
renderPatchCount: number,
|
|
88
|
+
renderCount: number,
|
|
89
|
+
renderTime: number,
|
|
90
|
+
queueLengthBeforeRender: number,
|
|
91
|
+
queueLengthAfterRender: number,
|
|
92
|
+
},
|
|
93
|
+
}
|
|
87
94
|
};
|
|
88
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
|
+
|
|
89
102
|
/** type-safe way to create a vode. useful for type inference and autocompletion.
|
|
90
103
|
*
|
|
91
|
-
*
|
|
92
|
-
* -
|
|
93
|
-
* - tag and
|
|
94
|
-
* -
|
|
95
|
-
* - 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>`
|
|
96
108
|
*/
|
|
97
109
|
export function vode<S extends object | unknown>(tag: Tag | Vode<S>, props?: Props<S> | ChildVode<S>, ...children: ChildVode<S>[]): Vode<S> {
|
|
98
110
|
if (Array.isArray(tag)) {
|
|
@@ -104,118 +116,129 @@ export function vode<S extends object | unknown>(tag: Tag | Vode<S>, props?: Pro
|
|
|
104
116
|
return [tag, ...children];
|
|
105
117
|
}
|
|
106
118
|
|
|
107
|
-
/**
|
|
108
|
-
export function createState<S>(state: S): PatchableState<S> { return state as PatchableState<S>; }
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
/** for a type safe way to create a deeply partial patch object or effect */
|
|
112
|
-
export function patch<S>(p: DeepPartial<S> | Effect<S> | NoRenderPatch): typeof p { return p; }
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* create a vode app inside a container element
|
|
119
|
+
/** create a vode app inside a container element
|
|
116
120
|
* @param container will use this container as root and places the result of the dom function and further renderings in it
|
|
117
|
-
* @param initialState
|
|
121
|
+
* @param initialState
|
|
118
122
|
* @param dom creates the initial dom from the state and is called on every render
|
|
119
123
|
* @param initialPatches variadic list of patches that are applied after the first render
|
|
120
124
|
* @returns a patch function that can be used to update the state
|
|
121
125
|
*/
|
|
122
|
-
export function app<S>(container: HTMLElement, initialState: Omit<S, "patch">, dom: Component<S>, ...initialPatches: Patch<S>[]) {
|
|
123
|
-
const
|
|
124
|
-
|
|
126
|
+
export function app<S extends object | unknown>(container: HTMLElement, initialState: Omit<S, "patch">, dom: Component<S>, ...initialPatches: Patch<S>[]) {
|
|
127
|
+
const _vode = {} as ContainerNode<S>["_vode"];
|
|
128
|
+
_vode.stats = { renderTime: 0, renderCount: 0, queueLengthBeforeRender: 0, queueLengthAfterRender: 0, liveEffectCount: 0, patchCount: 0, renderPatchCount: 0 };
|
|
125
129
|
|
|
126
130
|
Object.defineProperty(initialState, "patch", {
|
|
127
131
|
enumerable: false, configurable: true,
|
|
128
132
|
writable: false, value: async (action: Patch<S>) => {
|
|
129
133
|
if (!action || (typeof action !== "function" && typeof action !== "object")) return;
|
|
130
|
-
|
|
134
|
+
_vode.stats.patchCount++;
|
|
131
135
|
|
|
132
136
|
if ((action as AsyncGenerator<Patch<S>, unknown, void>)?.next) {
|
|
133
137
|
const generator = action as AsyncGenerator<Patch<S>, unknown, void>;
|
|
134
|
-
|
|
138
|
+
_vode.stats.liveEffectCount++;
|
|
135
139
|
try {
|
|
136
140
|
let v = await generator.next();
|
|
137
141
|
while (v.done === false) {
|
|
138
|
-
|
|
142
|
+
_vode.stats.liveEffectCount++;
|
|
139
143
|
try {
|
|
140
|
-
|
|
144
|
+
_vode.patch!(v.value);
|
|
141
145
|
v = await generator.next();
|
|
142
146
|
} finally {
|
|
143
|
-
|
|
147
|
+
_vode.stats.liveEffectCount--;
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
|
-
|
|
150
|
+
_vode.patch!(v.value as Patch<S>);
|
|
147
151
|
} finally {
|
|
148
|
-
|
|
152
|
+
_vode.stats.liveEffectCount--;
|
|
149
153
|
}
|
|
150
154
|
} else if ((action as Promise<S>).then) {
|
|
151
|
-
|
|
155
|
+
_vode.stats.liveEffectCount++;
|
|
152
156
|
try {
|
|
153
157
|
const nextState = await (action as Promise<S>);
|
|
154
|
-
|
|
158
|
+
_vode.patch!(<Patch<S>>nextState);
|
|
155
159
|
} finally {
|
|
156
|
-
|
|
160
|
+
_vode.stats.liveEffectCount--;
|
|
157
161
|
}
|
|
158
162
|
} else if (Array.isArray(action)) {
|
|
159
163
|
if (typeof action[0] === "function") {
|
|
160
164
|
if (action.length > 1)
|
|
161
|
-
|
|
162
|
-
else
|
|
165
|
+
_vode.patch!(action[0](_vode.state!, ...(action as any[]).slice(1)));
|
|
166
|
+
else _vode.patch!(action[0](_vode.state!));
|
|
163
167
|
} else {
|
|
164
|
-
|
|
168
|
+
_vode.stats.patchCount--;
|
|
165
169
|
}
|
|
166
170
|
} else if (typeof action === "function") {
|
|
167
|
-
|
|
171
|
+
_vode.patch!((<EffectFunction<S>>action)(_vode.state));
|
|
168
172
|
} else {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (!
|
|
173
|
+
_vode.stats.renderPatchCount++;
|
|
174
|
+
_vode.q!.push(<Patch<S>>action);
|
|
175
|
+
if (!_vode.isRendering) _vode.render!();
|
|
172
176
|
}
|
|
173
177
|
}
|
|
174
178
|
});
|
|
175
179
|
|
|
176
|
-
Object.defineProperty(
|
|
180
|
+
Object.defineProperty(_vode, "render", {
|
|
177
181
|
enumerable: false, configurable: true,
|
|
178
182
|
writable: false, value: () => requestAnimationFrame(() => {
|
|
179
|
-
if (
|
|
180
|
-
|
|
183
|
+
if (_vode.isRendering || _vode.q!.length === 0) return;
|
|
184
|
+
_vode.isRendering = true;
|
|
181
185
|
const sw = Date.now();
|
|
182
186
|
try {
|
|
183
|
-
|
|
187
|
+
_vode.stats.queueLengthBeforeRender = _vode.q!.length;
|
|
184
188
|
|
|
185
|
-
while (
|
|
186
|
-
const patch =
|
|
187
|
-
if(patch === EmptyPatch) continue;
|
|
188
|
-
mergeState(
|
|
189
|
+
while (_vode.q!.length > 0) {
|
|
190
|
+
const patch = _vode.q!.shift();
|
|
191
|
+
if (patch === EmptyPatch) continue;
|
|
192
|
+
mergeState(_vode.state, patch);
|
|
189
193
|
}
|
|
190
|
-
|
|
194
|
+
_vode.vode = render(_vode.state, _vode.patch, container, 0, _vode.vode, dom(_vode.state))!;
|
|
191
195
|
} finally {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (
|
|
197
|
-
|
|
196
|
+
_vode.isRendering = false;
|
|
197
|
+
_vode.stats.renderCount++;
|
|
198
|
+
_vode.stats.renderTime = Date.now() - sw;
|
|
199
|
+
_vode.stats.queueLengthAfterRender = _vode.q!.length;
|
|
200
|
+
if (_vode.q!.length > 0) {
|
|
201
|
+
_vode.render!();
|
|
198
202
|
}
|
|
199
203
|
}
|
|
200
204
|
})
|
|
201
205
|
});
|
|
202
206
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
_vode.patch = (<PatchableState<S>>initialState).patch;
|
|
208
|
+
_vode.state = <PatchableState<S>>initialState;
|
|
209
|
+
_vode.q = [];
|
|
210
|
+
|
|
211
|
+
const root = container as ContainerNode<S>;
|
|
212
|
+
root._vode = _vode;
|
|
213
|
+
|
|
206
214
|
const initialVode = dom(<S>initialState);
|
|
207
|
-
|
|
208
|
-
|
|
215
|
+
_vode.vode = <AttachedVode<S>>initialVode;
|
|
216
|
+
_vode.vode = render(<S>initialState, _vode.patch!, container, 0, undefined, initialVode)!;
|
|
209
217
|
|
|
210
218
|
for (const effect of initialPatches) {
|
|
211
|
-
|
|
219
|
+
_vode.patch!(effect);
|
|
212
220
|
}
|
|
213
221
|
|
|
214
|
-
return
|
|
222
|
+
return _vode.patch;
|
|
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;
|
|
215
238
|
}
|
|
216
239
|
|
|
217
|
-
/** get properties of a vode, if there
|
|
218
|
-
export function props<S
|
|
240
|
+
/** get properties object of a vode, if there is any */
|
|
241
|
+
export function props<S>(vode: ChildVode<S> | AttachedVode<S>): Props<S> | undefined {
|
|
219
242
|
if (Array.isArray(vode)
|
|
220
243
|
&& vode.length > 1
|
|
221
244
|
&& vode[1]
|
|
@@ -232,6 +255,7 @@ export function props<S extends object | unknown>(vode: ChildVode<S> | AttachedV
|
|
|
232
255
|
return undefined;
|
|
233
256
|
}
|
|
234
257
|
|
|
258
|
+
/** merge `ClassProp`s regardless of structure */
|
|
235
259
|
export function mergeClass(a: ClassProp, b: ClassProp): ClassProp {
|
|
236
260
|
if (!a) return b;
|
|
237
261
|
if (!b) return a;
|
|
@@ -282,48 +306,14 @@ export function mergeClass(a: ClassProp, b: ClassProp): ClassProp {
|
|
|
282
306
|
throw new Error(`cannot merge classes of ${a} (${typeof a}) and ${b} (${typeof b})`);
|
|
283
307
|
}
|
|
284
308
|
|
|
285
|
-
export function patchProps<S extends object | unknown>(vode: Vode<S>, props: Props<S>): void {
|
|
286
|
-
if (!Array.isArray(vode)) return;
|
|
287
|
-
|
|
288
|
-
if (vode.length > 1) {
|
|
289
|
-
if (!Array.isArray(vode[1]) && typeof vode[1] === "object") {
|
|
290
|
-
vode[1] = merge(vode[1], props);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (childCount(vode) > 0) {
|
|
295
|
-
(<FullVode<S>>vode).push(null);
|
|
296
|
-
}
|
|
297
|
-
for (let i = vode.length - 1; i > 0; i--) {
|
|
298
|
-
if (i > 1) vode[i] = vode[i - 1];
|
|
299
|
-
}
|
|
300
|
-
vode[1] = props;
|
|
301
|
-
} else {
|
|
302
|
-
(<FullVode<S>>vode).push(props);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
309
|
/** get a slice of all children of a vode, if there are any */
|
|
307
|
-
export function children<S
|
|
310
|
+
export function children<S>(vode: ChildVode<S> | AttachedVode<S>): ChildVode<S>[] | null {
|
|
308
311
|
const start = childrenStart(vode);
|
|
309
312
|
if (start > 0) {
|
|
310
313
|
return (<Vode<S>>vode).slice(start) as Vode<S>[];
|
|
311
314
|
}
|
|
312
315
|
|
|
313
|
-
return
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/** index in vode at which child-vodes start */
|
|
317
|
-
export function childrenStart<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): number {
|
|
318
|
-
return props(vode) ? 2 : 1;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/** html tag of the vode or #text if it is a text node */
|
|
322
|
-
export function tag<S extends object | unknown>(v: Vode<S> | TextVode | NoVode | AttachedVode<S>): Tag | "#text" | undefined {
|
|
323
|
-
return !!v ? (Array.isArray(v)
|
|
324
|
-
? v[0] : (typeof v === "string" || (<any>v).nodeType === Node.TEXT_NODE)
|
|
325
|
-
? "#text" : undefined) as Tag
|
|
326
|
-
: undefined;
|
|
316
|
+
return null;
|
|
327
317
|
}
|
|
328
318
|
|
|
329
319
|
export function childCount<S>(vode: Vode<S>) { return vode.length - childrenStart(vode); }
|
|
@@ -332,11 +322,13 @@ export function child<S>(vode: Vode<S>, index: number): ChildVode<S> | undefined
|
|
|
332
322
|
return vode[index + childrenStart(vode)] as ChildVode<S>;
|
|
333
323
|
}
|
|
334
324
|
|
|
335
|
-
/**
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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 {
|
|
340
332
|
first = mergeState({}, first);
|
|
341
333
|
for (const pp of p) {
|
|
342
334
|
if (!pp) continue;
|
|
@@ -345,66 +337,40 @@ export function merge(first?: any, ...p: any[]): any {
|
|
|
345
337
|
return first!;
|
|
346
338
|
}
|
|
347
339
|
|
|
348
|
-
function
|
|
349
|
-
if (
|
|
350
|
-
return classProp;
|
|
351
|
-
} else if (Array.isArray(classProp)) {
|
|
352
|
-
return classProp.map(classString).join(" ");
|
|
353
|
-
} else if (typeof classProp === "object") {
|
|
354
|
-
return Object.keys(classProp!).filter(k => classProp![k]).join(" ");
|
|
355
|
-
} else {
|
|
356
|
-
return "";
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function isNaturalVode(x: ChildVode<any>) {
|
|
361
|
-
return Array.isArray(x) && x.length > 0 && typeof x[0] === "string";
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function isTextVode(x: ChildVode<any>) {
|
|
365
|
-
return typeof x === "string" || (<Text><unknown>x)?.nodeType === Node.TEXT_NODE;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function unwrap<S>(c: Component<S> | ChildVode<S>, s: S): ChildVode<S> {
|
|
369
|
-
if (typeof c === "function") {
|
|
370
|
-
return unwrap(c(s), s);
|
|
371
|
-
} else {
|
|
372
|
-
return c;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/** memoization of the given component or props (compare array is compared element by element (===) with the previous render) */
|
|
377
|
-
export function memo<S extends object | unknown>(compare: any[], componentOrProps: Component<S> | ((s: S) => Props<S>)): typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S> {
|
|
378
|
-
(<any>componentOrProps).__memo = compare;
|
|
379
|
-
return componentOrProps as typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S>;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function remember<S>(state: S, present: any, past: any): ChildVode<S> | AttachedVode<S> {
|
|
383
|
-
if (typeof present !== "function")
|
|
384
|
-
return present;
|
|
385
|
-
|
|
386
|
-
const presentMemo = present?.__memo;
|
|
387
|
-
const pastMemo = past?.__memo;
|
|
340
|
+
function mergeState(target: any, source: any) {
|
|
341
|
+
if (!source) return target;
|
|
388
342
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
&&
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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);
|
|
398
363
|
}
|
|
399
364
|
}
|
|
400
|
-
if (
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
365
|
+
else if (value === undefined) {
|
|
366
|
+
delete target[key];
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
target[key] = value;
|
|
370
|
+
}
|
|
405
371
|
}
|
|
406
|
-
return
|
|
407
|
-
}
|
|
372
|
+
return target;
|
|
373
|
+
};
|
|
408
374
|
|
|
409
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 {
|
|
410
376
|
// unwrap component if it is memoized
|
|
@@ -561,6 +527,49 @@ function render<S>(state: S, patch: Dispatch<S>, parent: ChildNode, childIndex:
|
|
|
561
527
|
return undefined;
|
|
562
528
|
}
|
|
563
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
|
+
|
|
564
573
|
function patchProperties<S>(patch: Dispatch<S>, node: ChildNode, oldProps?: Props<S>, newProps?: Props<S>, isSvg?: boolean) {
|
|
565
574
|
if (!newProps && !oldProps) return;
|
|
566
575
|
if (!oldProps) { // set new props
|
|
@@ -658,37 +667,14 @@ function patchProperty<S>(patch: Dispatch<S>, node: ChildNode, key: string | key
|
|
|
658
667
|
return newValue;
|
|
659
668
|
}
|
|
660
669
|
|
|
661
|
-
function
|
|
662
|
-
if (
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
target[key] = [...value];
|
|
671
|
-
} else if (value instanceof Date && targetValue !== value) {
|
|
672
|
-
target[key] = new Date(value);
|
|
673
|
-
} else {
|
|
674
|
-
if (Array.isArray(targetValue)) target[key] = mergeState({}, value);
|
|
675
|
-
else if (typeof targetValue === "object") mergeState(target[key], value);
|
|
676
|
-
else target[key] = mergeState({}, value);
|
|
677
|
-
}
|
|
678
|
-
} else if (Array.isArray(value)) {
|
|
679
|
-
target[key] = [...value];
|
|
680
|
-
} else if (value instanceof Date) {
|
|
681
|
-
target[key] = new Date(value);
|
|
682
|
-
} else {
|
|
683
|
-
target[key] = mergeState({}, value);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
else if (value === undefined) {
|
|
687
|
-
delete target[key];
|
|
688
|
-
}
|
|
689
|
-
else {
|
|
690
|
-
target[key] = value;
|
|
691
|
-
}
|
|
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 "";
|
|
692
679
|
}
|
|
693
|
-
|
|
694
|
-
};
|
|
680
|
+
}
|