Add event bindings and data-sx-emit processing
- adapter-dom.sx: detect :on-click/:on-submit etc. in render-dom-element — if attr starts with "on-" and value is callable, wire via dom-listen - orchestration.sx: add process-emit-elements for data-sx-emit attrs — auto-dispatch custom events on click with optional JSON detail - bootstrap_js.py: add processEmitElements RENAME - Regenerate sx-ref.js with all changes - Update reactive-islands status table Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||||
var SX_VERSION = "2026-03-08T11:12:31Z";
|
var SX_VERSION = "2026-03-08T11:14:47Z";
|
||||||
|
|
||||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||||
@@ -1490,7 +1490,10 @@ continue; } else { return NIL; } } };
|
|||||||
return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() {
|
return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() {
|
||||||
var attrName = keywordName(arg);
|
var attrName = keywordName(arg);
|
||||||
var attrVal = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env));
|
var attrVal = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env));
|
||||||
(isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal))))));
|
(isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy((isSxTruthy(startsWith(attrName, "on-")) && isCallable(attrVal))) ? (function() {
|
||||||
|
var eventName = substring(attrName, 3, stringLength(attrName));
|
||||||
|
return domListen(el, eventName, attrVal);
|
||||||
|
})() : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal)))))));
|
||||||
return assoc(state, "skip", true, "i", (get(state, "i") + 1));
|
return assoc(state, "skip", true, "i", (get(state, "i") + 1));
|
||||||
})() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? domAppend(el, renderToDom(arg, env, newNs)) : NIL), assoc(state, "i", (get(state, "i") + 1)))));
|
})() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? domAppend(el, renderToDom(arg, env, newNs)) : NIL), assoc(state, "i", (get(state, "i") + 1)))));
|
||||||
})(); }, {["i"]: 0, ["skip"]: false}, args);
|
})(); }, {["i"]: 0, ["skip"]: false}, args);
|
||||||
@@ -2496,7 +2499,8 @@ return postSwap(target); })) : NIL);
|
|||||||
})();
|
})();
|
||||||
processBoosted(root);
|
processBoosted(root);
|
||||||
processSse(root);
|
processSse(root);
|
||||||
return bindInlineHandlers(root); };
|
bindInlineHandlers(root);
|
||||||
|
return processEmitElements(root); };
|
||||||
|
|
||||||
// process-one
|
// process-one
|
||||||
var processOne = function(el) { return (function() {
|
var processOne = function(el) { return (function() {
|
||||||
@@ -2504,6 +2508,19 @@ return bindInlineHandlers(root); };
|
|||||||
return (isSxTruthy(verbInfo) ? (isSxTruthy(!isSxTruthy(domHasAttr(el, "sx-disable"))) ? (bindTriggers(el, verbInfo), bindPreloadFor(el)) : NIL) : NIL);
|
return (isSxTruthy(verbInfo) ? (isSxTruthy(!isSxTruthy(domHasAttr(el, "sx-disable"))) ? (bindTriggers(el, verbInfo), bindPreloadFor(el)) : NIL) : NIL);
|
||||||
})(); };
|
})(); };
|
||||||
|
|
||||||
|
// process-emit-elements
|
||||||
|
var processEmitElements = function(root) { return (function() {
|
||||||
|
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() {
|
||||||
|
var detailJson = domGetAttr(el, "data-sx-emit-detail");
|
||||||
|
var detail = (isSxTruthy(detailJson) ? jsonParse(detailJson) : {});
|
||||||
|
return domDispatch(el, eventName, detail);
|
||||||
|
})(); }) : NIL);
|
||||||
|
})()) : NIL); }, els);
|
||||||
|
})(); };
|
||||||
|
|
||||||
// handle-popstate
|
// handle-popstate
|
||||||
var handlePopstate = function(scrollY) { return (function() {
|
var handlePopstate = function(scrollY) { return (function() {
|
||||||
var url = browserLocationHref();
|
var url = browserLocationHref();
|
||||||
|
|||||||
@@ -170,6 +170,12 @@
|
|||||||
;; nil or false → skip
|
;; nil or false → skip
|
||||||
(or (nil? attr-val) (= attr-val false))
|
(or (nil? attr-val) (= attr-val false))
|
||||||
nil
|
nil
|
||||||
|
;; Event handler: on-click, on-submit, on-input, etc.
|
||||||
|
;; Value must be callable (lambda/function)
|
||||||
|
(and (starts-with? attr-name "on-")
|
||||||
|
(callable? attr-val))
|
||||||
|
(let ((event-name (substring attr-name 3 (string-length attr-name))))
|
||||||
|
(dom-listen el event-name attr-val))
|
||||||
;; Boolean attr
|
;; Boolean attr
|
||||||
(contains? BOOLEAN_ATTRS attr-name)
|
(contains? BOOLEAN_ATTRS attr-name)
|
||||||
(when attr-val (dom-set-attr el attr-name ""))
|
(when attr-val (dom-set-attr el attr-name ""))
|
||||||
@@ -589,6 +595,11 @@
|
|||||||
;; (dom-child-nodes frag) → list of child nodes
|
;; (dom-child-nodes frag) → list of child nodes
|
||||||
;; (dom-remove-children-after m)→ void (remove all siblings after marker)
|
;; (dom-remove-children-after m)→ void (remove all siblings after marker)
|
||||||
;; (dom-set-data el key val) → void (store arbitrary data on element)
|
;; (dom-set-data el key val) → void (store arbitrary data on element)
|
||||||
|
;; (dom-get-data el key) → any (retrieve data stored on element)
|
||||||
|
;;
|
||||||
|
;; Event handling:
|
||||||
|
;; (dom-listen el name handler) → remove-fn (addEventListener, returns remover)
|
||||||
|
;; (dom-dispatch el name detail)→ boolean (dispatch CustomEvent, bubbles: true)
|
||||||
;;
|
;;
|
||||||
;; Content parsing:
|
;; Content parsing:
|
||||||
;; (dom-parse-html s) → DocumentFragment from HTML string
|
;; (dom-parse-html s) → DocumentFragment from HTML string
|
||||||
|
|||||||
@@ -397,6 +397,7 @@ class JSEmitter:
|
|||||||
"bind-sse": "bindSse",
|
"bind-sse": "bindSse",
|
||||||
"bind-sse-swap": "bindSseSwap",
|
"bind-sse-swap": "bindSseSwap",
|
||||||
"bind-inline-handlers": "bindInlineHandlers",
|
"bind-inline-handlers": "bindInlineHandlers",
|
||||||
|
"process-emit-elements": "processEmitElements",
|
||||||
"bind-preload-for": "bindPreloadFor",
|
"bind-preload-for": "bindPreloadFor",
|
||||||
"do-preload": "doPreload",
|
"do-preload": "doPreload",
|
||||||
"VERB_SELECTOR": "VERB_SELECTOR",
|
"VERB_SELECTOR": "VERB_SELECTOR",
|
||||||
|
|||||||
@@ -1087,10 +1087,11 @@
|
|||||||
(mark-processed! el "verb")
|
(mark-processed! el "verb")
|
||||||
(process-one el)))
|
(process-one el)))
|
||||||
els))
|
els))
|
||||||
;; Also process boost, SSE, inline handlers
|
;; Also process boost, SSE, inline handlers, emit attributes
|
||||||
(process-boosted root)
|
(process-boosted root)
|
||||||
(process-sse root)
|
(process-sse root)
|
||||||
(bind-inline-handlers root)))
|
(bind-inline-handlers root)
|
||||||
|
(process-emit-elements root)))
|
||||||
|
|
||||||
|
|
||||||
(define process-one
|
(define process-one
|
||||||
@@ -1104,6 +1105,40 @@
|
|||||||
(bind-preload-for el))))))
|
(bind-preload-for el))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; data-sx-emit — auto-dispatch custom events for lake→island bridge
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;;
|
||||||
|
;; Elements with data-sx-emit="event-name" get a click listener that
|
||||||
|
;; dispatches a CustomEvent with that name. Optional data-sx-emit-detail
|
||||||
|
;; provides JSON payload.
|
||||||
|
;;
|
||||||
|
;; Example:
|
||||||
|
;; <button data-sx-emit="cart:add"
|
||||||
|
;; data-sx-emit-detail='{"id":42,"name":"Widget"}'>
|
||||||
|
;; Add to Cart
|
||||||
|
;; </button>
|
||||||
|
;;
|
||||||
|
;; On click → dispatches CustomEvent "cart:add" with detail {id:42, name:"Widget"}
|
||||||
|
;; The event bubbles up to the island container where bridge-event catches it.
|
||||||
|
|
||||||
|
(define process-emit-elements
|
||||||
|
(fn (root)
|
||||||
|
(let ((els (dom-query-all (or root (dom-body)) "[data-sx-emit]")))
|
||||||
|
(for-each
|
||||||
|
(fn (el)
|
||||||
|
(when (not (is-processed? el "emit"))
|
||||||
|
(mark-processed! el "emit")
|
||||||
|
(let ((event-name (dom-get-attr el "data-sx-emit")))
|
||||||
|
(when event-name
|
||||||
|
(dom-listen el "click"
|
||||||
|
(fn (e)
|
||||||
|
(let ((detail-json (dom-get-attr el "data-sx-emit-detail"))
|
||||||
|
(detail (if detail-json (json-parse detail-json) (dict))))
|
||||||
|
(dom-dispatch el event-name detail))))))))
|
||||||
|
els))))
|
||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; History: popstate handler
|
;; History: popstate handler
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -115,8 +115,12 @@
|
|||||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "boot.sx: sx-hydrate-islands, hydrate-island, dispose-island"))
|
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "boot.sx: sx-hydrate-islands, hydrate-island, dispose-island"))
|
||||||
(tr :class "border-b border-stone-100"
|
(tr :class "border-b border-stone-100"
|
||||||
(td :class "px-3 py-2 text-stone-700" "Event bindings")
|
(td :class "px-3 py-2 text-stone-700" "Event bindings")
|
||||||
(td :class "px-3 py-2 text-amber-600 font-medium" "TODO")
|
(td :class "px-3 py-2 text-green-700 font-medium" "Spec'd")
|
||||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" ":on-click wiring"))
|
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "adapter-dom.sx: :on-click → domListen"))
|
||||||
|
(tr :class "border-b border-stone-100"
|
||||||
|
(td :class "px-3 py-2 text-stone-700" "data-sx-emit processing")
|
||||||
|
(td :class "px-3 py-2 text-green-700 font-medium" "Spec'd")
|
||||||
|
(td :class "px-3 py-2 font-mono text-xs text-stone-500" "orchestration.sx: process-emit-elements"))
|
||||||
(tr
|
(tr
|
||||||
(td :class "px-3 py-2 text-stone-700" "Keyed list reconciliation")
|
(td :class "px-3 py-2 text-stone-700" "Keyed list reconciliation")
|
||||||
(td :class "px-3 py-2 text-amber-600 font-medium" "TODO")
|
(td :class "px-3 py-2 text-amber-600 font-medium" "TODO")
|
||||||
|
|||||||
Reference in New Issue
Block a user