@ape-egg/vibe 1.0.2 → 1.0.5
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/CHANGELOG.md +32 -0
- package/ROADMAP.md +289 -0
- package/_vibe-compiled-iteration-batch.js +9 -8
- package/affected.js +47 -25
- package/conditionals.js +15 -24
- package/constants.js +103 -22
- package/hydrate.js +7 -19
- package/index.js +43 -32
- package/iterate.js +54 -31
- package/iteration-utils.js +17 -17
- package/link.js +1 -1
- package/package.json +1 -1
- package/parse.js +39 -32
- package/state.js +5 -5
- package/utils.js +16 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.5] - 2026-01-25
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Expression evaluation**: Normalize whitespace in `@[...]` expressions before evaluation, making bindings resilient to IDE auto-formatting that may break expressions across multiple lines
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **DRY refactor**: Consolidated expression evaluation into single `evalInScope()` function in utils.js, used by hydrate.js, conditionals.js, and affected.js
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## [1.0.4] - 2026-01-24
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **MutationObserver**: Use `takeRecords()` to preserve pending mutations before disconnecting during state changes, preventing queued DOM mutations (e.g., innerHTML replacements) from being lost when state updates occur simultaneously
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## [1.0.3] - 2026-01-23
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **MutationObserver**: Now processes `<!-- if -->` and `<!-- each -->` comments when new nodes are added to DOM, enabling dynamic content to use vibe's control flow without manual processing
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **affected.js**: Only marks elements as affected when their specific bound values change, preventing unnecessary DOM updates when unrelated state changes (fixes `@[color]` in `<style>` updating on every keystroke when `@[firstName]` changes)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
3
35
|
## [1.0.2] - 2026-01-20
|
|
4
36
|
|
|
5
37
|
### Fixed
|
package/ROADMAP.md
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Vibe Roadmap
|
|
2
|
+
|
|
3
|
+
Feature proposals and improvements for Vibe's runtime-first reactive framework.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Proposed: Manual Hydration API
|
|
8
|
+
|
|
9
|
+
**Status**: Proposal
|
|
10
|
+
**Priority**: Medium
|
|
11
|
+
**Category**: Core Runtime
|
|
12
|
+
|
|
13
|
+
### Problem
|
|
14
|
+
|
|
15
|
+
Vibe's MutationObserver automatically hydrates `@[bindings]` on initial page load and incremental DOM changes, but fails when developers perform wholesale DOM replacement via `innerHTML`:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// This doesn't trigger Vibe's hydration:
|
|
19
|
+
element.innerHTML = '<h1>Welcome, @[race]!</h1>';
|
|
20
|
+
// Result: Literal text "@[race]" instead of evaluated "human"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This happens because:
|
|
24
|
+
1. `innerHTML` replacement destroys all old DOM nodes and creates new ones
|
|
25
|
+
2. Vibe's MutationObserver is designed for incremental mutations, not complete replacement
|
|
26
|
+
3. No mechanism exists to manually trigger re-hydration
|
|
27
|
+
|
|
28
|
+
### Current Workaround
|
|
29
|
+
|
|
30
|
+
Users must manually evaluate bindings before setting innerHTML:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
const evaluateBindings = (html) => {
|
|
34
|
+
return html.replace(/@\[([^\]]+)\]/g, (match, expression) => {
|
|
35
|
+
const keys = Object.keys(window.$);
|
|
36
|
+
const values = Object.values(window.$);
|
|
37
|
+
const result = new Function(...keys, `return ${expression}`)(...values);
|
|
38
|
+
return result ?? '';
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
element.innerHTML = evaluateBindings(html); // Manually evaluated
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Issues with this approach:**
|
|
46
|
+
- Not DRY - duplicates Vibe's internal evaluation logic
|
|
47
|
+
- Fragile - user's regex might not match Vibe's parser exactly
|
|
48
|
+
- Knowledge burden - users need to know when manual evaluation is needed
|
|
49
|
+
- Inconsistent - some bindings auto-hydrate, others need manual work
|
|
50
|
+
|
|
51
|
+
### Use Cases
|
|
52
|
+
|
|
53
|
+
This affects multiple real-world scenarios:
|
|
54
|
+
|
|
55
|
+
1. **Dynamic content replacement** (tutorials, articles, modals)
|
|
56
|
+
2. **Client-side routing** (replacing page sections with new HTML)
|
|
57
|
+
3. **Lazy-loaded sections** (loading HTML from server with bindings)
|
|
58
|
+
4. **Template cloning** (using `<template>` elements with `@[bindings]`)
|
|
59
|
+
5. **Server-sent HTML** (SSR-like patterns where server sends HTML with bindings)
|
|
60
|
+
|
|
61
|
+
### Proposed Solution
|
|
62
|
+
|
|
63
|
+
Add a manual hydration API that allows users to trigger Vibe's binding evaluation:
|
|
64
|
+
|
|
65
|
+
#### Option 1: Element Hydration
|
|
66
|
+
```js
|
|
67
|
+
vibe.hydrate(element);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Usage:**
|
|
71
|
+
```js
|
|
72
|
+
tutorial.innerHTML = '<h1>Welcome, @[race]!</h1>';
|
|
73
|
+
vibe.hydrate(tutorial); // Scan tutorial and children for @[bindings]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Pros:**
|
|
77
|
+
- Most flexible - works with any element
|
|
78
|
+
- Matches web component patterns (`connectedCallback()`)
|
|
79
|
+
- Clear intent - "scan this element"
|
|
80
|
+
|
|
81
|
+
**Cons:**
|
|
82
|
+
- Requires import/reference to vibe library
|
|
83
|
+
- Two-step process (set innerHTML, then hydrate)
|
|
84
|
+
|
|
85
|
+
#### Option 2: HTML String Evaluation
|
|
86
|
+
```js
|
|
87
|
+
const evaluated = vibe.evaluate(html, state);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Usage:**
|
|
91
|
+
```js
|
|
92
|
+
const html = '<h1>Welcome, @[race]!</h1>';
|
|
93
|
+
const evaluated = vibe.evaluate(html, window.$);
|
|
94
|
+
tutorial.innerHTML = evaluated;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Pros:**
|
|
98
|
+
- Pure function - easier to test
|
|
99
|
+
- Works without DOM access
|
|
100
|
+
- Can be used server-side or in workers
|
|
101
|
+
|
|
102
|
+
**Cons:**
|
|
103
|
+
- Users must manage state passing
|
|
104
|
+
- Doesn't handle nested/dynamic state updates
|
|
105
|
+
|
|
106
|
+
#### Option 3: Safe innerHTML Setter
|
|
107
|
+
```js
|
|
108
|
+
vibe.setHTML(element, html);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Usage:**
|
|
112
|
+
```js
|
|
113
|
+
vibe.setHTML(tutorial, '<h1>Welcome, @[race]!</h1>');
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Pros:**
|
|
117
|
+
- Single operation - set and hydrate in one call
|
|
118
|
+
- Matches platform APIs (`element.setHTML()`)
|
|
119
|
+
- Simplest API surface
|
|
120
|
+
|
|
121
|
+
**Cons:**
|
|
122
|
+
- Yet another setter abstraction
|
|
123
|
+
- Might conflict with future platform APIs
|
|
124
|
+
|
|
125
|
+
### Recommendation
|
|
126
|
+
|
|
127
|
+
**Implement Option 1** (`vibe.hydrate(element)`):
|
|
128
|
+
- Aligns with Vibe's runtime-first philosophy
|
|
129
|
+
- Gives users explicit control over hydration timing
|
|
130
|
+
- Most flexible for different scenarios
|
|
131
|
+
- Clear and predictable behavior
|
|
132
|
+
|
|
133
|
+
### Implementation Notes
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
// Expose on the state proxy:
|
|
137
|
+
window.$ = state({ race: 'human' });
|
|
138
|
+
window.$.vibe.hydrate(element); // Scan element for @[bindings]
|
|
139
|
+
|
|
140
|
+
// Or as a module export:
|
|
141
|
+
import state, { hydrate } from '@ape-egg/vibe';
|
|
142
|
+
hydrate(element);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Should support:
|
|
146
|
+
- Single element: `hydrate(tutorial)`
|
|
147
|
+
- Multiple elements: `hydrate([el1, el2])`
|
|
148
|
+
- Selector: `hydrate('tutorial')` (convenience)
|
|
149
|
+
|
|
150
|
+
### Related
|
|
151
|
+
|
|
152
|
+
Compare to other frameworks:
|
|
153
|
+
- **Alpine.js**: `Alpine.initTree(el)` - manual initialization
|
|
154
|
+
- **Vue**: `app.mount(el)` - mount to element
|
|
155
|
+
- **Svelte**: Compiler handles this at build time
|
|
156
|
+
- **HTMX**: `htmx.process(el)` - process element for attributes
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Known Issues
|
|
161
|
+
|
|
162
|
+
### innerHTML Replacement Corrupts Conditional Branches in Iterations
|
|
163
|
+
|
|
164
|
+
**Status**: Bug
|
|
165
|
+
**Priority**: Low (edge case)
|
|
166
|
+
**Category**: Core Runtime
|
|
167
|
+
**Discovered**: 2026-01-25
|
|
168
|
+
|
|
169
|
+
#### Problem
|
|
170
|
+
|
|
171
|
+
When a parent element containing iterations with nested conditionals has its `innerHTML` replaced multiple times with identical HTML, the conditional's `else` branches become `null` after 2-3 replacements. This causes conditionals to fail rendering.
|
|
172
|
+
|
|
173
|
+
**Root cause:**
|
|
174
|
+
1. Iteration template's `branches` object is shared across all iteration instances (iterate.js:65: `branches, // Branch templates are reused`)
|
|
175
|
+
2. When `innerHTML` replacement happens, something mutates the shared `branches.else` to `null`
|
|
176
|
+
3. All instances reference the same corrupted branches object
|
|
177
|
+
4. Subsequent renders have no `else` branch template to mount
|
|
178
|
+
|
|
179
|
+
#### Reproduction
|
|
180
|
+
|
|
181
|
+
Programmatic test case:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
// HTML structure
|
|
185
|
+
const html = `
|
|
186
|
+
<item-list>
|
|
187
|
+
<!-- each items as item, i -->
|
|
188
|
+
<item-card>
|
|
189
|
+
<span>@[item]</span>
|
|
190
|
+
<!-- if i % 2 === 0 -->
|
|
191
|
+
<badge>Even</badge>
|
|
192
|
+
<!-- else -->
|
|
193
|
+
<badge secondary>Odd</badge>
|
|
194
|
+
<!-- /if -->
|
|
195
|
+
</item-card>
|
|
196
|
+
<!-- /each -->
|
|
197
|
+
</item-list>
|
|
198
|
+
`;
|
|
199
|
+
|
|
200
|
+
// State
|
|
201
|
+
window.$ = state({ items: ['Apple', 'Banana', 'Cherry'] });
|
|
202
|
+
|
|
203
|
+
// Trigger the bug
|
|
204
|
+
const container = document.querySelector('[vibe]');
|
|
205
|
+
|
|
206
|
+
// First replacement: works
|
|
207
|
+
container.innerHTML = html;
|
|
208
|
+
await new Promise(r => setTimeout(r, 100));
|
|
209
|
+
|
|
210
|
+
// Second replacement: works
|
|
211
|
+
container.innerHTML = html;
|
|
212
|
+
await new Promise(r => setTimeout(r, 100));
|
|
213
|
+
|
|
214
|
+
// Third replacement: branches.else becomes NULL
|
|
215
|
+
container.innerHTML = html;
|
|
216
|
+
await new Promise(r => setTimeout(r, 100));
|
|
217
|
+
|
|
218
|
+
// Result: Even/Odd badges fail to render in the third iteration
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### Observations
|
|
222
|
+
|
|
223
|
+
1. Cloning `branches` object during iteration (shallow copy) doesn't prevent the bug
|
|
224
|
+
2. The mutation happens BEFORE cloning, meaning the original template is corrupted
|
|
225
|
+
3. Setting a property trap on `branches.else` doesn't catch the mutation (already null when accessed)
|
|
226
|
+
4. Only affects conditionals inside iterations - standalone conditionals work fine
|
|
227
|
+
5. Only triggers with multiple innerHTML replacements - single replacement works
|
|
228
|
+
|
|
229
|
+
#### Affected Patterns
|
|
230
|
+
|
|
231
|
+
This bug only affects:
|
|
232
|
+
- Replacing innerHTML multiple times with identical HTML containing iterations + conditionals
|
|
233
|
+
- Demo infrastructure like tutorial.js that re-renders on state changes
|
|
234
|
+
- Not representative of typical usage patterns
|
|
235
|
+
|
|
236
|
+
Does NOT affect:
|
|
237
|
+
- Normal reactivity (state changes)
|
|
238
|
+
- Single innerHTML replacement
|
|
239
|
+
- Iterations without conditionals
|
|
240
|
+
- Conditionals outside iterations
|
|
241
|
+
- Incremental DOM mutations (appendChild, insertBefore, etc.)
|
|
242
|
+
|
|
243
|
+
#### Workaround
|
|
244
|
+
|
|
245
|
+
Avoid multiple innerHTML replacements on parents containing iteration+conditional templates. Instead:
|
|
246
|
+
1. Use incremental DOM APIs (appendChild, createElement)
|
|
247
|
+
2. Replace innerHTML once at initialization only
|
|
248
|
+
3. Use Vibe's normal reactivity for updates
|
|
249
|
+
4. Don't wrap demo content in `<tutorial>` that re-renders via innerHTML
|
|
250
|
+
|
|
251
|
+
#### Investigation Log
|
|
252
|
+
|
|
253
|
+
Debugging attempts (2026-01-25):
|
|
254
|
+
- ✓ Confirmed `branches.else` becomes `null` after 3rd innerHTML replacement
|
|
255
|
+
- ✓ Added deep cloning of branches object - didn't help (already null before clone)
|
|
256
|
+
- ✓ Added Object.defineProperty trap - didn't fire (already null)
|
|
257
|
+
- ✓ Checked parsing logic - correctly finds else comments
|
|
258
|
+
- ✗ Unable to identify where mutation occurs
|
|
259
|
+
- ✗ Unable to reproduce with simpler test case (needs tutorial.js pattern)
|
|
260
|
+
|
|
261
|
+
Likely related to:
|
|
262
|
+
- MutationObserver's removedNodes callback cleaning up references
|
|
263
|
+
- Template caching/reuse strategy in iterate.js
|
|
264
|
+
- Interaction between parse → clone → hydrate → render cycle
|
|
265
|
+
|
|
266
|
+
#### Resolution Path
|
|
267
|
+
|
|
268
|
+
**Phase 1 (Current)**: Document and work around
|
|
269
|
+
- Remove `<tutorial>` wrapper from demos
|
|
270
|
+
- Add note in CLAUDE.md about limitation
|
|
271
|
+
- Tests validate core reactivity works correctly
|
|
272
|
+
|
|
273
|
+
**Phase 2+**: Consider fixing if real-world need emerges
|
|
274
|
+
- Deep investigation into branch reference lifecycle
|
|
275
|
+
- Possibly: deep clone branches instead of sharing reference
|
|
276
|
+
- Possibly: rebuild conditional metadata on each innerHTML replacement
|
|
277
|
+
- Possibly: manual hydration API (see "Manual Hydration API" proposal above)
|
|
278
|
+
|
|
279
|
+
This is acceptable technical debt since:
|
|
280
|
+
1. Edge case not representative of normal usage
|
|
281
|
+
2. Core reactivity (the 99% case) works correctly
|
|
282
|
+
3. Can be addressed when/if users report needing this pattern
|
|
283
|
+
4. Phase 1 goals (iteration + conditionals) are met
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Future Proposals
|
|
288
|
+
|
|
289
|
+
*This section reserved for additional feature proposals*
|
|
@@ -58,13 +58,13 @@ const hasNestedStructures = (tree) => {
|
|
|
58
58
|
*/
|
|
59
59
|
export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys) => {
|
|
60
60
|
const templateHtml = template.element.innerHTML.trim();
|
|
61
|
-
const escaped = templateHtml
|
|
62
|
-
.replace(/\\/g, '\\\\')
|
|
63
|
-
.replace(/`/g, '\\`')
|
|
64
|
-
.replace(/\$\{/g, '\\${');
|
|
61
|
+
const escaped = templateHtml.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
65
62
|
const code = escaped.replace(BINDING_REGEX, (_, expr) => '${' + expr + '}');
|
|
66
63
|
|
|
67
|
-
return new Function(
|
|
64
|
+
return new Function(
|
|
65
|
+
'arr',
|
|
66
|
+
...stateKeys,
|
|
67
|
+
`
|
|
68
68
|
let html = '';
|
|
69
69
|
const len = arr.length;
|
|
70
70
|
for (let ${indexAlias} = 0; ${indexAlias} < len; ${indexAlias}++) {
|
|
@@ -72,7 +72,8 @@ export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys) => {
|
|
|
72
72
|
html += \`${code}\`;
|
|
73
73
|
}
|
|
74
74
|
return html;
|
|
75
|
-
|
|
75
|
+
`,
|
|
76
|
+
);
|
|
76
77
|
};
|
|
77
78
|
|
|
78
79
|
/**
|
|
@@ -92,7 +93,7 @@ export const renderFast = (iterationNode, array, state, parent, endComment) => {
|
|
|
92
93
|
const stateKeys = iterationNode.runtime.stateKeys;
|
|
93
94
|
|
|
94
95
|
// Build HTML string using batch function
|
|
95
|
-
const stateValues = stateKeys.map(k => state[k]);
|
|
96
|
+
const stateValues = stateKeys.map((k) => state[k]);
|
|
96
97
|
const html = batchFn(array, ...stateValues);
|
|
97
98
|
|
|
98
99
|
// Parse with reusable template element
|
|
@@ -141,7 +142,7 @@ export const updateFast = (iterationNode, newArray, state, startComment, endComm
|
|
|
141
142
|
// Build HTML string using cached batch function
|
|
142
143
|
const batchFn = iterationNode.runtime.batchFn;
|
|
143
144
|
const stateKeys = iterationNode.runtime.stateKeys;
|
|
144
|
-
const stateValues = stateKeys.map(k => state[k]);
|
|
145
|
+
const stateValues = stateKeys.map((k) => state[k]);
|
|
145
146
|
const html = batchFn(newArray, ...stateValues);
|
|
146
147
|
|
|
147
148
|
// Parse and insert
|
package/affected.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
import { resolvePath, deepEqual } from './iteration-utils.js'
|
|
2
|
-
import { extractDependencies } from './conditionals.js'
|
|
3
|
-
import { BINDING_REGEX } from './constants.js'
|
|
1
|
+
import { resolvePath, deepEqual } from './iteration-utils.js';
|
|
2
|
+
import { extractDependencies } from './conditionals.js';
|
|
3
|
+
import { BINDING_REGEX } from './constants.js';
|
|
4
|
+
import { evalInScope } from './utils.js';
|
|
4
5
|
|
|
5
6
|
// Evaluate conditional expression
|
|
6
|
-
const evaluateCondition = (expression, state) =>
|
|
7
|
-
try {
|
|
8
|
-
const keys = Object.keys(state);
|
|
9
|
-
const values = Object.values(state);
|
|
10
|
-
const result = new Function(...keys, `'use strict'; return !!(${expression})`)(...values);
|
|
11
|
-
return !!result;
|
|
12
|
-
} catch (e) {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
};
|
|
7
|
+
const evaluateCondition = (expression, state) => !!evalInScope(expression, state);
|
|
16
8
|
|
|
17
9
|
// Helper function to check if a match references a specific key
|
|
18
10
|
const matchesKey = (matchStr, key) => matchStr === key || matchStr.startsWith(key + '.');
|
|
@@ -29,7 +21,7 @@ const recursive = (tree, state, newState, affected) => {
|
|
|
29
21
|
affected.push({
|
|
30
22
|
type: 'iteration',
|
|
31
23
|
node: tree,
|
|
32
|
-
changeType: 'array'
|
|
24
|
+
changeType: 'array',
|
|
33
25
|
});
|
|
34
26
|
}
|
|
35
27
|
return affected;
|
|
@@ -45,7 +37,7 @@ const recursive = (tree, state, newState, affected) => {
|
|
|
45
37
|
affected.push({
|
|
46
38
|
type: 'conditional',
|
|
47
39
|
node: tree,
|
|
48
|
-
changeType: 'expression'
|
|
40
|
+
changeType: 'expression',
|
|
49
41
|
});
|
|
50
42
|
return affected;
|
|
51
43
|
}
|
|
@@ -70,21 +62,37 @@ const recursive = (tree, state, newState, affected) => {
|
|
|
70
62
|
if (matches.length) {
|
|
71
63
|
const shallowState = Object.keys(state);
|
|
72
64
|
const shallowNewState = Object.keys(newState);
|
|
65
|
+
const isInitialHydration = state === newState;
|
|
73
66
|
|
|
74
67
|
let hasAffected = false;
|
|
75
68
|
const checkedMatches = [];
|
|
76
69
|
|
|
77
70
|
for (const m of matches) {
|
|
78
|
-
const noMatch = !shallowState.some(key => matchesKey(m.inner, key));
|
|
79
|
-
|
|
71
|
+
const noMatch = !shallowState.some((key) => matchesKey(m.inner, key));
|
|
72
|
+
|
|
73
|
+
let shouldAffect = false;
|
|
74
|
+
let relevantKeys = [];
|
|
75
|
+
|
|
76
|
+
if (isInitialHydration) {
|
|
77
|
+
// Initial hydration: affect all matched keys
|
|
78
|
+
relevantKeys = shallowNewState.filter((key) => matchesKey(m.inner, key));
|
|
79
|
+
shouldAffect = noMatch || relevantKeys.length > 0;
|
|
80
|
+
} else {
|
|
81
|
+
// Update: only affect if value changed
|
|
82
|
+
const changedKeys = shallowNewState.filter((key) =>
|
|
83
|
+
matchesKey(m.inner, key) && state[key] !== newState[key]
|
|
84
|
+
);
|
|
85
|
+
relevantKeys = changedKeys.length > 0 ? changedKeys : shallowState.filter((key) => matchesKey(m.inner, key));
|
|
86
|
+
shouldAffect = noMatch || changedKeys.length > 0;
|
|
87
|
+
}
|
|
80
88
|
|
|
81
|
-
if (
|
|
89
|
+
if (shouldAffect) {
|
|
82
90
|
hasAffected = true;
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
checkedMatches.push({
|
|
86
94
|
...m,
|
|
87
|
-
matches:
|
|
95
|
+
matches: relevantKeys,
|
|
88
96
|
});
|
|
89
97
|
}
|
|
90
98
|
|
|
@@ -95,7 +103,7 @@ const recursive = (tree, state, newState, affected) => {
|
|
|
95
103
|
matchInner: m.inner,
|
|
96
104
|
input: m.input,
|
|
97
105
|
matches: m.matches,
|
|
98
|
-
element: tree.element
|
|
106
|
+
element: tree.element,
|
|
99
107
|
});
|
|
100
108
|
}
|
|
101
109
|
}
|
|
@@ -105,6 +113,7 @@ const recursive = (tree, state, newState, affected) => {
|
|
|
105
113
|
if (tree.attributes) {
|
|
106
114
|
const shallowState = Object.keys(state);
|
|
107
115
|
const shallowNewState = Object.keys(newState);
|
|
116
|
+
const isInitialHydration = state === newState;
|
|
108
117
|
|
|
109
118
|
for (const [attrName, attrValue] of Object.entries(tree.attributes)) {
|
|
110
119
|
BINDING_REGEX.lastIndex = 0;
|
|
@@ -115,17 +124,30 @@ const recursive = (tree, state, newState, affected) => {
|
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
for (const m of attrMatches) {
|
|
118
|
-
const noMatch = !shallowState.some(key => matchesKey(m.inner, key));
|
|
119
|
-
|
|
127
|
+
const noMatch = !shallowState.some((key) => matchesKey(m.inner, key));
|
|
128
|
+
|
|
129
|
+
let shouldAffect = false;
|
|
130
|
+
|
|
131
|
+
if (isInitialHydration) {
|
|
132
|
+
// Initial hydration: affect all matched keys
|
|
133
|
+
const newMatches = shallowNewState.filter((key) => matchesKey(m.inner, key));
|
|
134
|
+
shouldAffect = noMatch || newMatches.length > 0;
|
|
135
|
+
} else {
|
|
136
|
+
// Update: only affect if value changed
|
|
137
|
+
const changedKeys = shallowNewState.filter((key) =>
|
|
138
|
+
matchesKey(m.inner, key) && state[key] !== newState[key]
|
|
139
|
+
);
|
|
140
|
+
shouldAffect = noMatch || changedKeys.length > 0;
|
|
141
|
+
}
|
|
120
142
|
|
|
121
|
-
if (
|
|
143
|
+
if (shouldAffect) {
|
|
122
144
|
affected.push({
|
|
123
145
|
type: 'attribute',
|
|
124
146
|
attrName,
|
|
125
147
|
attrValue,
|
|
126
148
|
matchOuter: m.outer,
|
|
127
149
|
matchInner: m.inner,
|
|
128
|
-
element: tree.element
|
|
150
|
+
element: tree.element,
|
|
129
151
|
});
|
|
130
152
|
}
|
|
131
153
|
}
|
|
@@ -146,4 +168,4 @@ const recursive = (tree, state, newState, affected) => {
|
|
|
146
168
|
return affected;
|
|
147
169
|
};
|
|
148
170
|
|
|
149
|
-
export default (tree, state, newState) => recursive(tree, state, newState, [])
|
|
171
|
+
export default (tree, state, newState) => recursive(tree, state, newState, []);
|
package/conditionals.js
CHANGED
|
@@ -2,20 +2,10 @@ import parse from './parse.js';
|
|
|
2
2
|
import affected from './affected.js';
|
|
3
3
|
import hydrate from './hydrate.js';
|
|
4
4
|
import { createScopedState, renderAllIterations, initializeBlock } from './iterate.js';
|
|
5
|
+
import { evalInScope } from './utils.js';
|
|
5
6
|
|
|
6
7
|
// Evaluate conditional expression in state context
|
|
7
|
-
const evaluateCondition = (expression, state) =>
|
|
8
|
-
try {
|
|
9
|
-
const keys = Object.keys(state);
|
|
10
|
-
const values = Object.values(state);
|
|
11
|
-
// Create a function with state keys as parameters and evaluate the expression
|
|
12
|
-
const result = new Function(...keys, `'use strict'; return !!(${expression})`)(...values);
|
|
13
|
-
return !!result; // Coerce to boolean
|
|
14
|
-
} catch (e) {
|
|
15
|
-
console.warn(`Error evaluating condition "${expression}":`, e);
|
|
16
|
-
return false; // Default to false on error
|
|
17
|
-
}
|
|
18
|
-
};
|
|
8
|
+
const evaluateCondition = (expression, state) => !!evalInScope(expression, state);
|
|
19
9
|
|
|
20
10
|
// Extract state dependencies from an expression
|
|
21
11
|
// e.g., "count > 5" → ["count"]
|
|
@@ -44,7 +34,7 @@ export const renderAllConditionals = (tree, state, linkList, parentScope = {}) =
|
|
|
44
34
|
|
|
45
35
|
// Recursively render conditionals in child nodes
|
|
46
36
|
if (tree.children) {
|
|
47
|
-
Object.keys(tree.children).forEach(key => {
|
|
37
|
+
Object.keys(tree.children).forEach((key) => {
|
|
48
38
|
const child = tree.children[key];
|
|
49
39
|
if (typeof child === 'object' && child !== null) {
|
|
50
40
|
renderAllConditionals(child, state, linkList, parentScope);
|
|
@@ -104,15 +94,18 @@ const mountBranch = (node, branchData, state, linkList, parentScope) => {
|
|
|
104
94
|
const parent = startComment.parentNode;
|
|
105
95
|
|
|
106
96
|
// Create scoped state (with parent scope if inside iteration)
|
|
107
|
-
const scopedState =
|
|
108
|
-
? createScopedState(state, parentScope)
|
|
109
|
-
: state;
|
|
97
|
+
const scopedState =
|
|
98
|
+
Object.keys(parentScope).length > 0 ? createScopedState(state, parentScope) : state;
|
|
110
99
|
|
|
111
100
|
// Initialize block (clone, parse, hydrate)
|
|
112
|
-
const {
|
|
101
|
+
const {
|
|
102
|
+
element: firstElement,
|
|
103
|
+
tree: branchTree,
|
|
104
|
+
clonedNodes,
|
|
105
|
+
} = initializeBlock(templateContent, scopedState);
|
|
113
106
|
|
|
114
107
|
// Insert cloned nodes into DOM
|
|
115
|
-
clonedNodes.forEach(clonedNode => parent.insertBefore(clonedNode, endComment));
|
|
108
|
+
clonedNodes.forEach((clonedNode) => parent.insertBefore(clonedNode, endComment));
|
|
116
109
|
|
|
117
110
|
// Recursively render any nested iterations and conditionals
|
|
118
111
|
if (branchTree) {
|
|
@@ -124,7 +117,7 @@ const mountBranch = (node, branchData, state, linkList, parentScope) => {
|
|
|
124
117
|
node.runtime.activeInstance = {
|
|
125
118
|
branch: branchData,
|
|
126
119
|
nodes: clonedNodes,
|
|
127
|
-
parsedTree: branchTree
|
|
120
|
+
parsedTree: branchTree,
|
|
128
121
|
};
|
|
129
122
|
};
|
|
130
123
|
|
|
@@ -135,7 +128,7 @@ const unmountBranch = (node) => {
|
|
|
135
128
|
if (!activeInstance) return;
|
|
136
129
|
|
|
137
130
|
// Remove all nodes from DOM
|
|
138
|
-
activeInstance.nodes.forEach(domNode => {
|
|
131
|
+
activeInstance.nodes.forEach((domNode) => {
|
|
139
132
|
if (domNode.parentNode) {
|
|
140
133
|
domNode.parentNode.removeChild(domNode);
|
|
141
134
|
}
|
|
@@ -170,13 +163,11 @@ export const updateConditional = (node, newState, oldState, linkList, parentScop
|
|
|
170
163
|
const { activeInstance } = node.runtime;
|
|
171
164
|
|
|
172
165
|
if (activeInstance && activeInstance.parsedTree) {
|
|
173
|
-
const scopedState =
|
|
174
|
-
? createScopedState(newState, parentScope)
|
|
175
|
-
: newState;
|
|
166
|
+
const scopedState =
|
|
167
|
+
Object.keys(parentScope).length > 0 ? createScopedState(newState, parentScope) : newState;
|
|
176
168
|
|
|
177
169
|
const affectedElements = affected(activeInstance.parsedTree, oldState, scopedState);
|
|
178
170
|
hydrate(affectedElements, scopedState);
|
|
179
171
|
}
|
|
180
172
|
}
|
|
181
173
|
};
|
|
182
|
-
|