@uistate/renderer 1.0.0 → 1.0.2
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/README.md +11 -11
- package/package.json +1 -1
- package/renderer.js +13 -13
- package/self-test.js +14 -16
package/README.md
CHANGED
|
@@ -33,9 +33,9 @@ A reactive counter. No React. No Babel. No bundler. No innerHTML. Just HTML attr
|
|
|
33
33
|
|
|
34
34
|
## Three Primitives
|
|
35
35
|
|
|
36
|
-
### 1. Delegated Actions (DOM
|
|
36
|
+
### 1. Delegated Actions (DOM -> Store)
|
|
37
37
|
|
|
38
|
-
Attach store writes to user events. Three delegated listeners on the root handle everything
|
|
38
|
+
Attach store writes to user events. Three delegated listeners on the root handle everything; they survive DOM mutations and never need re-wiring.
|
|
39
39
|
|
|
40
40
|
| Attribute | Event | Example |
|
|
41
41
|
|---|---|---|
|
|
@@ -58,9 +58,9 @@ Attach store writes to user events. Three delegated listeners on the root handle
|
|
|
58
58
|
| `set="flag:false"` | `store.set('flag', false)` |
|
|
59
59
|
| `set="val:null"` | `store.set('val', null)` |
|
|
60
60
|
|
|
61
|
-
### 2. Direct Node Binding (Store
|
|
61
|
+
### 2. Direct Node Binding (Store -> DOM)
|
|
62
62
|
|
|
63
|
-
Bind store paths directly to DOM node properties. Each binding creates one EventState subscription and performs surgical updates
|
|
63
|
+
Bind store paths directly to DOM node properties. Each binding creates one EventState subscription and performs surgical updates; no re-rendering, no diffing.
|
|
64
64
|
|
|
65
65
|
| Attribute | What it does | Example |
|
|
66
66
|
|---|---|---|
|
|
@@ -124,16 +124,16 @@ const cleanup = mount(store);
|
|
|
124
124
|
import { parseSetExpr, evalExpr, parsePush } from '@uistate/renderer';
|
|
125
125
|
|
|
126
126
|
parseSetExpr('count:increment');
|
|
127
|
-
//
|
|
127
|
+
// -> { path: 'count', expr: 'increment' }
|
|
128
128
|
|
|
129
129
|
evalExpr('increment', 5);
|
|
130
|
-
//
|
|
130
|
+
// -> 6
|
|
131
131
|
|
|
132
132
|
parsePush('push(draft)');
|
|
133
|
-
//
|
|
133
|
+
// -> { source: 'draft' }
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
These are the same functions the renderer uses internally. Because they're pure, they run in Node with zero dependencies
|
|
136
|
+
These are the same functions the renderer uses internally. Because they're pure, they run in Node with zero dependencies, enabling the self-test.
|
|
137
137
|
|
|
138
138
|
## Testing
|
|
139
139
|
|
|
@@ -171,11 +171,11 @@ HTML is the skeleton. The store is the brain. Bindings are the nerves.
|
|
|
171
171
|
|
|
172
172
|
## Philosophy
|
|
173
173
|
|
|
174
|
-
The renderer exists to prove that **the state layer is the real product**. The same `@uistate/core` store works with React, Vue, Svelte, Angular
|
|
174
|
+
The renderer exists to prove that **the state layer is the real product**. The same `@uistate/core` store works with React, Vue, Svelte, Angular, or with this 268-line renderer that needs nothing but a browser.
|
|
175
175
|
|
|
176
176
|
```
|
|
177
|
-
A React Component (for comparison): f(props, ownState, lifecycle, hooks, context, memo, refs)
|
|
178
|
-
A UIstate Renderer (for comparison): mount(store)
|
|
177
|
+
A React Component (for comparison): f(props, ownState, lifecycle, hooks, context, memo, refs) -> VDOM -> DOM
|
|
178
|
+
A UIstate Renderer (for comparison): mount(store) -> bind-text="path" -> textContent
|
|
179
179
|
```
|
|
180
180
|
|
|
181
181
|
## Author
|
package/package.json
CHANGED
package/renderer.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @uistate/renderer
|
|
2
|
+
* @uistate/renderer: Direct-binding reactive renderer for EventState
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) 2025 Ajdin Imsirovic
|
|
5
5
|
*
|
|
6
6
|
* Three primitives:
|
|
7
|
-
* 1. Delegated Actions (DOM
|
|
8
|
-
* 2. Direct Node Binding (Store
|
|
7
|
+
* 1. Delegated Actions (DOM -> Store): set, set-blur, set-enter
|
|
8
|
+
* 2. Direct Node Binding (Store -> DOM): bind-text, bind-value, bind-data-*, bind-focus
|
|
9
9
|
* 3. Keyed Collections: each="path" + <template>
|
|
10
10
|
*
|
|
11
11
|
* No templates. No innerHTML. No interpolation. No diffing.
|
|
12
12
|
* Event delegation survives DOM mutations. Bindings are surgical.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
//
|
|
15
|
+
// -- Pure helpers ------------------------------------------------------
|
|
16
16
|
|
|
17
17
|
const BOUND = Symbol('r2');
|
|
18
18
|
|
|
@@ -43,12 +43,12 @@ export function parsePush(expr) {
|
|
|
43
43
|
return m ? { source: m[1].trim() } : null;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
//
|
|
46
|
+
// -- Mount -------------------------------------------------------------
|
|
47
47
|
|
|
48
48
|
export function mount(store, root = document.body) {
|
|
49
49
|
const subs = []; // { node, unsub }
|
|
50
50
|
|
|
51
|
-
//
|
|
51
|
+
// -- Binding helpers --
|
|
52
52
|
|
|
53
53
|
function addBinding(path, node, updateFn) {
|
|
54
54
|
updateFn(store.get(path));
|
|
@@ -65,7 +65,7 @@ export function mount(store, root = document.body) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
//
|
|
68
|
+
// -- Scan for bind-* attributes --
|
|
69
69
|
|
|
70
70
|
function scanBindings(el) {
|
|
71
71
|
const nodes = el instanceof Element ? [el, ...el.querySelectorAll('*')] : [];
|
|
@@ -108,7 +108,7 @@ export function mount(store, root = document.body) {
|
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
// bind-data-*
|
|
111
|
+
// bind-data-* -> dataset, bind-attr-* -> setAttribute
|
|
112
112
|
for (const attr of Array.from(node.attributes)) {
|
|
113
113
|
if (attr.name.startsWith('bind-data-')) {
|
|
114
114
|
const dName = attr.name.slice(10);
|
|
@@ -128,7 +128,7 @@ export function mount(store, root = document.body) {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
//
|
|
131
|
+
// -- Delegated action execution --
|
|
132
132
|
|
|
133
133
|
function executeSet(raw) {
|
|
134
134
|
const { path, expr } = parseSetExpr(raw);
|
|
@@ -174,7 +174,7 @@ export function mount(store, root = document.body) {
|
|
|
174
174
|
store.set(path, evalExpr(expr, store.get(path)));
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
//
|
|
177
|
+
// -- Step 1: Event delegation (once, never re-wired) --
|
|
178
178
|
|
|
179
179
|
root.addEventListener('click', e => {
|
|
180
180
|
const t = e.target.closest('[set]');
|
|
@@ -195,7 +195,7 @@ export function mount(store, root = document.body) {
|
|
|
195
195
|
}
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
-
//
|
|
198
|
+
// -- Step 3: Keyed collections --
|
|
199
199
|
|
|
200
200
|
function setupCollection(container) {
|
|
201
201
|
const collPath = container.getAttribute('each');
|
|
@@ -203,7 +203,7 @@ export function mount(store, root = document.body) {
|
|
|
203
203
|
if (!collPath || !tpl) return;
|
|
204
204
|
|
|
205
205
|
const templateHTML = tpl.innerHTML.trim();
|
|
206
|
-
const rendered = new Map(); // key
|
|
206
|
+
const rendered = new Map(); // key -> element
|
|
207
207
|
let lastKeyStr = '';
|
|
208
208
|
|
|
209
209
|
function resolve(html, key) {
|
|
@@ -257,7 +257,7 @@ export function mount(store, root = document.body) {
|
|
|
257
257
|
reconcile();
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
//
|
|
260
|
+
// -- Init --
|
|
261
261
|
|
|
262
262
|
root.querySelectorAll('[each]').forEach(setupCollection);
|
|
263
263
|
scanBindings(root);
|
package/self-test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @uistate/renderer
|
|
2
|
+
* @uistate/renderer: self-test
|
|
3
3
|
*
|
|
4
4
|
* Standalone test of pure functions. No dependencies beyond renderer.js.
|
|
5
5
|
* Runs on `node self-test.js` or as a postinstall hook.
|
|
@@ -17,21 +17,21 @@ let failed = 0;
|
|
|
17
17
|
function assert(name, condition) {
|
|
18
18
|
if (condition) {
|
|
19
19
|
passed++;
|
|
20
|
+
console.log(` ✓ ${name}`);
|
|
20
21
|
} else {
|
|
21
22
|
failed++;
|
|
22
23
|
console.error(` ✗ ${name}`);
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
console.log('\n1. parseSetExpr');
|
|
28
28
|
const p1 = parseSetExpr('count:increment');
|
|
29
29
|
assert('parseSetExpr: path', p1.path === 'count');
|
|
30
30
|
assert('parseSetExpr: expr', p1.expr === 'increment');
|
|
31
31
|
|
|
32
32
|
const p2 = parseSetExpr('count');
|
|
33
33
|
assert('parseSetExpr: path only', p2.path === 'count');
|
|
34
|
-
assert('parseSetExpr: no expr
|
|
34
|
+
assert('parseSetExpr: no expr -> null', p2.expr === null);
|
|
35
35
|
|
|
36
36
|
const p3 = parseSetExpr('user.name:Bob');
|
|
37
37
|
assert('parseSetExpr: dotted path', p3.path === 'user.name');
|
|
@@ -60,15 +60,14 @@ const p9 = parseSetExpr(' count : increment ');
|
|
|
60
60
|
assert('parseSetExpr: trims whitespace path', p9.path === 'count');
|
|
61
61
|
assert('parseSetExpr: trims whitespace expr', p9.expr === 'increment');
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
console.log('\n2. evalExpr');
|
|
65
64
|
assert('evalExpr: increment', evalExpr('increment', 5) === 6);
|
|
66
65
|
assert('evalExpr: increment from 0', evalExpr('increment', 0) === 1);
|
|
67
66
|
assert('evalExpr: increment from null', evalExpr('increment', null) === 1);
|
|
68
67
|
assert('evalExpr: decrement', evalExpr('decrement', 5) === 4);
|
|
69
68
|
assert('evalExpr: decrement from 0', evalExpr('decrement', 0) === -1);
|
|
70
|
-
assert('evalExpr: toggle true
|
|
71
|
-
assert('evalExpr: toggle false
|
|
69
|
+
assert('evalExpr: toggle true->false', evalExpr('toggle', true) === false);
|
|
70
|
+
assert('evalExpr: toggle false->true', evalExpr('toggle', false) === true);
|
|
72
71
|
assert('evalExpr: number 42', evalExpr('42', 0) === 42);
|
|
73
72
|
assert('evalExpr: number 0', evalExpr('0', 99) === 0);
|
|
74
73
|
assert('evalExpr: negative number', evalExpr('-5', 0) === -5);
|
|
@@ -76,25 +75,24 @@ assert('evalExpr: boolean true', evalExpr('true', 0) === true);
|
|
|
76
75
|
assert('evalExpr: boolean false', evalExpr('false', 1) === false);
|
|
77
76
|
assert('evalExpr: null keyword', evalExpr('null', 'x') === null);
|
|
78
77
|
assert('evalExpr: plain string', evalExpr('hello', '') === 'hello');
|
|
79
|
-
assert('evalExpr: null expr
|
|
80
|
-
assert('evalExpr: undefined expr
|
|
81
|
-
|
|
82
|
-
// ── parsePush ───────────────────────────────────────────────────────
|
|
78
|
+
assert('evalExpr: null expr -> returns current', evalExpr(null, 7) === 7);
|
|
79
|
+
assert('evalExpr: undefined expr -> returns current', evalExpr(undefined, 7) === 7);
|
|
83
80
|
|
|
81
|
+
console.log('\n3. parsePush');
|
|
84
82
|
const pp1 = parsePush('push');
|
|
85
|
-
assert('parsePush: bare push
|
|
83
|
+
assert('parsePush: bare push -> source null', pp1 !== null && pp1.source === null);
|
|
86
84
|
|
|
87
85
|
const pp2 = parsePush('push(draft)');
|
|
88
|
-
assert('parsePush: push(draft)
|
|
86
|
+
assert('parsePush: push(draft) -> source "draft"', pp2 !== null && pp2.source === 'draft');
|
|
89
87
|
|
|
90
88
|
const pp3 = parsePush('push(form.data)');
|
|
91
|
-
assert('parsePush: push(form.data)
|
|
89
|
+
assert('parsePush: push(form.data) -> source "form.data"', pp3 !== null && pp3.source === 'form.data');
|
|
92
90
|
|
|
93
91
|
assert('parsePush: non-push returns null', parsePush('increment') === null);
|
|
94
92
|
assert('parsePush: null returns null', parsePush(null) === null);
|
|
95
93
|
assert('parsePush: empty string returns null', parsePush('') === null);
|
|
96
94
|
|
|
97
|
-
//
|
|
95
|
+
// -- Results ---------------------------------------------------------
|
|
98
96
|
|
|
99
97
|
console.log(`\n@uistate/renderer v1.0.0 — self-test`);
|
|
100
98
|
console.log(`✓ ${passed} assertions passed${failed ? `, ✗ ${failed} failed` : ''}\n`);
|