diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 5b07f4a..05d9fed 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-24T18:28:36Z"; + var SX_VERSION = "2026-03-24T19:19:01Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -3159,6 +3159,12 @@ PRIMITIVES["SVG_NS"] = SVG_NS; var MATH_NS = "http://www.w3.org/1998/Math/MathML"; PRIMITIVES["MATH_NS"] = MATH_NS; + // dom-on + var domOn = function(el, name, handler) { return domListen(el, name, (isSxTruthy(isLambda(handler)) ? (isSxTruthy((0 == len(lambdaParams(handler)))) ? function() { trampoline(callLambda(handler, [])); +return runPostRenderHooks(); } : function(e) { trampoline(callLambda(handler, [e])); +return runPostRenderHooks(); }) : handler)); }; +PRIMITIVES["dom-on"] = domOn; + // render-to-dom var renderToDom = function(expr, env, ns) { setRenderActiveB(true); return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragment(); if (_m == "boolean") return createFragment(); if (_m == "raw-html") return domParseHtml(rawHtmlContent(expr)); if (_m == "string") return createTextNode(expr); if (_m == "number") return createTextNode((String(expr))); if (_m == "symbol") return renderToDom(trampoline(evalExpr(expr, env)), env, ns); if (_m == "keyword") return createTextNode(keywordName(expr)); if (_m == "dom-node") return expr; if (_m == "spread") return (sxEmit("element-attrs", spreadAttrs(expr)), expr); if (_m == "dict") return createFragment(); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? createFragment() : renderDomList(expr, env, ns)); return (isSxTruthy(isSignal(expr)) ? (isSxTruthy(sxContext("sx-island-scope", NIL)) ? reactiveText(expr) : createTextNode((String(deref(expr))))) : createTextNode((String(expr)))); })(); }; @@ -3200,7 +3206,7 @@ PRIMITIVES["render-dom-list"] = renderDomList; var attrExpr = nth(args, (get(state, "i") + 1)); (isSxTruthy(startsWith(attrName, "on-")) ? (function() { var attrVal = trampoline(evalExpr(attrExpr, env)); - return (isSxTruthy(isCallable(attrVal)) ? domListen(el, slice(attrName, 3), attrVal) : NIL); + return (isSxTruthy(isCallable(attrVal)) ? domOn(el, slice(attrName, 3), attrVal) : NIL); })() : (isSxTruthy((attrName == "bind")) ? (function() { var attrVal = trampoline(evalExpr(attrExpr, env)); return (isSxTruthy(isSignal(attrVal)) ? bindInput(el, attrVal) : NIL); @@ -3707,7 +3713,7 @@ PRIMITIVES["reactive-list"] = reactiveList; var v = (String(deref(sig))); return (isSxTruthy((domGetProp(el, "value") != v)) ? domSetProp(el, "value", v) : NIL); })()); }); - return domListen(el, (isSxTruthy(isCheckbox) ? "change" : "input"), function(e) { return (isSxTruthy(isCheckbox) ? reset_b(sig, domGetProp(el, "checked")) : reset_b(sig, domGetProp(el, "value"))); }); + return domOn(el, (isSxTruthy(isCheckbox) ? "change" : "input"), function(e) { return (isSxTruthy(isCheckbox) ? reset_b(sig, domGetProp(el, "checked")) : reset_b(sig, domGetProp(el, "value"))); }); })(); }; PRIMITIVES["bind-input"] = bindInput; @@ -4763,7 +4769,7 @@ PRIMITIVES["bind-sse-swap"] = bindSseSwap; var eventName = slice(name, 6); return (isSxTruthy(!isSxTruthy(isProcessed(el, (String("on:") + String(eventName))))) ? (markProcessed(el, (String("on:") + String(eventName))), (function() { var exprs = sxParse(body); - return domListen(el, eventName, function(e) { return (function() { + return domOn(el, eventName, function(e) { return (function() { var handlerEnv = envExtend({}); envBind(handlerEnv, "event", e); envBind(handlerEnv, "this", el); @@ -4820,7 +4826,7 @@ PRIMITIVES["process-one"] = processOne; var els = domQueryAll(sxOr(root, domBody()), "[data-sx-emit]"); return forEach(function(el) { return (isSxTruthy(!isSxTruthy(isProcessed(el, "emit"))) ? (markProcessed(el, "emit"), (function() { var eventName = domGetAttr(el, "data-sx-emit"); - return (isSxTruthy(eventName) ? domListen(el, "click", function(e) { return (function() { + return (isSxTruthy(eventName) ? domOn(el, "click", function(e) { return (function() { var detailJson = domGetAttr(el, "data-sx-emit-detail"); var detail = (isSxTruthy(detailJson) ? jsonParse(detailJson) : {}); return domDispatch(el, eventName, detail); @@ -5967,12 +5973,12 @@ PRIMITIVES["clear-stores"] = clearStores; PRIMITIVES["emit-event"] = emitEvent; // on-event - var onEvent = function(el, eventName, handler) { return domListen(el, eventName, handler); }; + var onEvent = function(el, eventName, handler) { return domOn(el, eventName, handler); }; PRIMITIVES["on-event"] = onEvent; // bridge-event var bridgeEvent = function(el, eventName, targetSignal, transformFn) { return effect(function() { return (function() { - var remove = domListen(el, eventName, function(e) { return (function() { + var remove = domOn(el, eventName, function(e) { return (function() { var detail = eventDetail(e); var newVal = (isSxTruthy(transformFn) ? cekCall(transformFn, [detail]) : detail); return reset_b(targetSignal, newVal); @@ -6321,8 +6327,8 @@ PRIMITIVES["resource"] = resource; // If lambda takes 0 params, call without event arg (convenience for on-click handlers) var wrapped = isLambda(handler) ? (lambdaParams(handler).length === 0 - ? function(e) { try { cekCall(handler, NIL); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } finally { runPostRenderHooks(); } } - : function(e) { try { cekCall(handler, [e]); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } finally { runPostRenderHooks(); } }) + ? function(e) { try { cekCall(handler, NIL); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } } + : function(e) { try { cekCall(handler, [e]); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }) : handler; if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler)); var passiveEvents = { touchstart: 1, touchmove: 1, wheel: 1, scroll: 1 }; diff --git a/web/adapter-dom.sx b/web/adapter-dom.sx index 185adba..97a10d1 100644 --- a/web/adapter-dom.sx +++ b/web/adapter-dom.sx @@ -14,6 +14,24 @@ (define MATH_NS "http://www.w3.org/1998/Math/MathML") +;; -------------------------------------------------------------------------- +;; dom-on — dom-listen with post-render hooks +;; +;; Wraps dom-listen so that run-post-render-hooks fires after every SX +;; event handler invocation. This is the SX-level hook integration; +;; the native dom-listen primitive is a clean addEventListener wrapper. +;; -------------------------------------------------------------------------- + +(define dom-on :effects [io] + (fn (el name handler) + (dom-listen el name + (if (lambda? handler) + (if (= 0 (len (lambda-params handler))) + (fn () (trampoline (call-lambda handler (list))) (run-post-render-hooks)) + (fn (e) (trampoline (call-lambda handler (list e))) (run-post-render-hooks))) + handler)))) + + ;; -------------------------------------------------------------------------- ;; render-to-dom — main entry point ;; -------------------------------------------------------------------------- @@ -199,7 +217,7 @@ (starts-with? attr-name "on-") (let ((attr-val (trampoline (eval-expr attr-expr env)))) (when (callable? attr-val) - (dom-listen el (slice attr-name 3) attr-val))) + (dom-on el (slice attr-name 3) attr-val))) ;; Two-way input binding: :bind signal (= attr-name "bind") (let ((attr-val (trampoline (eval-expr attr-expr env)))) @@ -1100,7 +1118,7 @@ (when (!= (dom-get-prop el "value") v) (dom-set-prop el "value" v)))))) ;; Element → signal (event listener) - (dom-listen el (if is-checkbox "change" "input") + (dom-on el (if is-checkbox "change" "input") (fn (e) (if is-checkbox (reset! sig (dom-get-prop el "checked"))