@@ -1506,8 +1506,12 @@ else if (name.startsWith('@') || name.startsWith(':')) {
15061506 var hasSignals = Object.values(eventCapturedScope).some(function(v) {
15071507 return v && typeof v === 'function' && v._isSignal;
15081508 });
1509- if (hasSignals) {
1510- // Use read/write proxy: reads unwrap signals, assignments write through signal.set()
1509+ // Only use signal read/write proxy for DIRECT assignment expressions
1510+ // like "count = count + 1" or "open = !open". Function calls like
1511+ // "openModal()" handle their own signal.set() internally — the writeback
1512+ // would RESET signals to their pre-handler values.
1513+ var isDirectAssignment = hasSignals && /^[a-zA-Z_$]\w*\s*=/.test(value.trim()) && !value.trim().startsWith('==');
1514+ if (isDirectAssignment) {
15111515 var getVars = Object.keys(eventCapturedScope).map(function(k) {
15121516 return 'var ' + k + ' = __s["' + k + '"] && typeof __s["' + k + '"] === "function" && __s["' + k + '"]._isSignal ? __s["' + k + '"]() : __s["' + k + '"]';
15131517 }).join(';');
@@ -1520,6 +1524,13 @@ else if (name.startsWith('@') || name.startsWith(':')) {
15201524 var body = getVars + ';' + value + ';' + setVars;
15211525 var fn2 = new Function('__s', '$event', body);
15221526 fn2(eventCapturedScope, event);
1527+ } else if (hasSignals) {
1528+ // Function call with signals in scope — unwrap for reading but NO writeback
1529+ var unwrapVars = Object.keys(eventCapturedScope).map(function(k) {
1530+ return 'var ' + k + ' = __s["' + k + '"] && typeof __s["' + k + '"] === "function" && __s["' + k + '"]._isSignal ? __s["' + k + '"]() : __s["' + k + '"]';
1531+ }).join(';');
1532+ var fn3 = new Function('__s', '$event', unwrapVars + ';' + value);
1533+ fn3(eventCapturedScope, event);
15231534 } else {
15241535 var fn = new Function(...Object.keys(eventCapturedScope), '$event', value);
15251536 fn(...Object.values(eventCapturedScope), event);
@@ -1922,9 +1933,10 @@ else {
19221933 });
19231934 }
19241935
1936+ var __bindIfCounter = 0;
19251937 function bindIf(el, passedScope = componentScope, attrName = '@if') {
19261938 // Guard: prevent double-binding on the same element
1927- if (el.__stx_if_bound) return;
1939+ if (el.__stx_if_bound) { console.log('[stx] bindIf SKIPPED (already bound):', el.getAttribute(attrName) || '(attr removed)', 'on', el.tagName); return; }
19281940 el.__stx_if_bound = true;
19291941
19301942 const expr = el.getAttribute(attrName);
@@ -1994,9 +2006,27 @@ catch (e) {
19942006 childrenProcessed = true;
19952007 };
19962008
1997- effect(() => {
1998- const value = evalExpr(expr);
2009+ // Evaluate the :if expression — use direct signal read for simple refs,
2010+ // falling back to evalExpr for complex expressions
2011+ const fullScope = { ...capturedComponentScope, ...(capturedElementScope || {}), ...globalHelpers };
2012+ const directSignal = fullScope[expr];
19992013
2014+ if (directSignal) {
2015+ console.log('[stx] bindIf direct signal for :if=' + expr, 'signal identity:', directSignal === componentScope[expr] ? 'SAME' : 'DIFFERENT', 'signal():', directSignal());
2016+ }
2017+ effect(() => {
2018+ var value;
2019+ if (directSignal && (directSignal._isSignal || directSignal._isDerived)) {
2020+ value = directSignal();
2021+ console.log('[stx] bindIf effect (direct):', expr, '→', value, 'isInserted:', isInserted);
2022+ } else {
2023+ // Complex expression: read all signals first for tracking, then evaluate
2024+ for (var sk in fullScope) {
2025+ var sv = fullScope[sk];
2026+ if (sv && typeof sv === 'function' && (sv._isSignal || sv._isDerived)) sv();
2027+ }
2028+ value = evalExpr(expr);
2029+ }
20002030
20012031 if (isTemplate) {
20022032 if (value && !isInserted) {
@@ -2013,6 +2043,8 @@ catch (e) {
20132043 node.querySelectorAll('[x-cloak]').forEach(c => c.removeAttribute('x-cloak'));
20142044 }
20152045 });
2046+ // Stamp insertion time for click propagation guard
2047+ currentNodes.forEach(function(n) { if (n.nodeType === 1) n.__stx_shown_at = performance.now(); });
20162048 isInserted = true;
20172049 }
20182050else if (!value && isInserted) {
@@ -2024,31 +2056,31 @@ else if (!value && isInserted) {
20242056 }
20252057else {
20262058 if (value && !isInserted) {
2059+ console.log('[stx] bindIf INSERTING element for :if=' + expr);
20272060 parent.insertBefore(el, placeholder.nextSibling);
2061+ el.__stx_shown_at = performance.now();
20282062 isInserted = true;
20292063 }
20302064else if (!value && isInserted) {
2065+ console.log('[stx] bindIf REMOVING element for :if=' + expr, 'el.isConnected:', el.isConnected, 'parent:', parent.tagName);
20312066 el.remove();
20322067 isInserted = false;
2033- childrenProcessed = false; // Reset so children get re-processed on next insert
2068+ console.log('[stx] bindIf REMOVED, el.isConnected:', el.isConnected);
20342069 }
2035- // Process the entire subtree when element is visible and not yet processed
2070+ // Process the entire subtree when element is visible and not yet processed.
2071+ // IMPORTANT: defer to next microtask so child effects don't accidentally
2072+ // subscribe to the parent bindIf's tracked signals (which would cause
2073+ // the parent effect to re-run and remove/re-insert in an infinite loop).
20362074 if (value && isInserted && !childrenProcessed) {
2037- // Use LIVE componentScope (not captured snapshot) so functions added after
2038- // initial binding (e.g. from <script client> setup) are available
2039- var childScope = { ...componentScope, ...(capturedElementScope || {}), ...globalHelpers };
2040- console.log('[stx] bindIf processing subtree for :if=' + expr, 'el:', el.tagName, 'children:', el.childNodes.length, 'connected:', el.isConnected, 'scope funcs:', Object.keys(childScope).filter(function(k) { return typeof childScope[k] === 'function' && !childScope[k]._isSignal; }).slice(0,5));
2041- // Process ALL descendants — not just direct children
2042- function processSubtree(node, sc) {
2043- if (node.nodeType === 1) {
2044- processElement(node, sc);
2045- } else if (node.nodeType === 3) {
2046- processElement(node, sc);
2047- }
2048- }
2049- // Process direct children and let processElement handle recursion
2050- Array.from(el.childNodes).forEach(function(child) { processSubtree(child, childScope); });
20512075 childrenProcessed = true;
2076+ setTimeout(function() {
2077+ var childScope = { ...componentScope, ...(capturedElementScope || {}), ...globalHelpers };
2078+ Array.from(el.childNodes).forEach(function(child) {
2079+ if (child.nodeType === 1 || child.nodeType === 3) {
2080+ processElement(child, childScope);
2081+ }
2082+ });
2083+ }, 0);
20522084 }
20532085 }
20542086 });
0 commit comments