@ryupold/vode 0.9.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 +23 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/index.ts +5 -0
- package/package.json +32 -0
- package/src/api-call.ts +116 -0
- package/src/helpers.ts +44 -0
- package/src/html.ts +37 -0
- package/src/style.ts +17 -0
- package/src/vode-tags.ts +207 -0
- package/src/vode.ts +694 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Publish to https://registry.npmjs.org
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- 'main'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
# Setup .npmrc file to publish to npm
|
|
17
|
+
- uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: '20.x'
|
|
20
|
+
registry-url: 'https://registry.npmjs.org'
|
|
21
|
+
- run: npm publish --provenance --access public
|
|
22
|
+
env:
|
|
23
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Michael Scherbakow
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# vode
|
|
2
|
+
|
|
3
|
+
Small web framework for minimal websites.
|
|
4
|
+
Each vode app has its own state and renders a tree of HTML elements.
|
|
5
|
+
The state is a singleton object that can be updated, and the UI will re-render when a patch is supplied. Nesting vode-apps is undefined behavior for now.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @ryupold/vode --save
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Patch
|
|
14
|
+
|
|
15
|
+
The `patch` function returned by `app(...)` is a function that can be passed an object called `Patch` this object is used to update the state and re-render the UI. It takes a `Patch` object that describes the changes to be made to the state in a "trickle down manner". The `Patch` can be a simple object or a function that returns a new `Patch` to the current state. It can also be an async and/or genrator function that yields `Patch`es. Events also can return a `Patch`. When a number | boolean | string | null | undefined is applied as a `Patch`, it will be ignored.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
index.html
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<html>
|
|
23
|
+
<head>
|
|
24
|
+
<title>Vode Example</title>
|
|
25
|
+
<script type="module" src="main.js"></script>
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<div id="app"></div>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
main.ts
|
|
34
|
+
```ts
|
|
35
|
+
// import { app, createState} from 'vode.js';
|
|
36
|
+
// import { BR, DIV, INPUT, SPAN } from 'vode-tags.js';
|
|
37
|
+
import { app, createState, BR, DIV, INPUT, SPAN } from '@ryupold/vode';
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
const init = createState({
|
|
41
|
+
counter: 0,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
type State = typeof init;
|
|
45
|
+
|
|
46
|
+
const appNode = document.getElementById('app');
|
|
47
|
+
|
|
48
|
+
app<State>(appNode, init,
|
|
49
|
+
(s: State) => [DIV,
|
|
50
|
+
[INPUT, {
|
|
51
|
+
type: 'button',
|
|
52
|
+
onclick: { counter: s.counter + 1 },
|
|
53
|
+
value: 'Click me',
|
|
54
|
+
}],
|
|
55
|
+
[BR],
|
|
56
|
+
[SPAN, { style: { color: 'red' } }, `${s.counter}`],
|
|
57
|
+
]
|
|
58
|
+
);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Claude
|
|
62
|
+
|
|
63
|
+
> Prompt: "what can you tell me about the overall design philosophy of the vode library based on what you see in `src/vode.ts`"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
Based on my analysis of `src/vode.ts`, here's the overall design philosophy of the vode library:
|
|
67
|
+
|
|
68
|
+
### Core Design Philosophy
|
|
69
|
+
|
|
70
|
+
**Functional Virtual DOM with Minimal Abstractions**
|
|
71
|
+
- Uses array-based virtual nodes (`[tag, props?, ...children]`) instead of object-based structures
|
|
72
|
+
- Prioritizes simplicity and performance over complex abstractions
|
|
73
|
+
|
|
74
|
+
**State-Centric Architecture**
|
|
75
|
+
- Central state object with patch-based updates
|
|
76
|
+
- Immutable-style state management with deep merging
|
|
77
|
+
- Single source of truth pattern
|
|
78
|
+
|
|
79
|
+
**Zero-Dependency Minimalism**
|
|
80
|
+
- Pure TypeScript/JavaScript implementation
|
|
81
|
+
- No external dependencies or build tools required
|
|
82
|
+
- Direct DOM manipulation without heavy runtime
|
|
83
|
+
|
|
84
|
+
### Key Architectural Decisions
|
|
85
|
+
|
|
86
|
+
#### Virtual DOM Structure
|
|
87
|
+
```typescript
|
|
88
|
+
// Simple array-based nodes
|
|
89
|
+
["div", { class: "container" }, "Hello", ["span", "World"]]
|
|
90
|
+
```
|
|
91
|
+
- Minimal memory footprint
|
|
92
|
+
- Easy serialization/debugging
|
|
93
|
+
- Type-safe with TypeScript generics
|
|
94
|
+
|
|
95
|
+
#### Reactive State Management
|
|
96
|
+
```typescript
|
|
97
|
+
state.patch(newData) // Direct updates
|
|
98
|
+
state.patch([effect, payload]) // Function-based effects
|
|
99
|
+
state.patch(promise) // Async support
|
|
100
|
+
```
|
|
101
|
+
- Unified patch system for all state changes
|
|
102
|
+
- Built-in async/generator support
|
|
103
|
+
- Functional effects pattern
|
|
104
|
+
|
|
105
|
+
#### Performance Optimizations
|
|
106
|
+
- **Memoization**: `memo()` for component caching
|
|
107
|
+
- **Reconciliation**: Intelligent DOM diffing
|
|
108
|
+
- **Batched Updates**: RequestAnimationFrame-based rendering
|
|
109
|
+
- **Memory Management**: Proper cleanup with mount/unmount hooks
|
|
110
|
+
|
|
111
|
+
#### Developer Experience
|
|
112
|
+
- **Type Safety**: Full TypeScript support with generics
|
|
113
|
+
- **Debugging**: Built-in performance statistics
|
|
114
|
+
- **Flexibility**: Support for HTML, SVG, and MathML
|
|
115
|
+
- **Composability**: Functional component model
|
|
116
|
+
|
|
117
|
+
### Philosophy Summary
|
|
118
|
+
|
|
119
|
+
Vode follows a **"less is more"** philosophy, providing a lightweight alternative to heavyweight frameworks while maintaining modern reactive patterns. It emphasizes:
|
|
120
|
+
|
|
121
|
+
1. **Explicitness over magic** - Clear, predictable behavior
|
|
122
|
+
2. **Performance by design** - Minimal overhead, efficient updates
|
|
123
|
+
3. **Developer control** - Direct state manipulation without hidden abstractions
|
|
124
|
+
4. **Functional paradigms** - Immutable updates, pure components
|
|
125
|
+
5. **Modern web standards** - Leverages native browser APIs effectively
|
|
126
|
+
|
|
127
|
+
The library appears designed for developers who want React-like reactivity without the complexity and bundle size of modern frameworks.
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ryupold/vode",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Small web framework for minimal websites",
|
|
5
|
+
"author": "Michael Scherbakow (ryupold)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"web",
|
|
9
|
+
"frontend",
|
|
10
|
+
"state",
|
|
11
|
+
"minimal",
|
|
12
|
+
"framework",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/ryupold/vode.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/ryupold/vode/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/ryupold/vode#readme",
|
|
23
|
+
"main": "index.js",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clean": "tsc -b --clean",
|
|
26
|
+
"build": "tsc -b",
|
|
27
|
+
"watch": "tsc -b -w"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^5.8.3"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/api-call.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Dispatch, Effect } from "./vode.js";
|
|
2
|
+
|
|
3
|
+
export const aborted = new Error("aborted");
|
|
4
|
+
|
|
5
|
+
export const metricDefaults = () => ({
|
|
6
|
+
metrics: {
|
|
7
|
+
requestCount: 0,
|
|
8
|
+
download: 0,
|
|
9
|
+
upload: 0,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Make HTTP Request
|
|
16
|
+
* @param {ApiCallOptions} options
|
|
17
|
+
* @param {Dispatch?} dispatch
|
|
18
|
+
* @returns {Promise<any>} promise wrapping the http request resolving in the response if status < 400, otherwise rejecting with the response
|
|
19
|
+
*/
|
|
20
|
+
export function httpRequest<S extends object | unknown>(options: {
|
|
21
|
+
url: string,
|
|
22
|
+
method: string,
|
|
23
|
+
headers?: Record<string, string>,
|
|
24
|
+
data?: any,
|
|
25
|
+
timeout?: number,
|
|
26
|
+
withCredentials?: boolean,
|
|
27
|
+
metrics?: {
|
|
28
|
+
requestCount: number,
|
|
29
|
+
download: number,
|
|
30
|
+
upload: number,
|
|
31
|
+
},
|
|
32
|
+
}, dispatch?: {
|
|
33
|
+
patch: Dispatch<S>,
|
|
34
|
+
action?: Effect<S>,
|
|
35
|
+
abortAction?: Effect<S>,
|
|
36
|
+
errAction?: Effect<S>,
|
|
37
|
+
progressAction?: Effect<S>,
|
|
38
|
+
uploadProgressAction?: Effect<S>
|
|
39
|
+
}): Promise<any> & { abort: () => void } {
|
|
40
|
+
const xhr = new XMLHttpRequest();
|
|
41
|
+
const promise = new Promise((resolve, reject) => {
|
|
42
|
+
if (!options.metrics) options.metrics = metricDefaults().metrics;
|
|
43
|
+
if (options.metrics) options.metrics.requestCount++;
|
|
44
|
+
|
|
45
|
+
xhr.withCredentials = options.withCredentials === undefined || options.withCredentials;
|
|
46
|
+
if (options.timeout) xhr.timeout = options.timeout;
|
|
47
|
+
|
|
48
|
+
xhr.addEventListener("timeout", (err) => {
|
|
49
|
+
reject(err || new Error("timeout"));
|
|
50
|
+
if (dispatch?.errAction) {
|
|
51
|
+
dispatch.patch([dispatch.errAction, err]);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (dispatch?.uploadProgressAction) {
|
|
56
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
57
|
+
if (event.lengthComputable) {
|
|
58
|
+
if (options.metrics && event.loaded) options.metrics.upload += event.loaded;
|
|
59
|
+
dispatch.patch([dispatch.uploadProgressAction,
|
|
60
|
+
{ loaded: event.loaded, total: event.total }]);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (dispatch?.progressAction) {
|
|
66
|
+
xhr.addEventListener("progress", (event) => {
|
|
67
|
+
console.log("progress", event);
|
|
68
|
+
if (event.lengthComputable) {
|
|
69
|
+
if (options.metrics && event.loaded) options.metrics.download += event.loaded;
|
|
70
|
+
dispatch.patch([dispatch.progressAction, event]);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
xhr.addEventListener("loadend", (e: ProgressEvent<XMLHttpRequestEventTarget>) => {
|
|
76
|
+
if (xhr.readyState === 4) {
|
|
77
|
+
if (options.metrics && (e.loaded)) {
|
|
78
|
+
options.metrics.download += e.loaded;
|
|
79
|
+
}
|
|
80
|
+
if (xhr.status < 400) {
|
|
81
|
+
resolve(xhr.response);
|
|
82
|
+
dispatch?.action && dispatch.patch(<Effect<S>>[dispatch.action, xhr.response]);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
reject({ status: xhr.status, statusText: xhr.statusText, response: xhr.response });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
xhr.addEventListener("abort", () => {
|
|
93
|
+
reject(aborted);
|
|
94
|
+
if (dispatch?.abortAction) {
|
|
95
|
+
dispatch.patch([dispatch.abortAction, aborted]);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
xhr.addEventListener("error", (err) => {
|
|
100
|
+
reject(err);
|
|
101
|
+
if (dispatch?.errAction) {
|
|
102
|
+
dispatch.patch([dispatch.errAction, err]);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
xhr.open(options.method, options.url, true);
|
|
107
|
+
if (options.headers) {
|
|
108
|
+
for (const key in options.headers) {
|
|
109
|
+
xhr.setRequestHeader(key, options.headers[key]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
xhr.send(options.data);
|
|
113
|
+
});
|
|
114
|
+
(<any>promise).abort = () => xhr.abort();
|
|
115
|
+
return <any>promise;
|
|
116
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DeepPartial } from "./vode.js";
|
|
2
|
+
|
|
3
|
+
type KeyPath<ObjectType extends object> =
|
|
4
|
+
{ [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
|
|
5
|
+
? `${Key}` | `${Key}.${KeyPath<ObjectType[Key]>}`
|
|
6
|
+
: `${Key}`
|
|
7
|
+
}[keyof ObjectType & (string | number)];
|
|
8
|
+
|
|
9
|
+
/** put a value deep inside an object addressed by a key path (creating necessary structure on the way). if target is null, a new object is created */
|
|
10
|
+
export function put<O extends object | unknown>(keyPath: O extends object ? KeyPath<O> : string, value: any = undefined, target: DeepPartial<O> | null = null) {
|
|
11
|
+
if (!target) target = {} as O as any;
|
|
12
|
+
|
|
13
|
+
const keys = keyPath.split('.');
|
|
14
|
+
if (keys.length > 1) {
|
|
15
|
+
let i = 0;
|
|
16
|
+
let raw = (<any>target)[keys[i]];
|
|
17
|
+
if (raw === undefined) {
|
|
18
|
+
(<any>target)[keys[i]] = raw = {};
|
|
19
|
+
}
|
|
20
|
+
for (i = 1; i < keys.length - 1; i++) {
|
|
21
|
+
const p = raw;
|
|
22
|
+
raw = raw[keys[i]];
|
|
23
|
+
if (raw === undefined) {
|
|
24
|
+
raw = {};
|
|
25
|
+
p[keys[i]] = raw;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (keys[i] === undefined) console.log(keyPath);
|
|
29
|
+
raw[keys[i]] = value;
|
|
30
|
+
} else {
|
|
31
|
+
(<any>target)[keys[0]] = value;
|
|
32
|
+
}
|
|
33
|
+
return target
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** get a value deep inside an object by its key path */
|
|
37
|
+
export function get<O extends object | unknown>(keyPath: O extends object ? KeyPath<O> : string, source: DeepPartial<O>) {
|
|
38
|
+
const keys = keyPath.split('.');
|
|
39
|
+
let raw = source ? (<any>source)[keys[0]] : undefined;
|
|
40
|
+
for (let i = 1; i < keys.length && !!raw; i++) {
|
|
41
|
+
raw = raw[keys[i]];
|
|
42
|
+
}
|
|
43
|
+
return raw;
|
|
44
|
+
}
|
package/src/html.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Props, Vode } from "./vode.js";
|
|
2
|
+
|
|
3
|
+
export function htmlToVode<S extends object | unknown>(html: string): (Vode<S> | string)[] {
|
|
4
|
+
const div = document.createElement('div');
|
|
5
|
+
div.innerHTML = html.trim();
|
|
6
|
+
|
|
7
|
+
const vodes: (Vode<S> | string)[] = [];
|
|
8
|
+
for (const child of div.childNodes) {
|
|
9
|
+
const v = elementToVode<S>(<Element>child);
|
|
10
|
+
if (v != null) vodes.push(v);
|
|
11
|
+
}
|
|
12
|
+
return vodes;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function elementToVode<S>(element: Element): Vode<S> | string | undefined | null {
|
|
16
|
+
if (element.nodeType === Node.TEXT_NODE) {
|
|
17
|
+
return element.textContent;
|
|
18
|
+
}
|
|
19
|
+
if (element.nodeType !== Node.ELEMENT_NODE) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const vode = <Vode<S>>[element.tagName.toLowerCase()];
|
|
23
|
+
|
|
24
|
+
if (element.hasAttributes()) {
|
|
25
|
+
const props = <Props<S>>{};
|
|
26
|
+
for (const att of element.attributes) {
|
|
27
|
+
props[att.name] = att.value;
|
|
28
|
+
}
|
|
29
|
+
(<any[]>vode).push(props);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const child of element.childNodes) {
|
|
33
|
+
const v = elementToVode(<Element>child);
|
|
34
|
+
if (v && (typeof v !== "string" || v.length > 0)) (<any[]>vode).push(v);
|
|
35
|
+
}
|
|
36
|
+
return vode;
|
|
37
|
+
}
|
package/src/style.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StyleProp } from "./vode.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
function generateCSS(style: StyleProp) {
|
|
5
|
+
let css = '';
|
|
6
|
+
for (const key in style) {
|
|
7
|
+
const value = style[key];
|
|
8
|
+
//transform camelCase to kebab-case
|
|
9
|
+
const kebab = key.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();
|
|
10
|
+
css += `${kebab}:${value};`;
|
|
11
|
+
}
|
|
12
|
+
return css;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function keyToKebab(key: string) : string {
|
|
16
|
+
return key.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();
|
|
17
|
+
}
|
package/src/vode-tags.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Tag } from "./vode.js";
|
|
2
|
+
|
|
3
|
+
//=== HTML ========================================================================================
|
|
4
|
+
export const A: Tag = "a";
|
|
5
|
+
export const ABBR: Tag = "abbr";
|
|
6
|
+
export const ADDRESS: Tag = "address";
|
|
7
|
+
export const AREA: Tag = "area";
|
|
8
|
+
export const ARTICLE: Tag = "article";
|
|
9
|
+
export const ASIDE: Tag = "aside";
|
|
10
|
+
export const AUDIO: Tag = "audio";
|
|
11
|
+
export const B: Tag = "b";
|
|
12
|
+
export const BASE: Tag = "base";
|
|
13
|
+
export const BDI: Tag = "bdi";
|
|
14
|
+
export const BDO: Tag = "bdo";
|
|
15
|
+
export const BLOCKQUOTE: Tag = "blockquote";
|
|
16
|
+
export const BODY: Tag = "body";
|
|
17
|
+
export const BR: Tag = "br";
|
|
18
|
+
export const BUTTON: Tag = "button";
|
|
19
|
+
export const CANVAS: Tag = "canvas";
|
|
20
|
+
export const CAPTION: Tag = "caption";
|
|
21
|
+
export const CITE: Tag = "cite";
|
|
22
|
+
export const CODE: Tag = "code";
|
|
23
|
+
export const COL: Tag = "col";
|
|
24
|
+
export const COLGROUP: Tag = "colgroup";
|
|
25
|
+
export const DATA: Tag = "data";
|
|
26
|
+
export const DATALIST: Tag = "datalist";
|
|
27
|
+
export const DD: Tag = "dd";
|
|
28
|
+
export const DEL: Tag = "del";
|
|
29
|
+
export const DETAILS: Tag = "details";
|
|
30
|
+
export const DFN: Tag = "dfn";
|
|
31
|
+
export const DIALOG: Tag = "dialog";
|
|
32
|
+
export const DIV: Tag = "div";
|
|
33
|
+
export const DL: Tag = "dl";
|
|
34
|
+
export const DT: Tag = "dt";
|
|
35
|
+
export const EM: Tag = "em";
|
|
36
|
+
export const EMBED: Tag = "embed";
|
|
37
|
+
export const FIELDSET: Tag = "fieldset";
|
|
38
|
+
export const FIGCAPTION: Tag = "figcaption";
|
|
39
|
+
export const FIGURE: Tag = "figure";
|
|
40
|
+
export const FOOTER: Tag = "footer";
|
|
41
|
+
export const FORM: Tag = "form";
|
|
42
|
+
export const H1: Tag = "h1";
|
|
43
|
+
export const H2: Tag = "h2";
|
|
44
|
+
export const H3: Tag = "h3";
|
|
45
|
+
export const H4: Tag = "h4";
|
|
46
|
+
export const H5: Tag = "h5";
|
|
47
|
+
export const H6: Tag = "h6";
|
|
48
|
+
export const HEAD: Tag = "head";
|
|
49
|
+
export const HEADER: Tag = "header";
|
|
50
|
+
export const HGROUP: Tag = "hgroup";
|
|
51
|
+
export const HR: Tag = "hr";
|
|
52
|
+
export const HTML: Tag = "html";
|
|
53
|
+
export const I: Tag = "i";
|
|
54
|
+
export const IFRAME: Tag = "iframe";
|
|
55
|
+
export const IMG: Tag = "img";
|
|
56
|
+
export const INPUT: Tag = "input";
|
|
57
|
+
export const INS: Tag = "ins";
|
|
58
|
+
export const KBD: Tag = "kbd";
|
|
59
|
+
export const LABEL: Tag = "label";
|
|
60
|
+
export const LEGEND: Tag = "legend";
|
|
61
|
+
export const LI: Tag = "li";
|
|
62
|
+
export const LINK: Tag = "link";
|
|
63
|
+
export const MAIN: Tag = "main";
|
|
64
|
+
export const MAP: Tag = "map";
|
|
65
|
+
export const MARK: Tag = "mark";
|
|
66
|
+
export const MENU: Tag = "menu";
|
|
67
|
+
export const META: Tag = "meta";
|
|
68
|
+
export const METER: Tag = "meter";
|
|
69
|
+
export const NAV: Tag = "nav";
|
|
70
|
+
export const NOSCRIPT: Tag = "noscript";
|
|
71
|
+
export const OBJECT: Tag = "object";
|
|
72
|
+
export const OL: Tag = "ol";
|
|
73
|
+
export const OPTGROUP: Tag = "optgroup";
|
|
74
|
+
export const OPTION: Tag = "option";
|
|
75
|
+
export const OUTPUT: Tag = "output";
|
|
76
|
+
export const P: Tag = "p";
|
|
77
|
+
export const PICTURE: Tag = "picture";
|
|
78
|
+
export const PRE: Tag = "pre";
|
|
79
|
+
export const PROGRESS: Tag = "progress";
|
|
80
|
+
export const Q: Tag = "q";
|
|
81
|
+
export const RP: Tag = "rp";
|
|
82
|
+
export const RT: Tag = "rt";
|
|
83
|
+
export const RUBY: Tag = "ruby";
|
|
84
|
+
export const S: Tag = "s";
|
|
85
|
+
export const SAMP: Tag = "samp";
|
|
86
|
+
export const SCRIPT: Tag = "script";
|
|
87
|
+
export const SECTION: Tag = "section";
|
|
88
|
+
export const SELECT: Tag = "select";
|
|
89
|
+
export const SLOT: Tag = "slot";
|
|
90
|
+
export const SMALL: Tag = "small";
|
|
91
|
+
export const SOURCE: Tag = "source";
|
|
92
|
+
export const SPAN: Tag = "span";
|
|
93
|
+
export const STRONG: Tag = "strong";
|
|
94
|
+
export const STYLE: Tag = "style";
|
|
95
|
+
export const SUB: Tag = "sub";
|
|
96
|
+
export const SUMMARY: Tag = "summary";
|
|
97
|
+
export const SUP: Tag = "sup";
|
|
98
|
+
export const TABLE: Tag = "table";
|
|
99
|
+
export const TBODY: Tag = "tbody";
|
|
100
|
+
export const TD: Tag = "td";
|
|
101
|
+
export const TEMPLATE: Tag = "template";
|
|
102
|
+
export const TEXTAREA: Tag = "textarea";
|
|
103
|
+
export const TFOOT: Tag = "tfoot";
|
|
104
|
+
export const TH: Tag = "th";
|
|
105
|
+
export const THEAD: Tag = "thead";
|
|
106
|
+
export const TIME: Tag = "time";
|
|
107
|
+
export const TITLE: Tag = "title";
|
|
108
|
+
export const TR: Tag = "tr";
|
|
109
|
+
export const TRACK: Tag = "track";
|
|
110
|
+
export const U: Tag = "u";
|
|
111
|
+
export const UL: Tag = "ul";
|
|
112
|
+
export const VIDEO: Tag = "video";
|
|
113
|
+
export const WBR: Tag = "wbr";
|
|
114
|
+
|
|
115
|
+
//=== SVG =========================================================================================
|
|
116
|
+
export const ANIMATE: Tag = "animate";
|
|
117
|
+
export const ANIMATEMOTION: Tag = "animateMotion";
|
|
118
|
+
export const ANIMATETRANSFORM: Tag = "animateTransform";
|
|
119
|
+
export const CIRCLE: Tag = "circle";
|
|
120
|
+
export const CLIPPATH: Tag = "clipPath";
|
|
121
|
+
export const DEFS: Tag = "defs";
|
|
122
|
+
export const DESC: Tag = "desc";
|
|
123
|
+
export const ELLIPSE: Tag = "ellipse";
|
|
124
|
+
export const FEBLEND: Tag = "feBlend";
|
|
125
|
+
export const FECOLORMATRIX: Tag = "feColorMatrix";
|
|
126
|
+
export const FECOMPONENTTRANSFER: Tag = "feComponentTransfer";
|
|
127
|
+
export const FECOMPOSITE: Tag = "feComposite";
|
|
128
|
+
export const FECONVOLVEMATRIX: Tag = "feConvolveMatrix";
|
|
129
|
+
export const FEDIFFUSELIGHTING: Tag = "feDiffuseLighting";
|
|
130
|
+
export const FEDISPLACEMENTMAP: Tag = "feDisplacementMap";
|
|
131
|
+
export const FEDISTANTLIGHT: Tag = "feDistantLight";
|
|
132
|
+
export const FEDROPSHADOW: Tag = "feDropShadow";
|
|
133
|
+
export const FEFLOOD: Tag = "feFlood";
|
|
134
|
+
export const FEFUNCA: Tag = "feFuncA";
|
|
135
|
+
export const FEFUNCB: Tag = "feFuncB";
|
|
136
|
+
export const FEFUNCG: Tag = "feFuncG";
|
|
137
|
+
export const FEFUNCR: Tag = "feFuncR";
|
|
138
|
+
export const FEGAUSSIANBLUR: Tag = "feGaussianBlur";
|
|
139
|
+
export const FEIMAGE: Tag = "feImage";
|
|
140
|
+
export const FEMERGE: Tag = "feMerge";
|
|
141
|
+
export const FEMERGENODE: Tag = "feMergeNode";
|
|
142
|
+
export const FEMORPHOLOGY: Tag = "feMorphology";
|
|
143
|
+
export const FEOFFSET: Tag = "feOffset";
|
|
144
|
+
export const FEPOINTLIGHT: Tag = "fePointLight";
|
|
145
|
+
export const FESPECULARLIGHTING: Tag = "feSpecularLighting";
|
|
146
|
+
export const FESPOTLIGHT: Tag = "feSpotLight";
|
|
147
|
+
export const FETILE: Tag = "feTile";
|
|
148
|
+
export const FETURBULENCE: Tag = "feTurbulence";
|
|
149
|
+
export const FILTER: Tag = "filter";
|
|
150
|
+
export const FOREIGNOBJECT: Tag = "foreignObject";
|
|
151
|
+
export const G: Tag = "g";
|
|
152
|
+
export const IMAGE: Tag = "image";
|
|
153
|
+
export const LINE: Tag = "line";
|
|
154
|
+
export const LINEARGRADIENT: Tag = "linearGradient";
|
|
155
|
+
export const MARKER: Tag = "marker";
|
|
156
|
+
export const MASK: Tag = "mask";
|
|
157
|
+
export const METADATA: Tag = "metadata";
|
|
158
|
+
export const MPATH: Tag = "mpath";
|
|
159
|
+
export const PATH: Tag = "path";
|
|
160
|
+
export const PATTERN: Tag = "pattern";
|
|
161
|
+
export const POLYGON: Tag = "polygon";
|
|
162
|
+
export const POLYLINE: Tag = "polyline";
|
|
163
|
+
export const RADIALGRADIENT: Tag = "radialGradient";
|
|
164
|
+
export const RECT: Tag = "rect";
|
|
165
|
+
export const SET: Tag = "set";
|
|
166
|
+
export const STOP: Tag = "stop";
|
|
167
|
+
export const SVG: Tag = "svg";
|
|
168
|
+
export const SWITCH: Tag = "switch";
|
|
169
|
+
export const SYMBOL: Tag = "symbol";
|
|
170
|
+
export const TEXT: Tag = "text";
|
|
171
|
+
export const TEXTPATH: Tag = "textPath";
|
|
172
|
+
export const TSPAN: Tag = "tspan";
|
|
173
|
+
export const USE: Tag = "use";
|
|
174
|
+
export const VIEW: Tag = "view";
|
|
175
|
+
|
|
176
|
+
//=== MathML ======================================================================================
|
|
177
|
+
|
|
178
|
+
export const ANNOTATION: Tag = "annotation";
|
|
179
|
+
export const ANNOTATION_XML: Tag = "annotation-xml";
|
|
180
|
+
export const MACTION: Tag = "maction";
|
|
181
|
+
export const MATH: Tag = "math";
|
|
182
|
+
export const MERROR: Tag = "merror";
|
|
183
|
+
export const MFRAC: Tag = "mfrac";
|
|
184
|
+
export const MI: Tag = "mi";
|
|
185
|
+
export const MMULTISCRIPTS: Tag = "mmultiscripts";
|
|
186
|
+
export const MN: Tag = "mn";
|
|
187
|
+
export const MO: Tag = "mo";
|
|
188
|
+
export const MOVER: Tag = "mover";
|
|
189
|
+
export const MPADDED: Tag = "mpadded";
|
|
190
|
+
export const MPHANTOM: Tag = "mphantom";
|
|
191
|
+
export const MPRESCRIPTS: Tag = "mprescripts";
|
|
192
|
+
export const MROOT: Tag = "mroot";
|
|
193
|
+
export const MROW: Tag = "mrow";
|
|
194
|
+
export const MS: Tag = "ms";
|
|
195
|
+
export const MSPACE: Tag = "mspace";
|
|
196
|
+
export const MSQRT: Tag = "msqrt";
|
|
197
|
+
export const MSTYLE: Tag = "mstyle";
|
|
198
|
+
export const MSUB: Tag = "msub";
|
|
199
|
+
export const MSUBSUP: Tag = "msubsup";
|
|
200
|
+
export const MSUP: Tag = "msup";
|
|
201
|
+
export const MTABLE: Tag = "mtable";
|
|
202
|
+
export const MTD: Tag = "mtd";
|
|
203
|
+
export const MTEXT: Tag = "mtext";
|
|
204
|
+
export const MTR: Tag = "mtr";
|
|
205
|
+
export const MUNDER: Tag = "munder";
|
|
206
|
+
export const MUNDEROVER: Tag = "munderover";
|
|
207
|
+
export const SEMANTICS: Tag = "semantics";
|
package/src/vode.ts
ADDED
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
export type Vode<S> = FullVode<S> | JustTagVode | NoPropsVode<S>;
|
|
2
|
+
export type ChildVode<S> = Vode<S> | TextVode | NoVode | Component<S>;
|
|
3
|
+
export type FullVode<S> = [tag: Tag, props: Props<S>, ...children: ChildVode<S>[]];
|
|
4
|
+
export type NoPropsVode<S> = [tag: Tag, ...children: ChildVode<S>[]] | string[];
|
|
5
|
+
export type JustTagVode = [tag: Tag];
|
|
6
|
+
export type TextVode = string;
|
|
7
|
+
export type NoVode = undefined | null | number | boolean | bigint | void;
|
|
8
|
+
export type AttachedVode<S> = Vode<S> & { node: ChildNode, id?: string } | Text & { node?: never, id?: never };
|
|
9
|
+
export type Tag = keyof (HTMLElementTagNameMap & SVGElementTagNameMap & MathMLElementTagNameMap);
|
|
10
|
+
export type Component<S> = (s: S) => ChildVode<S>;
|
|
11
|
+
|
|
12
|
+
export type Patch<S> =
|
|
13
|
+
| NoRenderPatch // ignored
|
|
14
|
+
| typeof EmptyPatch | DeepPartial<S> // render patches
|
|
15
|
+
| Promise<Patch<S>> | Effect<S> // effects resulting in patches
|
|
16
|
+
|
|
17
|
+
export const EmptyPatch = {} as const; // smallest patch to cause a render without any changes
|
|
18
|
+
export type NoRenderPatch = undefined | null | number | boolean | bigint | string | symbol | void;
|
|
19
|
+
|
|
20
|
+
export type DeepPartial<S> = { [P in keyof S]?: S[P] extends Array<infer I> ? Array<Patch<I>> : Patch<S[P]> };
|
|
21
|
+
|
|
22
|
+
export type Effect<S> =
|
|
23
|
+
| (() => Patch<S>)
|
|
24
|
+
| EffectFunction<S>
|
|
25
|
+
| [effect: EffectFunction<S>, ...args: any[]]
|
|
26
|
+
| Generator<Patch<S>, unknown, void>
|
|
27
|
+
| AsyncGenerator<Patch<S>, unknown, void>;
|
|
28
|
+
|
|
29
|
+
export type EffectFunction<S> = (state: S, ...args: any[]) => Patch<S>;
|
|
30
|
+
|
|
31
|
+
export type Dispatch<S> = (action: Patch<S>) => void;
|
|
32
|
+
export type PatchableState<S> = S & { patch: Dispatch<Patch<S>> };
|
|
33
|
+
|
|
34
|
+
export type Props<S> = Partial<
|
|
35
|
+
Omit<HTMLElement,
|
|
36
|
+
keyof (DocumentFragment & ElementCSSInlineStyle & GlobalEventHandlers)> &
|
|
37
|
+
{ [K in keyof EventsMap]: Patch<S> } // all on* events
|
|
38
|
+
> & {
|
|
39
|
+
[_: string]: unknown,
|
|
40
|
+
class?: ClassProp,
|
|
41
|
+
style?: StyleProp,
|
|
42
|
+
/** called after the element was attached */
|
|
43
|
+
onMount?: MountFunction<S>,
|
|
44
|
+
/** called before the element is detached */
|
|
45
|
+
onUnmount?: MountFunction<S>,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type MountFunction<S> =
|
|
49
|
+
| ((s: S, node: HTMLElement) => Patch<S>)
|
|
50
|
+
| ((s: S, node: SVGSVGElement) => Patch<S>)
|
|
51
|
+
| ((s: S, node: MathMLElement) => Patch<S>)
|
|
52
|
+
|
|
53
|
+
export type ClassProp =
|
|
54
|
+
| "" | false | null | undefined // no class
|
|
55
|
+
| string // "class1 class2"
|
|
56
|
+
| string[] // ["class1", "class2"]
|
|
57
|
+
| Record<string, boolean | undefined | null> // { class1: true, class2: false }
|
|
58
|
+
|
|
59
|
+
export type StyleProp = Record<number, never> & {
|
|
60
|
+
[K in keyof CSSStyleDeclaration]?: CSSStyleDeclaration[K] | null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type EventsMap =
|
|
64
|
+
& { [K in keyof HTMLElementEventMap as `on${K}`]: HTMLElementEventMap[K] }
|
|
65
|
+
& { [K in keyof WindowEventMap as `on${K}`]: WindowEventMap[K] }
|
|
66
|
+
& { [K in keyof SVGElementEventMap as `on${K}`]: SVGElementEventMap[K] }
|
|
67
|
+
& { onsearch: Event }
|
|
68
|
+
|
|
69
|
+
export type PropertyValue<S> = string | boolean | null | undefined | StyleProp | ClassProp | Patch<S> | void;
|
|
70
|
+
|
|
71
|
+
export type ContainerNode<S> = HTMLElement & {
|
|
72
|
+
state: PatchableState<S>,
|
|
73
|
+
vode: AttachedVode<S>,
|
|
74
|
+
patch: Dispatch<S>,
|
|
75
|
+
render: () => void,
|
|
76
|
+
q: Patch<S>[]
|
|
77
|
+
isRendering: boolean,
|
|
78
|
+
stats: {
|
|
79
|
+
patchCount: number,
|
|
80
|
+
liveEffectCount: number,
|
|
81
|
+
renderPatchCount: number,
|
|
82
|
+
renderCount: number,
|
|
83
|
+
renderTime: number,
|
|
84
|
+
queueLengthBeforeRender: number,
|
|
85
|
+
queueLengthAfterRender: number,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** type-safe way to create a vode. useful for type inference and autocompletion.
|
|
90
|
+
*
|
|
91
|
+
* overloads:
|
|
92
|
+
* - just a tag: `vode("div") // => ["div"]`
|
|
93
|
+
* - tag and props: `vode("div", { class: "foo" }) // => ["div", { class: "foo" }]`
|
|
94
|
+
* - tag, props and children: `vode("div", { class: "foo" }, ["span", "bar"]) // => ["div", { class: "foo" }, ["span", "bar"]]`
|
|
95
|
+
* - identity: `vode(["div", ["span", "bar"]]) // => ["div", ["span", "bar"]]`
|
|
96
|
+
*/
|
|
97
|
+
export function vode<S extends object | unknown>(tag: Tag | Vode<S>, props?: Props<S> | ChildVode<S>, ...children: ChildVode<S>[]): Vode<S> {
|
|
98
|
+
if (Array.isArray(tag)) {
|
|
99
|
+
return tag;
|
|
100
|
+
}
|
|
101
|
+
if (props) {
|
|
102
|
+
return [tag, props as Props<S>, ...children];
|
|
103
|
+
}
|
|
104
|
+
return [tag, ...children];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** pass an object whose type determines the initial state */
|
|
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
|
|
116
|
+
* @param container will use this container as root and places the result of the dom function and further renderings in it
|
|
117
|
+
* @param initialState @see createState
|
|
118
|
+
* @param dom creates the initial dom from the state and is called on every render
|
|
119
|
+
* @param initialPatches variadic list of patches that are applied after the first render
|
|
120
|
+
* @returns a patch function that can be used to update the state
|
|
121
|
+
*/
|
|
122
|
+
export function app<S>(container: HTMLElement, initialState: Omit<S, "patch">, dom: Component<S>, ...initialPatches: Patch<S>[]) {
|
|
123
|
+
const root = container as ContainerNode<S>;
|
|
124
|
+
root.stats = { renderTime: 0, renderCount: 0, queueLengthBeforeRender: 0, queueLengthAfterRender: 0, liveEffectCount: 0, patchCount: 0, renderPatchCount: 0 };
|
|
125
|
+
|
|
126
|
+
Object.defineProperty(initialState, "patch", {
|
|
127
|
+
enumerable: false, configurable: true,
|
|
128
|
+
writable: false, value: async (action: Patch<S>) => {
|
|
129
|
+
if (!action || (typeof action !== "function" && typeof action !== "object")) return;
|
|
130
|
+
root.stats.patchCount++;
|
|
131
|
+
|
|
132
|
+
if ((action as AsyncGenerator<Patch<S>, unknown, void>)?.next) {
|
|
133
|
+
const generator = action as AsyncGenerator<Patch<S>, unknown, void>;
|
|
134
|
+
root.stats.liveEffectCount++;
|
|
135
|
+
try {
|
|
136
|
+
let v = await generator.next();
|
|
137
|
+
while (v.done === false) {
|
|
138
|
+
root.stats.liveEffectCount++;
|
|
139
|
+
try {
|
|
140
|
+
root.patch!(v.value);
|
|
141
|
+
v = await generator.next();
|
|
142
|
+
} finally {
|
|
143
|
+
root.stats.liveEffectCount--;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
root.patch!(v.value as Patch<S>);
|
|
147
|
+
} finally {
|
|
148
|
+
root.stats.liveEffectCount--;
|
|
149
|
+
}
|
|
150
|
+
} else if ((action as Promise<S>).then) {
|
|
151
|
+
root.stats.liveEffectCount++;
|
|
152
|
+
try {
|
|
153
|
+
const nextState = await (action as Promise<S>);
|
|
154
|
+
root.patch!(<Patch<S>>nextState);
|
|
155
|
+
} finally {
|
|
156
|
+
root.stats.liveEffectCount--;
|
|
157
|
+
}
|
|
158
|
+
} else if (Array.isArray(action)) {
|
|
159
|
+
if (typeof action[0] === "function") {
|
|
160
|
+
if (action.length > 1)
|
|
161
|
+
root.patch!(action[0](root.state!, ...(action as any[]).slice(1)));
|
|
162
|
+
else root.patch!(action[0](root.state!));
|
|
163
|
+
} else {
|
|
164
|
+
root.stats.patchCount--;
|
|
165
|
+
}
|
|
166
|
+
} else if (typeof action === "function") {
|
|
167
|
+
root.patch!((<EffectFunction<S>>action)(root.state));
|
|
168
|
+
} else {
|
|
169
|
+
root.stats.renderPatchCount++;
|
|
170
|
+
root.q!.push(<Patch<S>>action);
|
|
171
|
+
if (!root.isRendering) root.render!();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
Object.defineProperty(root, "render", {
|
|
177
|
+
enumerable: false, configurable: true,
|
|
178
|
+
writable: false, value: () => requestAnimationFrame(() => {
|
|
179
|
+
if (root.isRendering || root.q!.length === 0) return;
|
|
180
|
+
root.isRendering = true;
|
|
181
|
+
const sw = Date.now();
|
|
182
|
+
try {
|
|
183
|
+
root.stats.queueLengthBeforeRender = root.q!.length;
|
|
184
|
+
|
|
185
|
+
while (root.q!.length > 0) {
|
|
186
|
+
const patch = root.q!.shift();
|
|
187
|
+
if(patch === EmptyPatch) continue;
|
|
188
|
+
mergeState(root.state, patch);
|
|
189
|
+
}
|
|
190
|
+
root.vode = render(root.state, root.patch, container, 0, root.vode, dom(root.state))!;
|
|
191
|
+
} finally {
|
|
192
|
+
root.isRendering = false;
|
|
193
|
+
root.stats.renderCount++;
|
|
194
|
+
root.stats.renderTime = Date.now() - sw;
|
|
195
|
+
root.stats.queueLengthAfterRender = root.q!.length;
|
|
196
|
+
if (root.q!.length > 0) {
|
|
197
|
+
root.render!();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
root.patch = (<PatchableState<S>>initialState).patch;
|
|
204
|
+
root.state = <PatchableState<S>>initialState;
|
|
205
|
+
root.q = [];
|
|
206
|
+
const initialVode = dom(<S>initialState);
|
|
207
|
+
root.vode = <AttachedVode<S>>initialVode;
|
|
208
|
+
root.vode = render(<S>initialState, root.patch!, container, 0, undefined, initialVode)!;
|
|
209
|
+
|
|
210
|
+
for (const effect of initialPatches) {
|
|
211
|
+
root.patch!(effect);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return root.patch;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** get properties of a vode, if there are any */
|
|
218
|
+
export function props<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): Props<S> | undefined {
|
|
219
|
+
if (Array.isArray(vode)
|
|
220
|
+
&& vode.length > 1
|
|
221
|
+
&& vode[1]
|
|
222
|
+
&& !Array.isArray(vode[1])
|
|
223
|
+
) {
|
|
224
|
+
if (
|
|
225
|
+
typeof vode[1] === "object"
|
|
226
|
+
&& (vode[1] as unknown as Node).nodeType !== Node.TEXT_NODE
|
|
227
|
+
) {
|
|
228
|
+
return vode[1];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function mergeClass(a: ClassProp, b: ClassProp): ClassProp {
|
|
236
|
+
if (!a) return b;
|
|
237
|
+
if (!b) return a;
|
|
238
|
+
|
|
239
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
240
|
+
const aSplit = a.split(" ");
|
|
241
|
+
const bSplit = b.split(" ");
|
|
242
|
+
const classSet = new Set([...aSplit, ...bSplit]);
|
|
243
|
+
return Array.from(classSet).join(" ").trim();
|
|
244
|
+
}
|
|
245
|
+
else if (typeof a === "string" && Array.isArray(b)) {
|
|
246
|
+
const classSet = new Set([...b, ...a.split(" ")]);
|
|
247
|
+
return Array.from(classSet).join(" ").trim();
|
|
248
|
+
}
|
|
249
|
+
else if (Array.isArray(a) && typeof b === "string") {
|
|
250
|
+
const classSet = new Set([...a, ...b.split(" ")]);
|
|
251
|
+
return Array.from(classSet).join(" ").trim();
|
|
252
|
+
}
|
|
253
|
+
else if (Array.isArray(a) && Array.isArray(b)) {
|
|
254
|
+
const classSet = new Set([...a, ...b]);
|
|
255
|
+
return Array.from(classSet).join(" ").trim();
|
|
256
|
+
}
|
|
257
|
+
else if (typeof a === "string" && typeof b === "object") {
|
|
258
|
+
return { [a]: true, ...b };
|
|
259
|
+
}
|
|
260
|
+
else if (typeof a === "object" && typeof b === "string") {
|
|
261
|
+
return { ...a, [b]: true };
|
|
262
|
+
}
|
|
263
|
+
else if (typeof a === "object" && typeof b === "object") {
|
|
264
|
+
return { ...a, ...b };
|
|
265
|
+
} else if (typeof a === "object" && Array.isArray(b)) {
|
|
266
|
+
const aa = { ...a };
|
|
267
|
+
for (const item of b as string[]) {
|
|
268
|
+
(<Record<string, boolean | null | undefined>>aa)[item] = true;
|
|
269
|
+
}
|
|
270
|
+
return aa;
|
|
271
|
+
} else if (Array.isArray(a) && typeof b === "object") {
|
|
272
|
+
const aa: Record<string, any> = {};
|
|
273
|
+
for (const item of a as string[]) {
|
|
274
|
+
aa[item] = true;
|
|
275
|
+
}
|
|
276
|
+
for (const bKey of (<Record<string, any>>b).keys) {
|
|
277
|
+
aa[bKey] = (<Record<string, boolean | null | undefined>>b)[bKey];
|
|
278
|
+
}
|
|
279
|
+
return b;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
throw new Error(`cannot merge classes of ${a} (${typeof a}) and ${b} (${typeof b})`);
|
|
283
|
+
}
|
|
284
|
+
|
|
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
|
+
/** get a slice of all children of a vode, if there are any */
|
|
307
|
+
export function children<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): ChildVode<S>[] | undefined {
|
|
308
|
+
const start = childrenStart(vode);
|
|
309
|
+
if (start > 0) {
|
|
310
|
+
return (<Vode<S>>vode).slice(start) as Vode<S>[];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return undefined;
|
|
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;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function childCount<S>(vode: Vode<S>) { return vode.length - childrenStart(vode); }
|
|
330
|
+
|
|
331
|
+
export function child<S>(vode: Vode<S>, index: number): ChildVode<S> | undefined {
|
|
332
|
+
return vode[index + childrenStart(vode)] as ChildVode<S>;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** merge multiple objects into one, applying from left to right
|
|
336
|
+
* @param first object to merge
|
|
337
|
+
* @returns merged object
|
|
338
|
+
*/
|
|
339
|
+
export function merge(first?: any, ...p: any[]): any {
|
|
340
|
+
first = mergeState({}, first);
|
|
341
|
+
for (const pp of p) {
|
|
342
|
+
if (!pp) continue;
|
|
343
|
+
first = mergeState(first, pp);
|
|
344
|
+
}
|
|
345
|
+
return first!;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function classString(classProp: ClassProp): string {
|
|
349
|
+
if (typeof classProp === "string") {
|
|
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;
|
|
388
|
+
|
|
389
|
+
if (Array.isArray(presentMemo)
|
|
390
|
+
&& Array.isArray(pastMemo)
|
|
391
|
+
&& presentMemo.length === pastMemo.length
|
|
392
|
+
) {
|
|
393
|
+
let same = true;
|
|
394
|
+
for (let i = 0; i < presentMemo.length; i++) {
|
|
395
|
+
if (presentMemo[i] !== pastMemo[i]) {
|
|
396
|
+
same = false;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (same) return past;
|
|
401
|
+
}
|
|
402
|
+
const newRender = unwrap(present, state);
|
|
403
|
+
if (typeof newRender === "object") {
|
|
404
|
+
(<any>newRender).__memo = present?.__memo;
|
|
405
|
+
}
|
|
406
|
+
return newRender;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
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
|
+
// unwrap component if it is memoized
|
|
411
|
+
newVode = remember(state, newVode, oldVode) as ChildVode<S>;
|
|
412
|
+
|
|
413
|
+
const isNoVode = !newVode || typeof newVode === "number" || typeof newVode === "boolean";
|
|
414
|
+
if (newVode === oldVode || (!oldVode && isNoVode)) {
|
|
415
|
+
return oldVode;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const oldIsText = (oldVode as Text)?.nodeType === Node.TEXT_NODE;
|
|
419
|
+
const oldNode: ChildNode | undefined = oldIsText ? oldVode as Text : oldVode?.node;
|
|
420
|
+
|
|
421
|
+
// falsy|text|element(A) -> undefined
|
|
422
|
+
if (isNoVode) {
|
|
423
|
+
(<any>oldNode)?.onUnmount && patch((<any>oldNode).onUnmount(oldNode));
|
|
424
|
+
oldNode?.remove();
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const isText = !isNoVode && isTextVode(newVode);
|
|
429
|
+
const isNode = !isNoVode && isNaturalVode(newVode);
|
|
430
|
+
const alreadyAttached = !!newVode && typeof newVode !== "string" && !!((<any>newVode)?.node || (<any>newVode)?.nodeType === Node.TEXT_NODE);
|
|
431
|
+
|
|
432
|
+
if (!isText && !isNode && !alreadyAttached && !oldVode) {
|
|
433
|
+
throw new Error("Invalid vode: " + typeof newVode + " " + JSON.stringify(newVode));
|
|
434
|
+
}
|
|
435
|
+
else if (alreadyAttached && isText) {
|
|
436
|
+
newVode = (<Text><any>newVode).wholeText;
|
|
437
|
+
}
|
|
438
|
+
else if (alreadyAttached && isNode) {
|
|
439
|
+
newVode = [...<Vode<S>>newVode];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// text -> text
|
|
443
|
+
if (oldIsText && isText) {
|
|
444
|
+
if ((<Text>oldNode).nodeValue !== <string>newVode) {
|
|
445
|
+
(<Text>oldNode).nodeValue = <string>newVode;
|
|
446
|
+
}
|
|
447
|
+
return oldVode;
|
|
448
|
+
}
|
|
449
|
+
// falsy|element -> text
|
|
450
|
+
if (isText && (!oldNode || !oldIsText)) {
|
|
451
|
+
const text = document.createTextNode(newVode as string)
|
|
452
|
+
if (oldNode) {
|
|
453
|
+
(<any>oldNode).onUnmount && patch((<any>oldNode).onUnmount(oldNode));
|
|
454
|
+
oldNode.replaceWith(text);
|
|
455
|
+
} else {
|
|
456
|
+
if (parent.childNodes[childIndex]) {
|
|
457
|
+
parent.insertBefore(text, parent.childNodes[childIndex]);
|
|
458
|
+
} else {
|
|
459
|
+
parent.appendChild(text);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return text as Text;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// falsy|text|element(A) -> element(B)
|
|
467
|
+
if (
|
|
468
|
+
(isNode && (!oldNode || oldIsText || (<Vode<S>>oldVode)[0] !== (<Vode<S>>newVode)[0]))
|
|
469
|
+
) {
|
|
470
|
+
svg = svg || (<Vode<S>>newVode)[0] === "svg";
|
|
471
|
+
const newNode: ChildNode = svg
|
|
472
|
+
? document.createElementNS("http://www.w3.org/2000/svg", (<Vode<S>>newVode)[0])
|
|
473
|
+
: document.createElement((<Vode<S>>newVode)[0]);
|
|
474
|
+
(<AttachedVode<S>>newVode).node = newNode;
|
|
475
|
+
|
|
476
|
+
const newvode = <Vode<S>>newVode;
|
|
477
|
+
if (1 in newvode) {
|
|
478
|
+
newvode[1] = remember(state, newvode[1], undefined) as Vode<S>;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const properties = props(newVode);
|
|
482
|
+
patchProperties(patch, newNode, undefined, properties, svg);
|
|
483
|
+
|
|
484
|
+
if (oldNode) {
|
|
485
|
+
(<any>oldNode).onUnmount && patch((<any>oldNode).onUnmount(oldNode));
|
|
486
|
+
oldNode.replaceWith(newNode);
|
|
487
|
+
} else {
|
|
488
|
+
if (parent.childNodes[childIndex]) {
|
|
489
|
+
parent.insertBefore(newNode, parent.childNodes[childIndex]);
|
|
490
|
+
} else {
|
|
491
|
+
parent.appendChild(newNode);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const newChildren = children(newVode);
|
|
496
|
+
if (newChildren) {
|
|
497
|
+
for (let i = 0; i < newChildren.length; i++) {
|
|
498
|
+
const child = newChildren[i];
|
|
499
|
+
const attached = render(state, patch, newNode, i, undefined, child, svg);
|
|
500
|
+
(<Vode<S>>newVode!)[properties ? i + 2 : i + 1] = <Vode<S>>attached;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
(<any>newNode).onMount && patch((<any>newNode).onMount(newNode));
|
|
505
|
+
return <AttachedVode<S>>newVode;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
//element(A) -> element(A)
|
|
509
|
+
if (!oldIsText && isNode && (<Vode<S>>oldVode)[0] === (<Vode<S>>newVode)[0]) {
|
|
510
|
+
svg = svg || (<Vode<S>>newVode)[0] === "svg";
|
|
511
|
+
(<AttachedVode<S>>newVode).node = oldNode;
|
|
512
|
+
|
|
513
|
+
const newvode = <Vode<S>>newVode;
|
|
514
|
+
const oldvode = <Vode<S>>oldVode;
|
|
515
|
+
|
|
516
|
+
let hasProps = false;
|
|
517
|
+
if ((<any>newvode[1])?.__memo) {
|
|
518
|
+
const prev = newvode[1] as any;
|
|
519
|
+
newvode[1] = remember(state, newvode[1], oldvode[1]) as Vode<S>;
|
|
520
|
+
if (prev !== newvode[1]) {
|
|
521
|
+
const properties = props(newVode);
|
|
522
|
+
patchProperties(patch, oldNode!, props(oldVode), properties, svg);
|
|
523
|
+
hasProps = !!properties;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
const properties = props(newVode);
|
|
528
|
+
patchProperties(patch, oldNode!, props(oldVode), properties, svg);
|
|
529
|
+
hasProps = !!properties;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const newKids = children(newVode);
|
|
533
|
+
const oldKids = children(oldVode) as AttachedVode<S>[];
|
|
534
|
+
if (newKids) {
|
|
535
|
+
for (let i = 0; i < newKids.length; i++) {
|
|
536
|
+
const child = newKids[i];
|
|
537
|
+
const oldChild = oldKids && oldKids[i];
|
|
538
|
+
|
|
539
|
+
const attached = render(state, patch, oldNode!, i, oldChild, child, svg);
|
|
540
|
+
if (attached) {
|
|
541
|
+
(<Vode<S>>newVode)[hasProps ? i + 2 : i + 1] = <Vode<S>>attached;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
for (let i = newKids.length; oldKids && i < oldKids.length; i++) {
|
|
545
|
+
if (oldKids[i]?.node)
|
|
546
|
+
oldKids[i].node!.remove();
|
|
547
|
+
else if ((oldKids[i] as Text)?.nodeType === Node.TEXT_NODE)
|
|
548
|
+
(oldKids[i] as Text).remove();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
for (let i = newKids?.length || 0; i < oldKids?.length || 0; i++) {
|
|
553
|
+
if (oldKids[i]?.node)
|
|
554
|
+
oldKids[i].node!.remove();
|
|
555
|
+
else if ((oldKids[i] as Text)?.nodeType === Node.TEXT_NODE)
|
|
556
|
+
(oldKids[i] as Text).remove();
|
|
557
|
+
}
|
|
558
|
+
return <AttachedVode<S>>newVode;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function patchProperties<S>(patch: Dispatch<S>, node: ChildNode, oldProps?: Props<S>, newProps?: Props<S>, isSvg?: boolean) {
|
|
565
|
+
if (!newProps && !oldProps) return;
|
|
566
|
+
if (!oldProps) { // set new props
|
|
567
|
+
for (const key in newProps) {
|
|
568
|
+
const newValue = newProps[key as keyof Props<S>] as PropertyValue<S>;
|
|
569
|
+
newProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, undefined, newValue, isSvg);
|
|
570
|
+
}
|
|
571
|
+
} else if (newProps) { // clear old props and set new in one loop
|
|
572
|
+
const combinedKeys = new Set([...Object.keys(oldProps), ...Object.keys(newProps)]);
|
|
573
|
+
for (const key of combinedKeys) {
|
|
574
|
+
const oldValue = oldProps[key as keyof Props<S>] as PropertyValue<S>;
|
|
575
|
+
const newValue = newProps[key as keyof Props<S>] as PropertyValue<S>;
|
|
576
|
+
if (key[0] === "o" && key[1] === "n") {
|
|
577
|
+
const oldEvent = (<any>node)["__" + key];
|
|
578
|
+
if ((oldEvent && oldEvent !== newValue) || (!oldEvent && oldValue !== newValue)) {
|
|
579
|
+
newProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, oldValue, newValue, isSvg);
|
|
580
|
+
}
|
|
581
|
+
(<any>node)["__" + key] = newValue;
|
|
582
|
+
}
|
|
583
|
+
else if (oldValue !== newValue) {
|
|
584
|
+
newProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, oldValue, newValue, isSvg);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
} else { //delete all old props, cause there are no new props
|
|
588
|
+
for (const key in oldProps) {
|
|
589
|
+
const oldValue = oldProps[key as keyof Props<S>] as PropertyValue<S>;
|
|
590
|
+
oldProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, oldValue, undefined, isSvg);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function patchProperty<S>(patch: Dispatch<S>, node: ChildNode, key: string | keyof ElementEventMap, oldValue?: PropertyValue<S>, newValue?: PropertyValue<S>, isSvg?: boolean) {
|
|
596
|
+
if (key === "style") {
|
|
597
|
+
if (!newValue) {
|
|
598
|
+
(node as HTMLElement).style.cssText = "";
|
|
599
|
+
} else if (oldValue) {
|
|
600
|
+
for (let k in { ...(oldValue as Props<S>), ...(newValue as Props<S>) }) {
|
|
601
|
+
if (!oldValue || newValue[k as keyof PropertyValue<S>] !== oldValue[k as keyof PropertyValue<S>]) {
|
|
602
|
+
(node as HTMLElement).style[k as keyof PropertyValue<S>] = newValue[k as keyof PropertyValue<S>];
|
|
603
|
+
}
|
|
604
|
+
else if (oldValue[k as keyof PropertyValue<S>] && !newValue[k as keyof PropertyValue<S>]) {
|
|
605
|
+
(<any>(node as HTMLElement).style)[k as keyof PropertyValue<S>] = undefined;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
for (let k in (newValue as Props<S>)) {
|
|
610
|
+
(node as HTMLElement).style[k as keyof PropertyValue<S>] = newValue[k as keyof PropertyValue<S>];
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
} else if (key === "class") {
|
|
614
|
+
if (isSvg) {
|
|
615
|
+
if (newValue) {
|
|
616
|
+
const newClass = classString(newValue as ClassProp);
|
|
617
|
+
(<SVGSVGElement>node).classList.value = newClass;
|
|
618
|
+
} else {
|
|
619
|
+
(<SVGSVGElement>node).classList.value = '';
|
|
620
|
+
}
|
|
621
|
+
} else {
|
|
622
|
+
if (newValue) {
|
|
623
|
+
const newClass = classString(newValue as ClassProp);
|
|
624
|
+
(<HTMLElement>node).className = newClass;
|
|
625
|
+
} else {
|
|
626
|
+
(<HTMLElement>node).className = '';
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
} else if (key[0] === "o" && key[1] === "n") {
|
|
630
|
+
if (newValue) {
|
|
631
|
+
let eventHandler: Function | null = null;
|
|
632
|
+
if (typeof newValue === "function") {
|
|
633
|
+
const action = newValue as EffectFunction<S>;
|
|
634
|
+
eventHandler = (evt: Event) => patch([action, evt]);
|
|
635
|
+
} else if (Array.isArray(newValue)) {
|
|
636
|
+
const arr = (newValue as Array<any>);
|
|
637
|
+
const action = newValue[0] as EffectFunction<S>;
|
|
638
|
+
if (arr.length > 1) {
|
|
639
|
+
eventHandler = () => patch([action, ...arr.slice(1)]);
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
eventHandler = (evt: Event) => patch([action, evt]);
|
|
643
|
+
}
|
|
644
|
+
} else if (typeof newValue === "object") {
|
|
645
|
+
eventHandler = () => patch(newValue as Patch<S>);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
(<any>node)[key] = eventHandler;
|
|
649
|
+
} else {
|
|
650
|
+
(<any>node)[key] = null;
|
|
651
|
+
}
|
|
652
|
+
} else if (newValue !== null && newValue !== undefined && newValue !== false) {
|
|
653
|
+
(<HTMLElement>node).setAttribute(key, <string>newValue);
|
|
654
|
+
} else {
|
|
655
|
+
(<HTMLElement>node).removeAttribute(key);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return newValue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function mergeState(target: any, source: any) {
|
|
662
|
+
if (!source) return target;
|
|
663
|
+
|
|
664
|
+
for (const key in source) {
|
|
665
|
+
const value = source[key];
|
|
666
|
+
if (value && typeof value === "object") {
|
|
667
|
+
const targetValue = target[key];
|
|
668
|
+
if (targetValue) {
|
|
669
|
+
if (Array.isArray(value)) {
|
|
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
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return target;
|
|
694
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2024",
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
|
+
"rootDir": ".",
|
|
7
|
+
"composite": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"removeComments": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"inlineSourceMap": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"allowSyntheticDefaultImports": true,
|
|
14
|
+
"strictBindCallApply": true,
|
|
15
|
+
"strictFunctionTypes": true,
|
|
16
|
+
"strictPropertyInitialization": true,
|
|
17
|
+
"strictNullChecks": true,
|
|
18
|
+
"allowJs": false,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noImplicitReturns": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"downlevelIteration": true,
|
|
23
|
+
"isolatedModules": true
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"./index.ts",
|
|
27
|
+
"./src/**/*.ts",
|
|
28
|
+
]
|
|
29
|
+
}
|