@ape-egg/vibe 1.9.1 → 1.9.6

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.
@@ -1,7 +1,7 @@
1
1
  import { updateIteration } from './iterate.js';
2
2
  import { updateConditional } from './conditionals.js';
3
3
  import { VALUE_ATTRS, DOM_PROPERTIES, BINDING_REGEX, PURE_BINDING_REGEX } from './constants.js';
4
- import { evalInScope } from './utils.js';
4
+ import { evalInScope, resolveCaseInsensitivePath } from './utils.js';
5
5
 
6
6
  export default (affected, state, manifest = {}, oldState = {}) => {
7
7
  affected.forEach((aff) => {
@@ -24,18 +24,15 @@ export default (affected, state, manifest = {}, oldState = {}) => {
24
24
  if (aff.type === 'nameBinding') {
25
25
  const { nameBinding, matchInner, element } = aff;
26
26
  try {
27
- // HTML lowercases attribute names, so we need case-insensitive lookup
28
- // Try exact match first, then try finding a case-insensitive match
27
+ // HTML lowercases attribute names, so we need case-insensitive lookup.
28
+ // Try exact match first, then walk the path case-insensitively if the
29
+ // exact lookup returned nothing — this recovers camelCase property
30
+ // names in dotted paths like `<icon @[fx.convertsIcon]>` (arrives at
31
+ // runtime as `@[fx.convertsicon]`).
29
32
  let attrName = evalInScope(matchInner, effectiveState, element);
30
33
 
31
- // If exact match failed and expression is a simple property (no dots/brackets)
32
- if (!attrName && !matchInner.includes('.') && !matchInner.includes('[')) {
33
- // Find the property with case-insensitive match
34
- const keys = Object.keys(effectiveState);
35
- const matchingKey = keys.find(k => k.toLowerCase() === matchInner.toLowerCase());
36
- if (matchingKey) {
37
- attrName = effectiveState[matchingKey];
38
- }
34
+ if (!attrName) {
35
+ attrName = resolveCaseInsensitivePath(effectiveState, matchInner);
39
36
  }
40
37
 
41
38
  // Track multiple name bindings per element (need a map of binding -> evaluated attr)
package/runtime/index.js CHANGED
@@ -6,6 +6,7 @@ import affected from './affected.js';
6
6
  import { deepMerge, hash } from './utils.js';
7
7
  import { renderAllIterations, setRenderAllConditionals, releaseOrphanedIterationProps } from './iterate.js';
8
8
  import { renderAllConditionals, branchNodeRegistry, managedNodes } from './conditionals.js';
9
+ import { installScopeResolver } from './loop-scope.js';
9
10
  import {
10
11
  NON_REACTIVE_ELEMENTS,
11
12
  PHASE_ATTACH,
@@ -327,6 +328,10 @@ let previousState = {};
327
328
  const main = (s, config = {}, stringSelector = '') => {
328
329
  const debug = !!config?.debug;
329
330
  const verbose = !!config?.verbose;
331
+ globalThis.__vibeDebug = debug;
332
+
333
+ // Expose the global `$scope` resolver used by loop-scoped `on*` handlers.
334
+ installScopeResolver();
330
335
 
331
336
  // Detect if running in compiler's headless browser
332
337
  // When true: skip cleanup to preserve [vibe] attribute in compiled HTML
@@ -550,7 +555,17 @@ const main = (s, config = {}, stringSelector = '') => {
550
555
  // while still detecting binding changes inside iteration instances.
551
556
  const currentState = { ...previousState };
552
557
  for (const prop of changedProps) {
553
- currentState[prop] = extractPlainValue($[prop]);
558
+ // Distinguish "set to undefined" (key still present in $) from "deleted"
559
+ // (key absent from $). For deletions, removing from currentState matches
560
+ // the live proxy's shape — otherwise downstream consumers that pass
561
+ // currentState as `$` to evalInScope (e.g. iterations that re-render via
562
+ // bindings reading the root state) would see the deleted key as a phantom
563
+ // own-property with value `undefined`.
564
+ if (prop in $) {
565
+ currentState[prop] = extractPlainValue($[prop]);
566
+ } else {
567
+ delete currentState[prop];
568
+ }
554
569
  }
555
570
 
556
571
  // Find what changed (compare previousState vs currentState)
@@ -608,6 +623,11 @@ const main = (s, config = {}, stringSelector = '') => {
608
623
  });
609
624
 
610
625
  // Add hook subscription method (non-enumerable so it won't be spread/cloned with state)
626
+ // `configurable: true` lets a component script's scoped `$` Proxy (built in
627
+ // component.js) legally return a wrapped `.on` that auto-registers cleanup
628
+ // — the Proxy invariant rejects overriding non-configurable + non-writable
629
+ // data properties. Enumerable stays false so `on` doesn't leak into state
630
+ // snapshots or Object.keys($).
611
631
  Object.defineProperty($, 'on', {
612
632
  value: (event, callback) => {
613
633
  if (hooks[event]) {
@@ -616,6 +636,7 @@ const main = (s, config = {}, stringSelector = '') => {
616
636
  return () => (hooks[event] = hooks[event].filter((cb) => cb !== callback));
617
637
  },
618
638
  enumerable: false,
639
+ configurable: true,
619
640
  });
620
641
 
621
642
  // Promise that resolves after the ready hook fires and all ready callbacks
@@ -644,6 +665,13 @@ const main = (s, config = {}, stringSelector = '') => {
644
665
  enumerable: false,
645
666
  });
646
667
 
668
+ // Expose the live reactive proxy to the iteration stamper so loop-scoped
669
+ // `on*` handlers (`$scope`) resolve the SAME object identity the app sees via
670
+ // `$`, instead of the plain diff-snapshot clones iterations render against
671
+ // (see extractPlainValue below). Non-enumerable so it never shows up in the
672
+ // manifest's node-path entry iteration.
673
+ Object.defineProperty(manifest, '__live', { value: $, enumerable: false, configurable: true });
674
+
647
675
  // Initial hydration - pass plain values so iteration can do reference comparison
648
676
  const initialState = extractPlainValue($);
649
677
  const affectedElements = affected(parsedTree, initialState, initialState);
@@ -860,6 +888,19 @@ const main = (s, config = {}, stringSelector = '') => {
860
888
  iteratedCount += counts.iteratedCount;
861
889
  evaluatedCount += counts.evaluatedCount;
862
890
 
891
+ // For inlined `<component>` wrappers belonging to an iteration row
892
+ // (marked by component.js's resolveIterationComponentProps transfer),
893
+ // stash the parsed tree on the wrapper. processCoreLoop has just run
894
+ // hydrate + renderAllConditionals + renderAllIterations on it, so
895
+ // `parsedNode` carries live `runtime.activeInstance` /
896
+ // `runtime.instances` data — exactly what `affected.js` needs to walk
897
+ // into branch / row content. Iterate.js's update path consumes this
898
+ // tree to re-hydrate bindings inside the inlined component on each
899
+ // row-scope change without rebuilding the wrapper's DOM.
900
+ if (node.nodeType === 1 && node._vibeIterPropExprs) {
901
+ node._vibeIterTree = parsedNode;
902
+ }
903
+
863
904
  // Count elements vs nodes separately
864
905
  if (node.nodeName.startsWith('#')) {
865
906
  addedNodes++;