@ryupold/vode 0.13.1 → 1.0.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 +2 -1
- package/README.md +223 -76
- package/dist/vode.js +932 -0
- package/dist/vode.min.js +1 -0
- package/dist/vode.min.mjs +1 -0
- package/logo.svg +50 -0
- package/package.json +7 -4
- package/src/vode.ts +11 -11
- /package/{vode.mjs → dist/vode.mjs} +0 -0
|
@@ -15,7 +15,8 @@ jobs:
|
|
|
15
15
|
steps:
|
|
16
16
|
- uses: actions/checkout@v4
|
|
17
17
|
- uses: oven-sh/setup-bun@v2
|
|
18
|
-
- run: bun
|
|
18
|
+
- run: bun install
|
|
19
|
+
- run: bun run release
|
|
19
20
|
- run: |
|
|
20
21
|
echo "releasing to npm..."
|
|
21
22
|
bun publish --provenance --access public | grep "+ @ryupold/vode@" > version.txt
|
package/README.md
CHANGED
|
@@ -1,11 +1,224 @@
|
|
|
1
|
-
# vode
|
|
1
|
+
# 
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A small web framework for a minimalistic development flow. Zero dependencies, no build step except for typescript compilation, and a simple virtual DOM implementation that is easy to understand and use. Autocompletion out of the box due to binding to `lib.dom.d.ts`.
|
|
4
|
+
|
|
5
|
+
## vode
|
|
6
|
+
|
|
7
|
+
A `vode` is a representation of a virtual DOM node, which is a tree structure of HTML elements. It is written as tuple:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
[TAG, PROPS?, CHILDREN...]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
As you can see, it is a simple array with the first element being the tag name, the second element being an optional properties object, and the rest being child-vodes.
|
|
14
|
+
|
|
15
|
+
### Component
|
|
16
|
+
```ts
|
|
17
|
+
type Component<S> = (s: S) => ChildVode<S>;
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
A `Component<State>` is a function that takes a state object and returns a `Vode<State>`. It is used to render the UI based on the current state.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// A full vode has a tag, properties, and children. props and children are optional.
|
|
24
|
+
const CompFooBar = (s) => [DIV, { class: "container" },
|
|
25
|
+
|
|
26
|
+
// a child vode can be a string, which results in a text node
|
|
27
|
+
[H1, "Hello World"],
|
|
28
|
+
|
|
29
|
+
// a vode can also be a self-closing tag
|
|
30
|
+
[BR],
|
|
31
|
+
|
|
32
|
+
// style object maps directly to the HTML style attribute
|
|
33
|
+
[P, { style: { color: "red", fontWeight: "bold" } }, "This is a paragraph."],
|
|
34
|
+
|
|
35
|
+
// class property has multiple forms
|
|
36
|
+
[UL,
|
|
37
|
+
[LI, {class: "class1 class2"}, "as string"],
|
|
38
|
+
[LI, {class: ["class1", "class2"]}, "as array"],
|
|
39
|
+
[LI, {class: {class1: true, class2: false}}, "as Record<string, boolean>"],
|
|
40
|
+
],
|
|
41
|
+
|
|
42
|
+
// events get the state object as first argument
|
|
43
|
+
// and the HTML event object as second argument
|
|
44
|
+
[BUTTON, {
|
|
45
|
+
// all on* events accept `Patch<State>`
|
|
46
|
+
onclick: (state, evt) => {
|
|
47
|
+
// objects returned by events are patched automatically
|
|
48
|
+
return { counter: state.counter + 1 };
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// you can set the patch object directly for events
|
|
52
|
+
onmouseenter: {pointing: true},
|
|
53
|
+
onmouseleave: {pointing: false},
|
|
54
|
+
|
|
55
|
+
// a patch can be an async function
|
|
56
|
+
onmouseup: async (state, evt) => {
|
|
57
|
+
state.patch(loading: true);
|
|
58
|
+
const result = await apiCall();
|
|
59
|
+
return { title: result.data.title, loading: false };
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// you can also use a generator function that yields patches
|
|
63
|
+
onmousedown: async function* (state, evt) => {
|
|
64
|
+
yield { loading: true };
|
|
65
|
+
const result = await apiCall();
|
|
66
|
+
yield {
|
|
67
|
+
body: result.data.body,
|
|
68
|
+
loading: false
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
class: { bar: s.pointing }
|
|
73
|
+
}, "Click me!"],
|
|
74
|
+
];
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### app
|
|
78
|
+
|
|
79
|
+
`app` is a function that takes a HTML node, an initial state object, and a render function (`Component<State>`).
|
|
80
|
+
```ts
|
|
81
|
+
const appNode = document.getElementById('APP-ID');
|
|
82
|
+
const initialState = {
|
|
83
|
+
counter: 0,
|
|
84
|
+
pointing: false,
|
|
85
|
+
loading: false,
|
|
86
|
+
title: '',
|
|
87
|
+
body: '',
|
|
88
|
+
};
|
|
89
|
+
const patch = app<State>(appNode, initialState, (s) => CompFooBar(s));
|
|
90
|
+
```
|
|
91
|
+
It will render the initial state and update the DOM when patches are applied to the patch function or via events. All elements returnded by the render function are placed under `appNode`.
|
|
92
|
+
|
|
93
|
+
You can have multiple isolated `app` instances on a page, each with its own state and render function. The returned patch function from `app` can be used to synchronize the state between them.
|
|
94
|
+
|
|
95
|
+
### memoization
|
|
96
|
+
To optimize performance, you can use `memo(Array, Component)` to cache the result of a component function. This is useful when the component does not depend on the state or when the state does not change frequently.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const CompMemoFooBar = (s) => [DIV, { class: "container" },
|
|
100
|
+
[H1, "Hello World"],
|
|
101
|
+
[BR],
|
|
102
|
+
[P, "This is a paragraph."],
|
|
103
|
+
|
|
104
|
+
// expensive component to render
|
|
105
|
+
memo(
|
|
106
|
+
// this array is shallow compared to the previous render
|
|
107
|
+
[s.title, s.body],
|
|
108
|
+
// this is the component function that will be
|
|
109
|
+
// called only when the array changes
|
|
110
|
+
(s) => {
|
|
111
|
+
const list = [UL];
|
|
112
|
+
for (let i = 0; i < 1000; i++) {
|
|
113
|
+
list.push([LI, `Item ${i}`]);
|
|
114
|
+
}
|
|
115
|
+
return list;
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
];
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### state
|
|
122
|
+
The state is a singleton object that can be updated. A re-render happens when a patch object is supplied to the patch function or via event.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// type safe way to create the state object
|
|
126
|
+
const s = createState({
|
|
127
|
+
counter: 0,
|
|
128
|
+
pointing: false,
|
|
129
|
+
loading: false,
|
|
130
|
+
title: 'foo',
|
|
131
|
+
body: '',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
type State = typeof s;
|
|
135
|
+
|
|
136
|
+
app(appNode, s, ...); // after calling app(), the state object is bound to the appNode
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
s.title = 'Hello World'; // update state directly as it is a singleton (silent patch)
|
|
140
|
+
|
|
141
|
+
s.patch({}); // render patch
|
|
142
|
+
|
|
143
|
+
s.patch({ title: 'bar' }); // render patch with a change that is applied to the state
|
|
144
|
+
|
|
145
|
+
s.patch((s) => ({body: s.body + ' baz'})); // render patch with a function that receives the state
|
|
146
|
+
|
|
147
|
+
s.patch(async function*(s){
|
|
148
|
+
// you can also use a generator function that yields patches
|
|
149
|
+
yield { loading: true };
|
|
150
|
+
const result = await apiCall();
|
|
151
|
+
yield { title: result.title, body: result.body };
|
|
152
|
+
return { loading: false };
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
s.patch(null); // ignored, also: undefined, number, string, boolean, void
|
|
156
|
+
|
|
157
|
+
s.patch({ pointing: undefined }); // deletes the property from the state
|
|
158
|
+
```
|
|
6
159
|
|
|
7
160
|
## Install
|
|
8
161
|
|
|
162
|
+
### ESM
|
|
163
|
+
```html
|
|
164
|
+
<!DOCTYPE html>
|
|
165
|
+
<html>
|
|
166
|
+
<head>
|
|
167
|
+
<title>ESM Example</title>
|
|
168
|
+
</head>
|
|
169
|
+
<body>
|
|
170
|
+
<div id="app"></div>
|
|
171
|
+
<script type="module">
|
|
172
|
+
import { app, createState, BR, DIV, INPUT, SPAN } from 'https://cdn.jsdelivr.net/npm/@ryupold/vode/dist/vode.min.mjs';
|
|
173
|
+
|
|
174
|
+
const appNode = document.getElementById('app');
|
|
175
|
+
|
|
176
|
+
app(appNode, { counter: 0 },
|
|
177
|
+
(s) => [DIV,
|
|
178
|
+
[INPUT, {
|
|
179
|
+
type: 'button',
|
|
180
|
+
onclick: { counter: s.counter + 1 },
|
|
181
|
+
value: 'Click me',
|
|
182
|
+
}],
|
|
183
|
+
[BR],
|
|
184
|
+
[SPAN, { style: { color: 'red' } }, `${s.counter}`],
|
|
185
|
+
]
|
|
186
|
+
);
|
|
187
|
+
</script>
|
|
188
|
+
</body>
|
|
189
|
+
</html>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Classic
|
|
193
|
+
```html
|
|
194
|
+
<!DOCTYPE html>
|
|
195
|
+
<html>
|
|
196
|
+
<head>
|
|
197
|
+
<title>Classic Script Example</title>
|
|
198
|
+
<script src="https://cdn.jsdelivr.net/npm/@ryupold/vode/dist/vode.min.js"></script>
|
|
199
|
+
</head>
|
|
200
|
+
<body>
|
|
201
|
+
<div id="app"></div>
|
|
202
|
+
<script>
|
|
203
|
+
const appNode = document.getElementById('app');
|
|
204
|
+
V.app(appNode, { counter: 0 },
|
|
205
|
+
(s) => ["DIV",
|
|
206
|
+
["INPUT", {
|
|
207
|
+
type: 'button',
|
|
208
|
+
onclick: { counter: s.counter + 1 },
|
|
209
|
+
value: 'Click me',
|
|
210
|
+
}
|
|
211
|
+
],
|
|
212
|
+
["BR"],
|
|
213
|
+
["SPAN", { style: { color: 'red' } }, `${s.counter}`],
|
|
214
|
+
]);
|
|
215
|
+
</script>
|
|
216
|
+
</body>
|
|
217
|
+
</html>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### NPM
|
|
221
|
+
|
|
9
222
|
```bash
|
|
10
223
|
# npm
|
|
11
224
|
npm install @ryupold/vode --save
|
|
@@ -17,14 +230,7 @@ yarn add @ryupold/vode
|
|
|
17
230
|
bun add @ryupold/vode
|
|
18
231
|
```
|
|
19
232
|
|
|
20
|
-
## Patch
|
|
21
|
-
|
|
22
|
-
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.
|
|
23
|
-
|
|
24
|
-
## Usage
|
|
25
|
-
|
|
26
233
|
index.html
|
|
27
|
-
|
|
28
234
|
```html
|
|
29
235
|
<html>
|
|
30
236
|
<head>
|
|
@@ -39,8 +245,6 @@ index.html
|
|
|
39
245
|
|
|
40
246
|
main.ts
|
|
41
247
|
```ts
|
|
42
|
-
// import { app, createState} from 'vode.js';
|
|
43
|
-
// import { BR, DIV, INPUT, SPAN } from 'vode-tags.js';
|
|
44
248
|
import { app, createState, BR, DIV, INPUT, SPAN } from '@ryupold/vode';
|
|
45
249
|
|
|
46
250
|
|
|
@@ -65,70 +269,13 @@ app<State>(appNode, init,
|
|
|
65
269
|
);
|
|
66
270
|
```
|
|
67
271
|
|
|
68
|
-
##
|
|
69
|
-
|
|
70
|
-
> Prompt: "what can you tell me about the overall design philosophy of the vode library based on what you see in `src/vode.ts`"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
Based on my analysis of `src/vode.ts`, here's the overall design philosophy of the vode library:
|
|
74
|
-
|
|
75
|
-
### Core Design Philosophy
|
|
76
|
-
|
|
77
|
-
**Functional Virtual DOM with Minimal Abstractions**
|
|
78
|
-
- Uses array-based virtual nodes (`[tag, props?, ...children]`) instead of object-based structures
|
|
79
|
-
- Prioritizes simplicity and performance over complex abstractions
|
|
80
|
-
|
|
81
|
-
**State-Centric Architecture**
|
|
82
|
-
- Central state object with patch-based updates
|
|
83
|
-
- Immutable-style state management with deep merging
|
|
84
|
-
- Single source of truth pattern
|
|
85
|
-
|
|
86
|
-
**Zero-Dependency Minimalism**
|
|
87
|
-
- Pure TypeScript/JavaScript implementation
|
|
88
|
-
- No external dependencies or build tools required
|
|
89
|
-
- Direct DOM manipulation without heavy runtime
|
|
90
|
-
|
|
91
|
-
### Key Architectural Decisions
|
|
92
|
-
|
|
93
|
-
#### Virtual DOM Structure
|
|
94
|
-
```typescript
|
|
95
|
-
// Simple array-based nodes
|
|
96
|
-
["div", { class: "container" }, "Hello", ["span", "World"]]
|
|
97
|
-
```
|
|
98
|
-
- Minimal memory footprint
|
|
99
|
-
- Easy serialization/debugging
|
|
100
|
-
- Type-safe with TypeScript generics
|
|
101
|
-
|
|
102
|
-
#### Reactive State Management
|
|
103
|
-
```typescript
|
|
104
|
-
state.patch(newData) // Direct updates
|
|
105
|
-
state.patch([effect, payload]) // Function-based effects
|
|
106
|
-
state.patch(promise) // Async support
|
|
107
|
-
```
|
|
108
|
-
- Unified patch system for all state changes
|
|
109
|
-
- Built-in async/generator support
|
|
110
|
-
- Functional effects pattern
|
|
111
|
-
|
|
112
|
-
#### Performance Optimizations
|
|
113
|
-
- **Memoization**: `memo()` for component caching
|
|
114
|
-
- **Reconciliation**: Intelligent DOM diffing
|
|
115
|
-
- **Batched Updates**: RequestAnimationFrame-based rendering
|
|
116
|
-
- **Memory Management**: Proper cleanup with mount/unmount hooks
|
|
117
|
-
|
|
118
|
-
#### Developer Experience
|
|
119
|
-
- **Type Safety**: Full TypeScript support with generics
|
|
120
|
-
- **Debugging**: Built-in performance statistics
|
|
121
|
-
- **Flexibility**: Support for HTML, SVG, and MathML
|
|
122
|
-
- **Composability**: Functional component model
|
|
272
|
+
## Contributing
|
|
123
273
|
|
|
124
|
-
|
|
274
|
+
I was delighted by the simplicity of [hyperapp](https://github.com/jorgebucaran/hyperapp), which inspired me to create this library.
|
|
125
275
|
|
|
126
|
-
|
|
276
|
+
Not planning to add more features, just keeping it simple and easy.
|
|
127
277
|
|
|
128
|
-
|
|
129
|
-
2. **Performance by design** - Minimal overhead, efficient updates
|
|
130
|
-
3. **Developer control** - Direct state manipulation without hidden abstractions
|
|
131
|
-
4. **Functional paradigms** - Immutable updates, pure components
|
|
132
|
-
5. **Modern web standards** - Leverages native browser APIs effectively
|
|
278
|
+
But if you find bugs or have suggestions, feel free to open an [issue](https://github.com/ryupold/vode/issues) or a pull request.
|
|
133
279
|
|
|
134
|
-
|
|
280
|
+
## License
|
|
281
|
+
[MIT](./LICENSE)
|