@ape-egg/vibe 1.0.3 → 1.1.1

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +228 -23
  3. package/compiler/bin/vibe-compile.js +109 -0
  4. package/compiler/native/.gitkeep +0 -0
  5. package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
  6. package/compiler/src/Cargo.lock +1885 -0
  7. package/compiler/src/Cargo.toml +29 -0
  8. package/compiler/src/compiler/compile.rs +1209 -0
  9. package/compiler/src/compiler/mod.rs +5 -0
  10. package/compiler/src/config.rs +184 -0
  11. package/compiler/src/main.rs +284 -0
  12. package/compiler/src/parser/element.rs +96 -0
  13. package/compiler/src/parser/html.rs +339 -0
  14. package/compiler/src/parser/mod.rs +8 -0
  15. package/index.js +2 -233
  16. package/package.json +26 -3
  17. package/{affected.js → runtime/affected.js} +66 -14
  18. package/runtime/cleanup.js +59 -0
  19. package/runtime/component.js +116 -0
  20. package/{conditionals.js → runtime/conditionals.js} +27 -23
  21. package/{constants.js → runtime/constants.js} +23 -3
  22. package/runtime/debug.js +91 -0
  23. package/{hydrate.js → runtime/hydrate.js} +58 -20
  24. package/runtime/index.js +614 -0
  25. package/{iterate.js → runtime/iterate.js} +53 -45
  26. package/{iteration-utils.js → runtime/iteration-utils.js} +11 -1
  27. package/{parse.js → runtime/parse.js} +37 -7
  28. package/runtime/state.js +52 -0
  29. package/{utils.js → runtime/utils.js} +13 -0
  30. package/llms.txt +0 -279
  31. package/state.js +0 -26
  32. /package/{_vibe-compiled-iteration-batch.js → runtime/_vibe-compiled-iteration-batch.js} +0 -0
  33. /package/{link.js → runtime/manifest.js} +0 -0
  34. /package/{vibe.css → runtime/vibe.css} +0 -0
@@ -126,37 +126,30 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null) =
126
126
  let clonedNodes = [];
127
127
  let firstElement = null;
128
128
 
129
- // Fast path: clone cached tree with new element references
130
- if (cachedTree) {
131
- // Clone nodes directly
132
- for (let i = 0; i < templateNodes.length; i++) {
133
- const cloned = templateNodes[i].cloneNode(true);
134
- clonedNodes.push(cloned);
135
- if (!firstElement && cloned.nodeType === 1) {
136
- firstElement = cloned;
137
- }
138
- }
139
- // Use virtual root to match template structure (avoids creating DOM container)
140
- // cloneTreeWithElements only needs childNodes property
141
- tree = cloneTreeWithElements(cachedTree, { childNodes: clonedNodes });
142
- } else {
143
- // Slow path: clone and parse from scratch
144
- for (let i = 0; i < templateNodes.length; i++) {
145
- const cloned = templateNodes[i].cloneNode(true);
146
- clonedNodes.push(cloned);
147
- if (!firstElement && cloned.nodeType === 1) {
148
- firstElement = cloned;
149
- }
129
+ // Always use slow path: clone and parse from scratch
130
+ // TODO: Re-enable fast path once cloneTreeWithElements properly handles nested conditionals
131
+
132
+ // Create a container for parsing (to capture all nodes including comment nodes like <!-- if -->)
133
+ const parseContainer = document.createElement('div');
134
+
135
+ // Clone all template nodes into the container
136
+ for (let i = 0; i < templateNodes.length; i++) {
137
+ const cloned = templateNodes[i].cloneNode(true);
138
+ parseContainer.appendChild(cloned);
139
+ if (!firstElement && cloned.nodeType === 1) {
140
+ firstElement = cloned;
150
141
  }
142
+ }
151
143
 
152
- // Fallback for text-only templates
153
- if (!firstElement && clonedNodes.length > 0) {
154
- const container = document.createElement('span');
155
- clonedNodes.forEach((node) => container.appendChild(node.cloneNode(true)));
156
- firstElement = container;
157
- }
144
+ // Parse the entire container (includes all nodes + conditionals)
145
+ tree = parse(parseContainer);
146
+
147
+ // Extract the cloned nodes from the container (these are the same nodes the tree references)
148
+ clonedNodes = Array.from(parseContainer.childNodes);
158
149
 
159
- tree = firstElement ? parse(firstElement) : null;
150
+ // If no firstElement found, use parseContainer as fallback
151
+ if (!firstElement) {
152
+ firstElement = parseContainer;
160
153
  }
161
154
 
162
155
  if (tree) {
@@ -229,11 +222,13 @@ export const createScopedState = (globalState, localVars, parentScope = {}) => {
229
222
  };
230
223
 
231
224
  // Render all iterations in the parsed tree
232
- export const renderAllIterations = (tree, state, linkList, parentScope = {}) => {
225
+ export const renderAllIterations = (tree, state, manifest, parentScope = {}) => {
226
+ let count = 0;
227
+
233
228
  // If this is an iteration node, render it
234
229
  if (tree.type === 'iteration') {
235
- renderIteration(tree, state, linkList, parentScope);
236
- return;
230
+ renderIteration(tree, state, manifest, parentScope);
231
+ return 1;
237
232
  }
238
233
 
239
234
  // Recursively render iterations in child nodes
@@ -241,10 +236,12 @@ export const renderAllIterations = (tree, state, linkList, parentScope = {}) =>
241
236
  for (const key in tree.children) {
242
237
  const child = tree.children[key];
243
238
  if (child && typeof child === 'object') {
244
- renderAllIterations(child, state, linkList, parentScope);
239
+ count += renderAllIterations(child, state, manifest, parentScope);
245
240
  }
246
241
  }
247
242
  }
243
+
244
+ return count;
248
245
  };
249
246
 
250
247
  // Callback for rendering conditionals - set by conditionals.js to avoid circular import
@@ -254,13 +251,20 @@ export const setRenderAllConditionals = (fn) => {
254
251
  };
255
252
 
256
253
  // Initial render of an iteration block
257
- export const renderIteration = (iterationNode, state, linkList, parentScope = {}) => {
254
+ export const renderIteration = (iterationNode, state, manifest, parentScope = {}) => {
258
255
  const { arrayPath, itemAlias, indexAlias, template, startComment, endComment } =
259
256
  iterationNode.meta;
260
257
 
261
258
  // Already rendered - updates go through updateIteration
262
259
  if (iterationNode.runtime.instances?.length > 0) return;
263
260
 
261
+ // Check if this iteration has already been rendered
262
+ // We use a marker on the startComment node itself (survives re-parsing)
263
+ // @ts-ignore - adding custom property to comment node
264
+ if (startComment.__vibeRendered) {
265
+ return;
266
+ }
267
+
264
268
  // Remove template nodes from DOM on first render
265
269
  if (!iterationNode.runtime.templateRemoved) {
266
270
  let node = startComment.nextSibling;
@@ -304,18 +308,22 @@ export const renderIteration = (iterationNode, state, linkList, parentScope = {}
304
308
  // Recursively render nested iterations and conditionals
305
309
  if (tree) {
306
310
  const nestedScope = { ...parentScope, ...localVars };
307
- renderAllIterations(tree, scopedState, linkList, nestedScope);
308
- _renderAllConditionals(tree, scopedState, linkList, nestedScope);
311
+ renderAllIterations(tree, scopedState, manifest, nestedScope);
312
+ _renderAllConditionals(tree, scopedState, manifest, nestedScope);
309
313
  }
310
314
 
311
- instances.push({ element, tree, item, index: i, clonedNodes });
315
+ instances.push({ element, tree, item, index: i, clonedNodes, scopedState });
312
316
  }
313
317
 
314
318
  iterationNode.runtime.instances = instances;
319
+
320
+ // Mark comment as rendered (survives re-parsing since comment nodes persist in DOM)
321
+ // @ts-ignore - adding custom property to comment node
322
+ startComment.__vibeRendered = true;
315
323
  };
316
324
 
317
325
  // Update an iteration block when array changes
318
- export const updateIteration = (iterationNode, newState, oldState, linkList, parentScope = {}) => {
326
+ export const updateIteration = (iterationNode, newState, oldState, manifest, parentScope = {}) => {
319
327
  if (!iterationNode.runtime.instances || !iterationNode.runtime.templateRemoved) return;
320
328
 
321
329
  const { arrayPath, template, startComment, endComment } = iterationNode.meta;
@@ -348,13 +356,13 @@ export const updateIteration = (iterationNode, newState, oldState, linkList, par
348
356
  removeInstance(iterationNode, op.index);
349
357
  break;
350
358
  case 'ADD':
351
- addInstance(iterationNode, op.item, op.index, newState, linkList, parentScope);
359
+ addInstance(iterationNode, op.item, op.index, newState, manifest, parentScope);
352
360
  break;
353
361
  case 'MOVE':
354
362
  moveInstance(iterationNode, op.from, op.to);
355
363
  break;
356
364
  case 'UPDATE':
357
- updateInstance(iterationNode, op.index, op.item, newState, linkList, parentScope);
365
+ updateInstance(iterationNode, op.index, op.item, newState, manifest, parentScope);
358
366
  break;
359
367
  }
360
368
  });
@@ -365,7 +373,7 @@ export const updateIteration = (iterationNode, newState, oldState, linkList, par
365
373
  };
366
374
 
367
375
  // Add a new instance at the specified index
368
- const addInstance = (iterationNode, item, index, state, linkList, parentScope) => {
376
+ const addInstance = (iterationNode, item, index, state, manifest, parentScope) => {
369
377
  const { itemAlias, indexAlias, template, startComment, endComment } = iterationNode.meta;
370
378
 
371
379
  const localVars = { [itemAlias]: item, [indexAlias]: index };
@@ -387,8 +395,8 @@ const addInstance = (iterationNode, item, index, state, linkList, parentScope) =
387
395
  // Recursively render nested iterations and conditionals
388
396
  if (tree) {
389
397
  const nestedScope = { ...parentScope, ...localVars };
390
- renderAllIterations(tree, scopedState, linkList, nestedScope);
391
- _renderAllConditionals(tree, scopedState, linkList, nestedScope);
398
+ renderAllIterations(tree, scopedState, manifest, nestedScope);
399
+ _renderAllConditionals(tree, scopedState, manifest, nestedScope);
392
400
  }
393
401
 
394
402
  iterationNode.runtime.instances.splice(index, 0, { element, tree, item, index, clonedNodes });
@@ -432,7 +440,7 @@ const moveInstance = (iterationNode, fromIndex, toIndex) => {
432
440
  };
433
441
 
434
442
  // Update an instance with new item data
435
- const updateInstance = (iterationNode, index, newItem, state, linkList, parentScope = {}) => {
443
+ const updateInstance = (iterationNode, index, newItem, state, manifest, parentScope = {}) => {
436
444
  if (index < 0 || index >= iterationNode.runtime.instances.length) return;
437
445
 
438
446
  const { itemAlias, indexAlias, template } = iterationNode.meta;
@@ -455,8 +463,8 @@ const updateInstance = (iterationNode, index, newItem, state, linkList, parentSc
455
463
  // Recursively render nested iterations and conditionals
456
464
  if (tree) {
457
465
  const nestedScope = { ...parentScope, ...localVars };
458
- renderAllIterations(tree, scopedState, linkList, nestedScope);
459
- _renderAllConditionals(tree, scopedState, linkList, nestedScope);
466
+ renderAllIterations(tree, scopedState, manifest, nestedScope);
467
+ _renderAllConditionals(tree, scopedState, manifest, nestedScope);
460
468
  }
461
469
 
462
470
  // Update instance
@@ -59,7 +59,17 @@ export const findConditionalEnd = (nodes, startIndex) => {
59
59
  }
60
60
  }
61
61
 
62
- throw new Error('Unmatched <!-- if --> comment: missing <!-- /if -->');
62
+ // Build error message with context
63
+ const nodeTypes = nodes.slice(startIndex - 1, Math.min(startIndex + 10, nodes.length)).map((n, idx) => {
64
+ const actualIdx = startIndex - 1 + idx;
65
+ const prefix = actualIdx === startIndex - 1 ? '→ ' : ' ';
66
+ if (n.nodeName === '#comment') {
67
+ return `${prefix}[${actualIdx}] #comment: "${n.textContent.trim()}"`;
68
+ }
69
+ return `${prefix}[${actualIdx}] ${n.nodeName}`;
70
+ });
71
+
72
+ throw new Error(`Unmatched <!-- if --> comment: missing <!-- /if -->\nSearching from index ${startIndex} in ${nodes.length} nodes\nContext:\n${nodeTypes.join('\n')}`);
63
73
  };
64
74
 
65
75
  // Generate stable hash for objects
@@ -19,7 +19,7 @@ const parseHTML = (children, rootKey = undefined) =>
19
19
  return rootKey || `${s}${`\$[${innerNodeIdentifier}]`}`;
20
20
  }, '');
21
21
 
22
- const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
22
+ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats = { skipped: 0 }) => {
23
23
  let result = {};
24
24
 
25
25
  for (let i = 0; i < children.length; i++) {
@@ -31,7 +31,10 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
31
31
 
32
32
  // Skip non-reactive elements and dehydrated elements
33
33
  if (NON_REACTIVE_ELEMENTS.includes(nodeName)) continue;
34
- if (element.hasAttribute?.('dehydrate')) continue;
34
+ if (element.hasAttribute?.('dehydrate')) {
35
+ stats.skipped++;
36
+ continue;
37
+ }
35
38
 
36
39
  // Handle iteration comments
37
40
  if (nodeName === '#comment') {
@@ -54,7 +57,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
54
57
  });
55
58
 
56
59
  // Parse the template recursively
57
- const templateParsed = recursive([...templateContainer.childNodes]);
60
+ const templateParsed = recursive([...templateContainer.childNodes], undefined, new Set(), stats);
58
61
 
59
62
  // Store iteration metadata
60
63
  const iterationKey = `iteration_${hash()}`;
@@ -115,7 +118,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
115
118
  });
116
119
 
117
120
  // Parse the true branch recursively
118
- const trueBranchParsed = recursive([...trueBranchContainer.childNodes]);
121
+ const trueBranchParsed = recursive([...trueBranchContainer.childNodes], undefined, new Set(), stats);
119
122
 
120
123
  // Extract false branch nodes if else exists
121
124
  let falseBranchParsed = null;
@@ -126,7 +129,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
126
129
  falseBranchNodes.forEach((node) => {
127
130
  falseBranchContainer.appendChild(node.cloneNode(true));
128
131
  });
129
- falseBranchParsed = recursive([...falseBranchContainer.childNodes]);
132
+ falseBranchParsed = recursive([...falseBranchContainer.childNodes], undefined, new Set(), stats);
130
133
  }
131
134
 
132
135
  // Store conditional metadata
@@ -184,17 +187,28 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
184
187
 
185
188
  // Check for attribute bindings
186
189
  const attributes = {};
190
+ const nameBindings = [];
187
191
  if (element.attributes) {
188
192
  for (let j = 0; j < element.attributes.length; j++) {
189
193
  const attr = element.attributes[j];
190
194
  // Reset lastIndex before test - BINDING_REGEX has 'g' flag which persists state
191
195
  BINDING_REGEX.lastIndex = 0;
196
+
197
+ // Check if attribute name contains binding (e.g., @[section.icon])
198
+ if (BINDING_REGEX.test(attr.name)) {
199
+ nameBindings.push(attr.name);
200
+ continue; // Don't process as regular attribute
201
+ }
202
+
203
+ // Check if attribute value contains binding
204
+ BINDING_REGEX.lastIndex = 0;
192
205
  if (BINDING_REGEX.test(attr.value)) {
193
206
  attributes[attr.name] = attr.value;
194
207
  }
195
208
  }
196
209
  }
197
210
  const hasAttributeBindings = Object.keys(attributes).length > 0;
211
+ const hasNameBindings = nameBindings.length > 0;
198
212
 
199
213
  const hasChildren = childNodes.length;
200
214
 
@@ -205,8 +219,9 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
205
219
  result[rootKey || nodeIdentifier] = {
206
220
  parsed,
207
221
  element,
208
- children: recursive(iteratableChildren),
222
+ children: recursive(iteratableChildren, undefined, new Set(), stats),
209
223
  ...(hasAttributeBindings && { attributes }),
224
+ ...(hasNameBindings && { nameBindings }),
210
225
  };
211
226
  } else {
212
227
  result[nodeIdentifier] = {
@@ -214,6 +229,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
214
229
  element,
215
230
  children: {},
216
231
  ...(hasAttributeBindings && { attributes }),
232
+ ...(hasNameBindings && { nameBindings }),
217
233
  };
218
234
  }
219
235
  }
@@ -223,13 +239,25 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
223
239
 
224
240
  export default (root, rootKey = undefined) => {
225
241
  const { childNodes } = root;
242
+ const stats = { skipped: 0 };
226
243
 
227
244
  // Check for attribute bindings on the root element itself (only if element has attributes)
228
245
  let attributes = null;
246
+ let nameBindings = null;
229
247
  if (root.attributes && root.attributes.length > 0) {
230
248
  for (let j = 0; j < root.attributes.length; j++) {
231
249
  const attr = root.attributes[j];
232
250
  BINDING_REGEX.lastIndex = 0;
251
+
252
+ // Check if attribute name contains binding
253
+ if (BINDING_REGEX.test(attr.name)) {
254
+ if (!nameBindings) nameBindings = [];
255
+ nameBindings.push(attr.name);
256
+ continue;
257
+ }
258
+
259
+ // Check if attribute value contains binding
260
+ BINDING_REGEX.lastIndex = 0;
233
261
  if (BINDING_REGEX.test(attr.value)) {
234
262
  if (!attributes) attributes = {};
235
263
  attributes[attr.name] = attr.value;
@@ -241,7 +269,9 @@ export default (root, rootKey = undefined) => {
241
269
  // html: root.outerHTML,
242
270
  parsed: parseHTML([...childNodes], rootKey),
243
271
  element: root,
244
- children: recursive(Array.from(childNodes), rootKey),
272
+ children: recursive(Array.from(childNodes), rootKey, new Set(), stats),
245
273
  ...(attributes && { attributes }),
274
+ ...(nameBindings && { nameBindings }),
275
+ stats,
246
276
  };
247
277
  };
@@ -0,0 +1,52 @@
1
+ // Track which objects are already proxied to avoid double-wrapping
2
+ const proxyCache = new WeakMap();
3
+
4
+ // Deep proxy: recursively wrap nested objects and arrays
5
+ const createDeepProxy = (target, rerender, rootState = null, rootProp = null) => {
6
+ // For root level, rootState is the target itself
7
+ if (rootState === null) {
8
+ rootState = target;
9
+ }
10
+
11
+ // Check cache first
12
+ if (proxyCache.has(target)) {
13
+ return proxyCache.get(target);
14
+ }
15
+
16
+ const proxy = new Proxy(target, {
17
+ set(obj, prop, value) {
18
+ const oldValue = obj[prop];
19
+ const ref = Reflect.set(obj, prop, value);
20
+
21
+ // Only trigger rerender if value actually changed
22
+ if (oldValue !== value) {
23
+ // If we're at root level, use the prop being set
24
+ // If we're nested, use the root prop that contains this nested object
25
+ const changedProp = rootProp || prop;
26
+ rerender({ [changedProp]: rootState[changedProp] });
27
+ }
28
+
29
+ return ref;
30
+ },
31
+
32
+ get(target, prop) {
33
+ const value = Reflect.get(target, prop);
34
+
35
+ // Don't proxy non-objects, functions, or null
36
+ if (value === null || typeof value !== 'object' || typeof value === 'function') {
37
+ return value;
38
+ }
39
+
40
+ // Recursively wrap nested objects/arrays
41
+ // If rootProp is null (we're at root level), set it to current prop
42
+ // Otherwise keep passing down the same rootProp
43
+ const topProp = rootProp === null ? prop : rootProp;
44
+ return createDeepProxy(value, rerender, rootState, topProp);
45
+ },
46
+ });
47
+
48
+ proxyCache.set(target, proxy);
49
+ return proxy;
50
+ };
51
+
52
+ export default (state, rerender) => createDeepProxy(state, rerender);
@@ -2,6 +2,19 @@
2
2
  let hashCounter = 0;
3
3
  export const hash = () => `_${hashCounter++}`;
4
4
 
5
+ // Evaluate expression in the context of state
6
+ export const evalInScope = (expr, state) => {
7
+ try {
8
+ // Normalize whitespace - collapse newlines/spaces to single space (resilient to IDE formatting)
9
+ const normalized = expr.replace(/\s+/g, ' ').trim();
10
+ const keys = Object.keys(state);
11
+ const values = Object.values(state);
12
+ return new Function(...keys, `'use strict'; return (${normalized})`)(...values);
13
+ } catch (e) {
14
+ return undefined;
15
+ }
16
+ };
17
+
5
18
  // Instead of using lodash-es as a dependency, we run our own deepMerge (mergeWith in lodash)
6
19
  export const deepMerge = (target, source) => {
7
20
  // Handle null/undefined
package/llms.txt DELETED
@@ -1,279 +0,0 @@
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
- - Surgical DOM updates (only affected elements re-render)
12
- - MutationObserver for dynamic element tracking
13
- - Works with vanilla HTML - no special file format
14
-
15
- ## Installation
16
-
17
- ```bash
18
- npm install @ape-egg/vibe
19
- ```
20
-
21
- ## Quick Start
22
-
23
- ```html
24
- <script type="module">
25
- import state from "@ape-egg/vibe";
26
- window.$ = state({ name: "World", count: 0 });
27
- </script>
28
-
29
- <h1>Hello, @[name]!</h1>
30
- <button onclick="$.count++">Clicked @[count] times</button>
31
- ```
32
-
33
- ## Core Syntax
34
-
35
- ### Reactive Bindings
36
-
37
- Use `@[property]` syntax anywhere in HTML or CSS:
38
-
39
- ```html
40
- <!-- Text content -->
41
- <div>@[firstName]</div>
42
-
43
- <!-- Expressions -->
44
- <div>@[firstName + ' ' + lastName]</div>
45
- <div>@[count * 2]</div>
46
-
47
- <!-- Attributes -->
48
- <input value="@[inputValue]">
49
- <button disabled="@[isLoading]">Submit</button>
50
-
51
- <!-- CSS -->
52
- <style>
53
- .box { background: @[themeColor]; }
54
- </style>
55
- ```
56
-
57
- ⚠️ **Important**: Bindings are evaluated using `new Function()`. Do not bind untrusted user input.
58
-
59
- ### State Access
60
-
61
- State is accessed globally via `window.$`:
62
-
63
- ```javascript
64
- // Read
65
- console.log($.firstName);
66
-
67
- // Write (triggers re-render)
68
- $.firstName = "John";
69
-
70
- // Increment
71
- $.count++;
72
- ```
73
-
74
- ## Control Flow
75
-
76
- ### Iteration
77
-
78
- ```html
79
- <!-- each items as item -->
80
- <li>@[item]</li>
81
- <!-- /each -->
82
- ```
83
-
84
- With index:
85
-
86
- ```html
87
- <!-- each items as item, index -->
88
- <li>@[index]: @[item]</li>
89
- <!-- /each -->
90
- ```
91
-
92
- ### Nested Iteration
93
-
94
- Use dot paths for nested arrays:
95
-
96
- ```html
97
- <!-- each categories as category -->
98
- <h2>@[category.name]</h2>
99
- <!-- each category.items as item -->
100
- <span>@[item.name]</span>
101
- <!-- /each -->
102
- <!-- /each -->
103
- ```
104
-
105
- ### Conditionals
106
-
107
- ```html
108
- <!-- if isLoggedIn -->
109
- <span>Welcome, @[username]!</span>
110
- <!-- else -->
111
- <span>Please log in</span>
112
- <!-- /if -->
113
- ```
114
-
115
- Conditionals can be nested inside iterations and vice versa.
116
-
117
- ## Special Attributes
118
-
119
- ### Dehydrate
120
-
121
- Skip reactive processing for an element and its children:
122
-
123
- ```html
124
- <code dehydrate>@[this] displays literally, not parsed</code>
125
- ```
126
-
127
- Use cases:
128
- - Displaying `@[...]` syntax in documentation
129
- - Static content that shouldn't be reactive
130
- - Performance optimization for large static sections
131
-
132
- ### Boolean Attributes
133
-
134
- Attributes not in the value whitelist are removed when falsy:
135
-
136
- ```html
137
- <button disabled="@[isLoading]">Submit</button>
138
- <!-- When isLoading is false, disabled attribute is removed entirely -->
139
- ```
140
-
141
- ## Events
142
-
143
- Use standard inline event handlers:
144
-
145
- ```html
146
- <button onclick="$.count++">Increment</button>
147
- <input oninput="$.text = this.value">
148
- <form onsubmit="event.preventDefault(); handleSubmit()">
149
- ```
150
-
151
- ## Styling
152
-
153
- ### CSS Bindings
154
-
155
- Reactive values work inside `<style>` tags:
156
-
157
- ```html
158
- <style>
159
- .box {
160
- background: @[backgroundColor];
161
- color: @[textColor];
162
- width: @[width]px;
163
- }
164
- </style>
165
- ```
166
-
167
- ### Preventing FOUC
168
-
169
- Hide content until hydration completes:
170
-
171
- ```html
172
- <body style="visibility: hidden;">
173
- ```
174
-
175
- Or use the included CSS:
176
-
177
- ```html
178
- <link rel="stylesheet" href="@ape-egg/vibe/vibe.css">
179
- <body vibe>
180
- ```
181
-
182
- ## Dynamic Elements
183
-
184
- Elements added via JavaScript are automatically hydrated through MutationObserver:
185
-
186
- ```javascript
187
- const div = document.createElement('div');
188
- div.innerHTML = '<span>Hello, @[name]!</span>';
189
- document.body.appendChild(div);
190
- // Automatically becomes reactive
191
- ```
192
-
193
- ## API Reference
194
-
195
- ### `state(initialState, afterUpdate?)`
196
-
197
- Creates reactive state and initializes the framework.
198
-
199
- ```javascript
200
- import state from "@ape-egg/vibe";
201
-
202
- window.$ = state(
203
- { count: 0, user: { name: "Alice" } },
204
- (newState, oldState) => {
205
- console.log("State updated:", newState);
206
- }
207
- );
208
- ```
209
-
210
- **Parameters:**
211
- - `initialState` - Object containing initial state values
212
- - `afterUpdate` - Optional callback after each state change (receives read-only snapshots)
213
-
214
- **Returns:** Proxy object for reactive state access
215
-
216
- ## Scoped Variables
217
-
218
- Inside `<!-- each -->` blocks, these variables are available:
219
- - `item` (or custom name) - current array element
220
- - `index` (or custom name) - current index
221
- - Parent state remains accessible via `$`
222
-
223
- ```html
224
- <!-- each users as user, i -->
225
- <div>@[i]: @[user.name] (total: @[users.length])</div>
226
- <!-- /each -->
227
- ```
228
-
229
- ## Architecture
230
-
231
- Vibe consists of these core modules:
232
-
233
- - **state.js** - Proxy-based reactive state container
234
- - **parse.js** - DOM parser that finds `@[...]` bindings
235
- - **link.js** - Maps elements to parsed tree nodes
236
- - **hydrate.js** - Updates DOM with current state values
237
- - **affected.js** - Determines which elements need updating
238
- - **iterate.js** - Array rendering with efficient diffing
239
- - **conditionals.js** - Conditional block rendering
240
-
241
- ## How It Works
242
-
243
- ```
244
- 1. state() initializes the Proxy and framework
245
- 2. parse.js scans DOM for @[...], <!-- each -->, <!-- if -->
246
- 3. link.js maps elements to the parsed tree
247
- 4. hydrate.js replaces bindings with values
248
- 5. iterate.js renders <!-- each --> loops
249
- 6. conditionals.js renders <!-- if --> blocks
250
- 7. MutationObserver watches for new elements
251
- 8. On state change: affected.js finds changed elements → hydrate.js updates them
252
- ```
253
-
254
- ## Current Limitations
255
-
256
- - **Top-level reactivity only**: `$.nested.prop = value` doesn't trigger updates (must replace parent object)
257
- - **No computed values**: Derived state must be calculated manually
258
- - **No two-way binding sugar**: Must wire input events manually
259
- - **Expression security**: `new Function()` evaluation - don't bind untrusted input
260
-
261
- ## Best Practices
262
-
263
- 1. **Initialize state before DOM**: Place `<script>` in `<head>` or before reactive elements
264
- 2. **Use dehydrate for docs**: When showing `@[...]` syntax examples
265
- 3. **Prevent FOUC**: Use `visibility: hidden` on body until hydration
266
- 4. **Keep expressions simple**: Complex logic belongs in JavaScript, not templates
267
- 5. **Replace objects for deep updates**: `$.user = { ...$.user, name: "New" }`
268
-
269
- ## Browser Support
270
-
271
- Modern browsers with:
272
- - Proxy (ES6)
273
- - MutationObserver
274
- - ES Modules
275
-
276
- ## Resources
277
-
278
- - **Homepage**: https://vibe.korte.kim
279
- - **npm**: https://www.npmjs.com/package/@ape-egg/vibe