@ape-egg/vibe 1.3.0 → 1.3.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/CHANGELOG.md +26 -0
- package/ROADMAP.md +289 -0
- package/boot.js +46 -0
- package/component.js +59 -0
- package/llms.txt +304 -0
- package/package.json +2 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.3.2] - 2025-02-06
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Package completeness**: Added missing `boot.js` to npm package
|
|
8
|
+
- File was missing from the "files" allowlist in package.json
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Package strategy**: Switched from allowlist to denylist approach
|
|
13
|
+
- Removed "files" field from package.json
|
|
14
|
+
- Added `.npmignore` to exclude build artifacts (`target/`) and dev files
|
|
15
|
+
- Ensures all source files are included without manual maintenance
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## [1.3.1] - 2025-02-06
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Package exports**: Added missing `./component` export to package.json
|
|
24
|
+
- Enables proper import: `import component from '@ape-egg/vibe/component'`
|
|
25
|
+
- Previously `component.js` was included in package files but not exposed via exports field
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
3
29
|
## [1.3.0] - 2025-02-06
|
|
4
30
|
|
|
5
31
|
### Added
|
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*
|
package/boot.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Shared boot mechanism for vibe
|
|
2
|
+
// Used by both index.js (global state) and component.js (component state)
|
|
3
|
+
|
|
4
|
+
import main from './runtime/index.js';
|
|
5
|
+
|
|
6
|
+
let bootQueued = false;
|
|
7
|
+
let booted = false;
|
|
8
|
+
|
|
9
|
+
export const ensureBoot = () => {
|
|
10
|
+
if (booted || bootQueued) return;
|
|
11
|
+
bootQueued = true;
|
|
12
|
+
|
|
13
|
+
// Auto-boot in next microtask if no explicit boot
|
|
14
|
+
queueMicrotask(() => {
|
|
15
|
+
if (!booted) {
|
|
16
|
+
boot();
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const boot = () => {
|
|
22
|
+
if (booted) {
|
|
23
|
+
return window.$;
|
|
24
|
+
}
|
|
25
|
+
booted = true;
|
|
26
|
+
|
|
27
|
+
// Merge all state: global + components
|
|
28
|
+
const globalState = window.__vibeGlobalState || {};
|
|
29
|
+
const componentStates = window.__vibeComponents || {};
|
|
30
|
+
|
|
31
|
+
const mergedState = {
|
|
32
|
+
...globalState,
|
|
33
|
+
...componentStates,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Get config and targetSelector (first caller wins)
|
|
37
|
+
const config = window.__vibeConfig || {};
|
|
38
|
+
const targetSelector = window.__vibeTargetSelector || '';
|
|
39
|
+
|
|
40
|
+
// Boot with merged state
|
|
41
|
+
window.$ = main(mergedState, config, targetSelector);
|
|
42
|
+
|
|
43
|
+
return window.$;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const isBooted = () => booted;
|
package/component.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Component state entry point
|
|
2
|
+
// Usage:
|
|
3
|
+
// <component>
|
|
4
|
+
// <script type="module">
|
|
5
|
+
// import component from 'vibe/component.js';
|
|
6
|
+
// component({ count: 0 }, { debug: false }, 'body');
|
|
7
|
+
// </script>
|
|
8
|
+
// <div>@[this.count]</div>
|
|
9
|
+
// </component>
|
|
10
|
+
//
|
|
11
|
+
// Or with class:
|
|
12
|
+
// <div class="component">...</div>
|
|
13
|
+
|
|
14
|
+
import { generateComponentId } from './runtime/component-state.js';
|
|
15
|
+
import { ensureBoot } from './boot.js';
|
|
16
|
+
|
|
17
|
+
const component = (state = {}, config, targetSelector) => {
|
|
18
|
+
// Find the first unprocessed INLINE component wrapper
|
|
19
|
+
// Skip <component src=""> (fetched components) - they don't need state tagging
|
|
20
|
+
// Scripts execute in DOM order, so we claim wrappers in DOM order too
|
|
21
|
+
// Supports: <component> or <div class="component">
|
|
22
|
+
const allWrappers = Array.from(document.querySelectorAll('component:not([src]), div.component:not([src])'));
|
|
23
|
+
const wrapper = allWrappers.find(el => !el.hasAttribute('data-vibe-component-id'));
|
|
24
|
+
|
|
25
|
+
if (!wrapper) {
|
|
26
|
+
console.warn('[vibe] component() must be called inside <component> or <div class="component">');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Generate unique component ID
|
|
31
|
+
const componentId = generateComponentId();
|
|
32
|
+
|
|
33
|
+
// Tag only the wrapper element
|
|
34
|
+
// All descendants will find it via element.closest('[data-vibe-component-id]')
|
|
35
|
+
wrapper.setAttribute('data-vibe-component-id', componentId);
|
|
36
|
+
|
|
37
|
+
// Register component state in shared registry
|
|
38
|
+
if (!window.__vibeComponents) {
|
|
39
|
+
window.__vibeComponents = {};
|
|
40
|
+
}
|
|
41
|
+
window.__vibeComponents[componentId] = state;
|
|
42
|
+
|
|
43
|
+
// Store config (first caller wins)
|
|
44
|
+
if (config && !window.__vibeConfig) {
|
|
45
|
+
window.__vibeConfig = config;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Store targetSelector (first caller wins)
|
|
49
|
+
if (targetSelector && !window.__vibeTargetSelector) {
|
|
50
|
+
window.__vibeTargetSelector = targetSelector;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Ensure boot happens
|
|
54
|
+
ensureBoot();
|
|
55
|
+
|
|
56
|
+
return componentId;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default component;
|
package/llms.txt
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# Vibe - Complete Documentation
|
|
2
|
+
|
|
3
|
+
> Runtime-first reactivity. No virtual DOM. No build step.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Vibe is a lightweight reactive library that uses Proxy-based state and MutationObserver for fine-grained DOM updates. It works directly in the browser with zero compilation required.
|
|
8
|
+
|
|
9
|
+
**Key characteristics:**
|
|
10
|
+
- Proxy-based reactive state (`window.$`)
|
|
11
|
+
- Deep reactivity (nested mutations automatically trigger updates)
|
|
12
|
+
- Surgical DOM updates (only affected elements re-render)
|
|
13
|
+
- MutationObserver for dynamic element tracking
|
|
14
|
+
- Works with vanilla HTML - no special file format
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @ape-egg/vibe
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<script type="module">
|
|
26
|
+
import state from "@ape-egg/vibe";
|
|
27
|
+
window.$ = state({ name: "World", count: 0 });
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<h1>Hello, @[name]!</h1>
|
|
31
|
+
<button onclick="$.count++">Clicked @[count] times</button>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Core Syntax
|
|
35
|
+
|
|
36
|
+
### Reactive Bindings
|
|
37
|
+
|
|
38
|
+
Use `@[property]` syntax anywhere in HTML or CSS:
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<!-- Text content -->
|
|
42
|
+
<div>@[firstName]</div>
|
|
43
|
+
|
|
44
|
+
<!-- Expressions -->
|
|
45
|
+
<div>@[firstName + ' ' + lastName]</div>
|
|
46
|
+
<div>@[count * 2]</div>
|
|
47
|
+
|
|
48
|
+
<!-- Attributes -->
|
|
49
|
+
<input value="@[inputValue]">
|
|
50
|
+
<button disabled="@[isLoading]">Submit</button>
|
|
51
|
+
|
|
52
|
+
<!-- CSS -->
|
|
53
|
+
<style>
|
|
54
|
+
.box { background: @[themeColor]; }
|
|
55
|
+
</style>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
⚠️ **Important**: Bindings are evaluated using `new Function()`. Do not bind untrusted user input.
|
|
59
|
+
|
|
60
|
+
### State Access
|
|
61
|
+
|
|
62
|
+
State is accessed globally via `window.$`:
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
// Read
|
|
66
|
+
console.log($.firstName);
|
|
67
|
+
|
|
68
|
+
// Write (triggers re-render)
|
|
69
|
+
$.firstName = "John";
|
|
70
|
+
|
|
71
|
+
// Increment
|
|
72
|
+
$.count++;
|
|
73
|
+
|
|
74
|
+
// Deep mutations (also trigger re-render)
|
|
75
|
+
$.user.profile.name = "Alice";
|
|
76
|
+
$.todos[2].completed = true;
|
|
77
|
+
$.config.theme.colors.primary = "#007bff";
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Vibe uses recursive proxies to detect changes at any nesting level automatically.
|
|
81
|
+
|
|
82
|
+
## Control Flow
|
|
83
|
+
|
|
84
|
+
### Iteration
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<!-- each items as item -->
|
|
88
|
+
<li>@[item]</li>
|
|
89
|
+
<!-- /each -->
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
With index:
|
|
93
|
+
|
|
94
|
+
```html
|
|
95
|
+
<!-- each items as item, index -->
|
|
96
|
+
<li>@[index]: @[item]</li>
|
|
97
|
+
<!-- /each -->
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Nested Iteration
|
|
101
|
+
|
|
102
|
+
Use dot paths for nested arrays:
|
|
103
|
+
|
|
104
|
+
```html
|
|
105
|
+
<!-- each categories as category -->
|
|
106
|
+
<h2>@[category.name]</h2>
|
|
107
|
+
<!-- each category.items as item -->
|
|
108
|
+
<span>@[item.name]</span>
|
|
109
|
+
<!-- /each -->
|
|
110
|
+
<!-- /each -->
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Conditionals
|
|
114
|
+
|
|
115
|
+
```html
|
|
116
|
+
<!-- if isLoggedIn -->
|
|
117
|
+
<span>Welcome, @[username]!</span>
|
|
118
|
+
<!-- else -->
|
|
119
|
+
<span>Please log in</span>
|
|
120
|
+
<!-- /if -->
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Conditionals can be nested inside iterations and vice versa.
|
|
124
|
+
|
|
125
|
+
## Special Attributes
|
|
126
|
+
|
|
127
|
+
### Dehydrate
|
|
128
|
+
|
|
129
|
+
Skip reactive processing for an element and its children:
|
|
130
|
+
|
|
131
|
+
```html
|
|
132
|
+
<code dehydrate>@[this] displays literally, not parsed</code>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Use cases:
|
|
136
|
+
- Displaying `@[...]` syntax in documentation
|
|
137
|
+
- Static content that shouldn't be reactive
|
|
138
|
+
- Performance optimization for large static sections
|
|
139
|
+
|
|
140
|
+
### Boolean Attributes
|
|
141
|
+
|
|
142
|
+
Attributes not in the value whitelist are removed when falsy:
|
|
143
|
+
|
|
144
|
+
```html
|
|
145
|
+
<button disabled="@[isLoading]">Submit</button>
|
|
146
|
+
<!-- When isLoading is false, disabled attribute is removed entirely -->
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Events
|
|
150
|
+
|
|
151
|
+
Use standard inline event handlers:
|
|
152
|
+
|
|
153
|
+
```html
|
|
154
|
+
<button onclick="$.count++">Increment</button>
|
|
155
|
+
<input oninput="$.text = this.value">
|
|
156
|
+
<form onsubmit="event.preventDefault(); handleSubmit()">
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Styling
|
|
160
|
+
|
|
161
|
+
### CSS Bindings
|
|
162
|
+
|
|
163
|
+
Reactive values work inside `<style>` tags:
|
|
164
|
+
|
|
165
|
+
```html
|
|
166
|
+
<style>
|
|
167
|
+
.box {
|
|
168
|
+
background: @[backgroundColor];
|
|
169
|
+
color: @[textColor];
|
|
170
|
+
width: @[width]px;
|
|
171
|
+
}
|
|
172
|
+
</style>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Preventing FOUC
|
|
176
|
+
|
|
177
|
+
Hide content until hydration completes:
|
|
178
|
+
|
|
179
|
+
```html
|
|
180
|
+
<body style="visibility: hidden;">
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Or use the included CSS:
|
|
184
|
+
|
|
185
|
+
```html
|
|
186
|
+
<link rel="stylesheet" href="@ape-egg/vibe/vibe.css">
|
|
187
|
+
<body vibe-fouc>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Dynamic Elements
|
|
191
|
+
|
|
192
|
+
Elements added via JavaScript are automatically hydrated through MutationObserver:
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
const div = document.createElement('div');
|
|
196
|
+
div.innerHTML = '<span>Hello, @[name]!</span>';
|
|
197
|
+
document.body.appendChild(div);
|
|
198
|
+
// Automatically becomes reactive
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## API Reference
|
|
202
|
+
|
|
203
|
+
### `state(initialState, afterUpdate?)`
|
|
204
|
+
|
|
205
|
+
Creates reactive state and initializes the framework.
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
import state from "@ape-egg/vibe";
|
|
209
|
+
|
|
210
|
+
window.$ = state(
|
|
211
|
+
{ count: 0, user: { name: "Alice" } },
|
|
212
|
+
(newState, oldState) => {
|
|
213
|
+
console.log("State updated:", newState);
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Parameters:**
|
|
219
|
+
- `initialState` - Object containing initial state values
|
|
220
|
+
- `afterUpdate` - Optional callback after each state change (receives read-only snapshots)
|
|
221
|
+
|
|
222
|
+
**Returns:** Proxy object for reactive state access
|
|
223
|
+
|
|
224
|
+
## Scoped Variables
|
|
225
|
+
|
|
226
|
+
Inside `<!-- each -->` blocks, these variables are available:
|
|
227
|
+
- `item` (or custom name) - current array element
|
|
228
|
+
- `index` (or custom name) - current index
|
|
229
|
+
- Parent state remains accessible via `$`
|
|
230
|
+
|
|
231
|
+
```html
|
|
232
|
+
<!-- each users as user, i -->
|
|
233
|
+
<div>@[i]: @[user.name] (total: @[users.length])</div>
|
|
234
|
+
<!-- /each -->
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Scoped State (Internal)
|
|
238
|
+
|
|
239
|
+
Each iteration instance stores a `scopedState` - a Proxy wrapper that provides access to both local variables (`item`, `index`) and global state. This enables reactive updates inside iterations:
|
|
240
|
+
|
|
241
|
+
```html
|
|
242
|
+
<!-- each menuItems as item -->
|
|
243
|
+
<a completed="@[tutorialProgress[item.id]]">
|
|
244
|
+
@[item.label]
|
|
245
|
+
</a>
|
|
246
|
+
<!-- /each -->
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
When `tutorialProgress` changes, Vibe:
|
|
250
|
+
1. Finds affected elements inside iteration instances
|
|
251
|
+
2. Uses that instance's `scopedState` for evaluation
|
|
252
|
+
3. Can access both `item` (local) and `tutorialProgress` (global)
|
|
253
|
+
4. Updates only the affected elements without re-rendering the entire iteration
|
|
254
|
+
|
|
255
|
+
## Architecture
|
|
256
|
+
|
|
257
|
+
Vibe consists of these core modules:
|
|
258
|
+
|
|
259
|
+
- **state.js** - Proxy-based reactive state container with recursive proxies for deep reactivity
|
|
260
|
+
- **parse.js** - DOM parser that finds `@[...]` bindings
|
|
261
|
+
- **link.js** - Maps elements to parsed tree nodes
|
|
262
|
+
- **hydrate.js** - Updates DOM with current state values (supports scoped state for iterations)
|
|
263
|
+
- **affected.js** - Determines which elements need updating (walks iteration instances with scoped state)
|
|
264
|
+
- **iterate.js** - Array rendering with efficient diffing (stores scoped state per instance)
|
|
265
|
+
- **conditionals.js** - Conditional block rendering
|
|
266
|
+
|
|
267
|
+
## How It Works
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
1. state() initializes the Proxy and framework
|
|
271
|
+
2. parse.js scans DOM for @[...], <!-- each -->, <!-- if -->
|
|
272
|
+
3. link.js maps elements to the parsed tree
|
|
273
|
+
4. hydrate.js replaces bindings with values
|
|
274
|
+
5. iterate.js renders <!-- each --> loops
|
|
275
|
+
6. conditionals.js renders <!-- if --> blocks
|
|
276
|
+
7. MutationObserver watches for new elements
|
|
277
|
+
8. On state change: affected.js finds changed elements → hydrate.js updates them
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Current Limitations
|
|
281
|
+
|
|
282
|
+
- **No computed values**: Derived state must be calculated manually
|
|
283
|
+
- **No two-way binding sugar**: Must wire input events manually
|
|
284
|
+
- **Expression security**: `new Function()` evaluation - don't bind untrusted input
|
|
285
|
+
|
|
286
|
+
## Best Practices
|
|
287
|
+
|
|
288
|
+
1. **Initialize state before DOM**: Place `<script>` in `<head>` or before reactive elements
|
|
289
|
+
2. **Use dehydrate for docs**: When showing `@[...]` syntax examples
|
|
290
|
+
3. **Prevent FOUC**: Use `visibility: hidden` on body until hydration
|
|
291
|
+
4. **Keep expressions simple**: Complex logic belongs in JavaScript, not templates
|
|
292
|
+
5. **Mutate freely**: Deep reactivity means `$.user.name = "New"` just works - no spread operators needed
|
|
293
|
+
|
|
294
|
+
## Browser Support
|
|
295
|
+
|
|
296
|
+
Modern browsers with:
|
|
297
|
+
- Proxy (ES6)
|
|
298
|
+
- MutationObserver
|
|
299
|
+
- ES Modules
|
|
300
|
+
|
|
301
|
+
## Resources
|
|
302
|
+
|
|
303
|
+
- **Homepage**: https://vibe.korte.kim
|
|
304
|
+
- **npm**: https://www.npmjs.com/package/@ape-egg/vibe
|
package/package.json
CHANGED
|
@@ -1,33 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ape-egg/vibe",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime-first reactivity with optional compiler",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"homepage": "https://vibe.korte.kim",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./index.js",
|
|
10
|
+
"./component": "./component.js",
|
|
10
11
|
"./runtime": "./runtime/index.js",
|
|
11
12
|
"./compiler": "./compiler/bin/vibe-compile.js"
|
|
12
13
|
},
|
|
13
14
|
"bin": {
|
|
14
15
|
"vibe": "./compiler/bin/vibe-compile.js"
|
|
15
16
|
},
|
|
16
|
-
"files": [
|
|
17
|
-
"index.js",
|
|
18
|
-
"vibe.css",
|
|
19
|
-
"runtime/",
|
|
20
|
-
"compiler/bin/",
|
|
21
|
-
"compiler/native/",
|
|
22
|
-
"compiler/src/Cargo.lock",
|
|
23
|
-
"compiler/src/Cargo.toml",
|
|
24
|
-
"compiler/src/compiler/",
|
|
25
|
-
"compiler/src/config.rs",
|
|
26
|
-
"compiler/src/main.rs",
|
|
27
|
-
"compiler/src/parser/",
|
|
28
|
-
"README.md",
|
|
29
|
-
"CHANGELOG.md"
|
|
30
|
-
],
|
|
31
17
|
"keywords": [
|
|
32
18
|
"reactive",
|
|
33
19
|
"framework",
|