Web extension module for def-forms + modifier-key clicks + CSSX SSR fix
Move defhandler/defquery/defaction/defpage/defrelation from hardcoded evaluator dispatch to web/web-forms.sx extension module, registered via register-special-form!. Adapters updated to use definition-form? and dynamically extended form-name lists. Fix modifier-key clicks (ctrl-click → new tab) in three click handlers: bindBoostLink, bindClientRouteClick, and orchestration.sx bind-event. Add event-modifier-key? primitive (eventModifierKey_p for transpiler). Fix CSSX SSR: ~cssx/flush no longer drains the collected bucket on the server, so the shell template correctly emits CSSX rules in <head>. Add missing server-side DOM stubs (create-text-node, dom-append, etc.) and SSR passthrough for portal/error-boundary/promise-delayed. Passive event listeners for touch/wheel/scroll to fix touchpad scrolling. 97/97 Playwright demo tests + 4/4 isomorphic SSR tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2047,8 +2047,10 @@ PLATFORM_DOM_JS = """
|
||||
: function(e) { try { cekCall(handler, [e]); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } finally { runPostRenderHooks(); } })
|
||||
: handler;
|
||||
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
||||
el.addEventListener(name, wrapped);
|
||||
return function() { el.removeEventListener(name, wrapped); };
|
||||
var passiveEvents = { touchstart: 1, touchmove: 1, wheel: 1, scroll: 1 };
|
||||
var opts = passiveEvents[name] ? { passive: true } : undefined;
|
||||
el.addEventListener(name, wrapped, opts);
|
||||
return function() { el.removeEventListener(name, wrapped, opts); };
|
||||
}
|
||||
|
||||
function eventDetail(e) {
|
||||
@@ -2670,6 +2672,7 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
|
||||
function preventDefault_(e) { if (e && e.preventDefault) e.preventDefault(); }
|
||||
function stopPropagation_(e) { if (e && e.stopPropagation) e.stopPropagation(); }
|
||||
function eventModifierKey_p(e) { return !!(e && (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey)); }
|
||||
function domFocus(el) { if (el && el.focus) el.focus(); }
|
||||
function tryCatch(tryFn, catchFn) {
|
||||
var t = _wrapSxFn(tryFn);
|
||||
@@ -2773,6 +2776,7 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
|
||||
function bindBoostLink(el, _href) {
|
||||
el.addEventListener("click", function(e) {
|
||||
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
|
||||
e.preventDefault();
|
||||
// Re-read href from element at click time (not closed-over value)
|
||||
var liveHref = el.getAttribute("href") || _href;
|
||||
@@ -2804,6 +2808,7 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
|
||||
function bindClientRouteClick(link, _href, fallbackFn) {
|
||||
link.addEventListener("click", function(e) {
|
||||
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
|
||||
e.preventDefault();
|
||||
// Re-read href from element at click time (not closed-over value)
|
||||
var liveHref = link.getAttribute("href") || _href;
|
||||
@@ -2954,7 +2959,7 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
});
|
||||
}, { passive: true });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3283,6 +3288,7 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_
|
||||
if (typeof domClosest === "function") PRIMITIVES["dom-closest"] = domClosest;
|
||||
if (typeof domMatches === "function") PRIMITIVES["dom-matches?"] = domMatches;
|
||||
if (typeof preventDefault_ === "function") PRIMITIVES["prevent-default"] = preventDefault_;
|
||||
if (typeof eventModifierKey_p === "function") PRIMITIVES["event-modifier-key?"] = eventModifierKey_p;
|
||||
if (typeof elementValue === "function") PRIMITIVES["element-value"] = elementValue;
|
||||
if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml;
|
||||
if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml;
|
||||
|
||||
@@ -125,11 +125,9 @@ let io_batch_mode = ref false
|
||||
let io_queue : (int * string * value list) list ref = ref []
|
||||
let io_counter = ref 0
|
||||
|
||||
(** Module-level scope stacks — shared between make_server_env (aser
|
||||
scope-push!/pop!) and step-sf-context (via get-primitive "scope-peek"). *)
|
||||
(** Request cookies — set by Python bridge before each page render.
|
||||
get-cookie reads from here on the server; set-cookie is a no-op
|
||||
(server can't set response cookies from SX — that's the framework's job). *)
|
||||
(* Request cookies — set by Python bridge before each page render.
|
||||
get-cookie reads from here on the server; set-cookie is a no-op
|
||||
(server can't set response cookies from SX — that's the framework's job). *)
|
||||
let _request_cookies : (string, string) Hashtbl.t = Hashtbl.create 8
|
||||
|
||||
let () = Sx_primitives.register "get-cookie" (fun args ->
|
||||
@@ -144,6 +142,8 @@ let () = Sx_primitives.register "set-cookie" (fun _args ->
|
||||
(* No-op on server — cookies are set via HTTP response headers *)
|
||||
Nil)
|
||||
|
||||
(* Module-level scope stacks — shared between make_server_env (aser
|
||||
scope-push!/pop!) and step-sf-context (via get-primitive "scope-peek"). *)
|
||||
let _scope_stacks : (string, value list) Hashtbl.t = Hashtbl.create 8
|
||||
|
||||
let () = Sx_primitives.register "scope-push!" (fun args ->
|
||||
@@ -496,6 +496,10 @@ let make_server_env () =
|
||||
bind "dom-text-content" (fun _args -> String "");
|
||||
bind "dom-set-text-content" (fun _args -> Nil);
|
||||
bind "dom-body" (fun _args -> Nil);
|
||||
bind "dom-create-element" (fun _args -> Nil);
|
||||
bind "dom-append" (fun _args -> Nil);
|
||||
bind "create-text-node" (fun _args -> Nil);
|
||||
bind "render-to-dom" (fun _args -> Nil);
|
||||
|
||||
(* Raw HTML — platform primitives for adapter-html.sx *)
|
||||
bind "make-raw-html" (fun args ->
|
||||
@@ -617,6 +621,16 @@ let make_server_env () =
|
||||
Sx_ref.eval_expr m.m_body (Env body_env)
|
||||
| _ -> raise (Eval_error "expand-macro: expected (macro args env)"));
|
||||
|
||||
(* Expose register-special-form! and *custom-special-forms* to SX code
|
||||
(used by web-forms.sx and adapter form-classification functions) *)
|
||||
bind "register-special-form!" (fun args ->
|
||||
match args with
|
||||
| [String name; handler] ->
|
||||
ignore (Sx_ref.register_special_form (String name) handler);
|
||||
Nil
|
||||
| _ -> raise (Eval_error "register-special-form!: expected (name handler)"));
|
||||
ignore (env_bind env "*custom-special-forms*" Sx_ref.custom_special_forms);
|
||||
|
||||
(* Register <> as a special form — evaluates all children, returns list *)
|
||||
ignore (Sx_ref.register_special_form (String "<>") (NativeFn ("<>", fun args ->
|
||||
List (List.map (fun a -> Sx_ref.eval_expr a (Env env)) args))));
|
||||
@@ -725,10 +739,8 @@ let make_server_env () =
|
||||
bind "set-response-status" (fun args -> io_request "set-response-status" args);
|
||||
bind "set-response-header" (fun args -> io_request "set-response-header" args);
|
||||
|
||||
(* Application constructs — no-ops in the kernel, but needed so
|
||||
handler/page files load successfully (their define forms get evaluated) *)
|
||||
ignore (Sx_ref.register_special_form (String "defhandler") (NativeFn ("defhandler", fun _args -> Nil)));
|
||||
ignore (Sx_ref.register_special_form (String "defpage") (NativeFn ("defpage", fun _args -> Nil)));
|
||||
(* defhandler/defpage/defquery/defaction/defrelation are registered by
|
||||
web-forms.sx via register-special-form!, no longer hardcoded here. *)
|
||||
|
||||
bind "cond-scheme?" (fun args ->
|
||||
match args with
|
||||
@@ -1504,6 +1516,7 @@ let cli_mode mode =
|
||||
Filename.concat base "render.sx";
|
||||
Filename.concat web_base "adapter-html.sx";
|
||||
Filename.concat web_base "adapter-sx.sx";
|
||||
Filename.concat web_base "web-forms.sx";
|
||||
] in
|
||||
(* Load spec files for all CLI modes that need rendering *)
|
||||
(if mode = "aser" || mode = "aser-slot" || mode = "render" then
|
||||
@@ -1588,6 +1601,7 @@ let test_mode () =
|
||||
Filename.concat web_base "signals.sx";
|
||||
Filename.concat web_base "adapter-html.sx";
|
||||
Filename.concat web_base "adapter-sx.sx";
|
||||
Filename.concat web_base "web-forms.sx";
|
||||
] in
|
||||
cli_load_files env files;
|
||||
(* Register JIT *)
|
||||
|
||||
Reference in New Issue
Block a user