From b85a46bb62daf8251998a6676415142c99ed4619 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 7 Mar 2026 02:06:21 +0000 Subject: [PATCH] Re-read element attributes at click time, not from closed-over bind values All click handlers (bind-event, bindBoostLink, bindClientRouteClick) now re-read href/verb-info from the DOM element when the click fires, instead of using values captured at bind time. This ensures correct behavior when DOM is replaced or attributes are morphed after binding. Co-Authored-By: Claude Opus 4.6 --- shared/static/scripts/sx-browser.js | 30 ++++++++++++++++------------- shared/sx/ref/bootstrap_js.py | 20 +++++++++++-------- shared/sx/ref/orchestration.sx | 18 ++++++++--------- 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 9465cbd..2e95189 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-07T01:41:53Z"; + var SX_VERSION = "2026-03-07T02:03:55Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -1984,13 +1984,13 @@ return postSwap(target); }); })(); } return (isSxTruthy(shouldFire) ? ((isSxTruthy(sxOr((eventName == "submit"), (isSxTruthy((eventName == "click")) && domHasAttr(el, "href")))) ? preventDefault_(e) : NIL), (function() { - var isGetLink = (isSxTruthy((eventName == "click")) && isSxTruthy((get(verbInfo, "method") == "GET")) && isSxTruthy(domHasAttr(el, "href")) && !isSxTruthy(get(mods, "delay"))); + var liveInfo = sxOr(getVerbInfo(el), verbInfo); + var isGetLink = (isSxTruthy((eventName == "click")) && isSxTruthy((get(liveInfo, "method") == "GET")) && isSxTruthy(domHasAttr(el, "href")) && !isSxTruthy(get(mods, "delay"))); var clientRouted = false; if (isSxTruthy(isGetLink)) { - logInfo((String("sx:route trying ") + String(get(verbInfo, "url")))); - clientRouted = tryClientRoute(urlPathname(get(verbInfo, "url")), domGetAttr(el, "sx-target")); + clientRouted = tryClientRoute(urlPathname(get(liveInfo, "url")), domGetAttr(el, "sx-target")); } - return (isSxTruthy(clientRouted) ? (browserPushState(get(verbInfo, "url")), browserScrollTo(0, 0)) : ((isSxTruthy(isGetLink) ? logInfo((String("sx:route server fetch ") + String(get(verbInfo, "url")))) : NIL), (isSxTruthy(get(mods, "delay")) ? (clearTimeout_(timer), (timer = setTimeout_(function() { return executeRequest(el, verbInfo, NIL); }, get(mods, "delay")))) : executeRequest(el, verbInfo, NIL)))); + return (isSxTruthy(clientRouted) ? (browserPushState(get(liveInfo, "url")), browserScrollTo(0, 0)) : ((isSxTruthy(isGetLink) ? logInfo((String("sx:route server fetch ") + String(get(liveInfo, "url")))) : NIL), (isSxTruthy(get(mods, "delay")) ? (clearTimeout_(timer), (timer = setTimeout_(function() { return executeRequest(el, NIL, NIL); }, get(mods, "delay")))) : executeRequest(el, NIL, NIL)))); })()) : NIL); })(); }, (isSxTruthy(get(mods, "once")) ? {["once"]: true} : NIL)) : NIL); })(); }; @@ -3449,11 +3449,13 @@ callExpr.push(dictGet(kwargs, k)); } } // --- Boost bindings --- - function bindBoostLink(el, href) { + function bindBoostLink(el, _href) { el.addEventListener("click", function(e) { e.preventDefault(); - executeRequest(el, { method: "GET", url: href }).then(function() { - try { history.pushState({ sxUrl: href, scrollY: window.scrollY }, "", href); } catch (err) {} + // Re-read href from element at click time (not closed-over value) + var liveHref = el.getAttribute("href") || _href; + executeRequest(el, { method: "GET", url: liveHref }).then(function() { + try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} }); }); } @@ -3469,10 +3471,12 @@ callExpr.push(dictGet(kwargs, k)); } } // --- Client-side route bindings --- - function bindClientRouteClick(link, href, fallbackFn) { + function bindClientRouteClick(link, _href, fallbackFn) { link.addEventListener("click", function(e) { e.preventDefault(); - var pathname = urlPathname(href); + // Re-read href from element at click time (not closed-over value) + var liveHref = link.getAttribute("href") || _href; + var pathname = urlPathname(liveHref); // Find target selector: sx-boost ancestor, explicit sx-target, or #main-panel var boostEl = link.closest("[sx-boost]"); var targetSel = boostEl ? boostEl.getAttribute("sx-boost") : null; @@ -3480,12 +3484,12 @@ callExpr.push(dictGet(kwargs, k)); } } targetSel = link.getAttribute("sx-target") || "#main-panel"; } if (tryClientRoute(pathname, targetSel)) { - try { history.pushState({ sxUrl: href, scrollY: window.scrollY }, "", href); } catch (err) {} + try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} if (typeof window !== "undefined") window.scrollTo(0, 0); } else { logInfo("sx:route server " + pathname); - executeRequest(link, { method: "GET", url: href }).then(function() { - try { history.pushState({ sxUrl: href, scrollY: window.scrollY }, "", href); } catch (err) {} + executeRequest(link, { method: "GET", url: liveHref }).then(function() { + try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} }); } }); diff --git a/shared/sx/ref/bootstrap_js.py b/shared/sx/ref/bootstrap_js.py index a66a707..1dd9822 100644 --- a/shared/sx/ref/bootstrap_js.py +++ b/shared/sx/ref/bootstrap_js.py @@ -2720,11 +2720,13 @@ PLATFORM_ORCHESTRATION_JS = """ // --- Boost bindings --- - function bindBoostLink(el, href) { + function bindBoostLink(el, _href) { el.addEventListener("click", function(e) { e.preventDefault(); - executeRequest(el, { method: "GET", url: href }).then(function() { - try { history.pushState({ sxUrl: href, scrollY: window.scrollY }, "", href); } catch (err) {} + // Re-read href from element at click time (not closed-over value) + var liveHref = el.getAttribute("href") || _href; + executeRequest(el, { method: "GET", url: liveHref }).then(function() { + try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} }); }); } @@ -2740,10 +2742,12 @@ PLATFORM_ORCHESTRATION_JS = """ // --- Client-side route bindings --- - function bindClientRouteClick(link, href, fallbackFn) { + function bindClientRouteClick(link, _href, fallbackFn) { link.addEventListener("click", function(e) { e.preventDefault(); - var pathname = urlPathname(href); + // Re-read href from element at click time (not closed-over value) + var liveHref = link.getAttribute("href") || _href; + var pathname = urlPathname(liveHref); // Find target selector: sx-boost ancestor, explicit sx-target, or #main-panel var boostEl = link.closest("[sx-boost]"); var targetSel = boostEl ? boostEl.getAttribute("sx-boost") : null; @@ -2751,12 +2755,12 @@ PLATFORM_ORCHESTRATION_JS = """ targetSel = link.getAttribute("sx-target") || "#main-panel"; } if (tryClientRoute(pathname, targetSel)) { - try { history.pushState({ sxUrl: href, scrollY: window.scrollY }, "", href); } catch (err) {} + try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} if (typeof window !== "undefined") window.scrollTo(0, 0); } else { logInfo("sx:route server " + pathname); - executeRequest(link, { method: "GET", url: href }).then(function() { - try { history.pushState({ sxUrl: href, scrollY: window.scrollY }, "", href); } catch (err) {} + executeRequest(link, { method: "GET", url: liveHref }).then(function() { + try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} }); } }); diff --git a/shared/sx/ref/orchestration.sx b/shared/sx/ref/orchestration.sx index 81abd7d..823e7a5 100644 --- a/shared/sx/ref/orchestration.sx +++ b/shared/sx/ref/orchestration.sx @@ -388,33 +388,33 @@ (dom-has-attr? el "href"))) (prevent-default e)) - ;; For GET clicks on links, try client-side routing first - (let ((is-get-link (and (= event-name "click") - (= (get verbInfo "method") "GET") + ;; Re-read verb info from element at click time (not closed-over) + (let ((live-info (or (get-verb-info el) verbInfo)) + (is-get-link (and (= event-name "click") + (= (get live-info "method") "GET") (dom-has-attr? el "href") (not (get mods "delay")))) (client-routed false)) (when is-get-link - (log-info (str "sx:route trying " (get verbInfo "url"))) (set! client-routed (try-client-route - (url-pathname (get verbInfo "url")) + (url-pathname (get live-info "url")) (dom-get-attr el "sx-target")))) (if client-routed (do - (browser-push-state (get verbInfo "url")) + (browser-push-state (get live-info "url")) (browser-scroll-to 0 0)) (do (when is-get-link - (log-info (str "sx:route server fetch " (get verbInfo "url")))) + (log-info (str "sx:route server fetch " (get live-info "url")))) (if (get mods "delay") (do (clear-timeout timer) (set! timer (set-timeout - (fn () (execute-request el verbInfo nil)) + (fn () (execute-request el nil nil)) (get mods "delay")))) - (execute-request el verbInfo nil)))))))) + (execute-request el nil nil)))))))) (if (get mods "once") (dict "once" true) nil))))))