@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.
@@ -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 run build
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
+ # ![vode-logo](./logo.svg)
2
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.
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
- ## Claude
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
- ### Philosophy Summary
274
+ I was delighted by the simplicity of [hyperapp](https://github.com/jorgebucaran/hyperapp), which inspired me to create this library.
125
275
 
126
- Vode follows a **"less is more"** philosophy, providing a lightweight alternative to heavyweight frameworks while maintaining modern reactive patterns. It emphasizes:
276
+ Not planning to add more features, just keeping it simple and easy.
127
277
 
128
- 1. **Explicitness over magic** - Clear, predictable behavior
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
- The library appears designed for developers who want React-like reactivity without the complexity and bundle size of modern frameworks.
280
+ ## License
281
+ [MIT](./LICENSE)