Add SSE, response headers, view transitions, and 5 new sx attributes
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled

Implement missing SxEngine features:
- SSE (sx-sse, sx-sse-swap) with EventSource management and auto-cleanup
- Response headers: SX-Trigger, SX-Retarget, SX-Reswap, SX-Redirect,
  SX-Refresh, SX-Location, SX-Replace-Url, SX-Trigger-After-Swap/Settle
- View Transitions API: transition:true swap modifier + global config
- every:<time> trigger for polling (setInterval)
- sx-replace-url (replaceState instead of pushState)
- sx-disabled-elt (disable elements during request)
- sx-prompt (window.prompt, value sent as SX-Prompt header)
- sx-params (filter form parameters: *, none, not x,y, x,y)

Adds docs (ATTR_DETAILS, BEHAVIOR_ATTRS, headers, events), demo
components in reference.sx, API endpoints (prompt-echo, sse-time),
and 27 new unit tests for engine logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 11:55:21 +00:00
parent 3bffc212cc
commit 213421516e
6 changed files with 888 additions and 10 deletions

View File

@@ -120,6 +120,12 @@ BEHAVIOR_ATTRS = [
("sx-validate", "Run browser constraint validation (or custom validator) before sending the request", True),
("sx-ignore", "Ignore element and its subtree during morph/swap — no updates applied", True),
("sx-optimistic", "Apply optimistic UI updates immediately, reconcile on server response", True),
("sx-replace-url", "Replace the current URL in the browser location bar (replaceState instead of pushState)", True),
("sx-disabled-elt", "CSS selector for elements to disable during the request", True),
("sx-prompt", "Show a prompt dialog before the request — input is sent as SX-Prompt header", True),
("sx-params", 'Filter which form parameters are sent: "*" (all), "none", "not x,y", or "x,y"', True),
("sx-sse", "Connect to a Server-Sent Events endpoint for real-time server push", True),
("sx-sse-swap", "SSE event name to listen for and swap into the target (default: message)", True),
]
SX_UNIQUE_ATTRS = [
@@ -140,11 +146,21 @@ REQUEST_HEADERS = [
("SX-Css", "hash or class list", "CSS classes/hash the client already has"),
("SX-History-Restore", "true", "Set when restoring from browser history"),
("SX-Css-Hash", "8-char hash", "Hash of the client's known CSS class set"),
("SX-Prompt", "string", "Value entered by the user in a window.prompt dialog (from sx-prompt)"),
]
RESPONSE_HEADERS = [
("SX-Css-Hash", "8-char hash", "Hash of the cumulative CSS class set after this response"),
("SX-Css-Add", "class1,class2,...", "New CSS classes added by this response"),
("SX-Trigger", "event or JSON", "Dispatch custom event(s) on the target element after the request"),
("SX-Trigger-After-Swap", "event or JSON", "Dispatch custom event(s) after the swap completes"),
("SX-Trigger-After-Settle", "event or JSON", "Dispatch custom event(s) after the DOM settles"),
("SX-Retarget", "CSS selector", "Override the target element for this response"),
("SX-Reswap", "swap strategy", "Override the swap strategy for this response"),
("SX-Redirect", "URL", "Redirect the browser to a new URL (full navigation)"),
("SX-Refresh", "true", "Reload the current page"),
("SX-Location", "URL or JSON", "Client-side navigation — fetch URL, swap into #main-panel, pushState"),
("SX-Replace-Url", "URL", "Replace the current URL using replaceState (server-side override)"),
]
# ---------------------------------------------------------------------------
@@ -158,6 +174,10 @@ EVENTS = [
("sx:afterSettle", "Fired after the DOM has settled (scripts executed, etc)."),
("sx:responseError", "Fired on HTTP error responses (4xx, 5xx)."),
("sx:sendError", "Fired when the request fails to send (network error)."),
("sx:validationFailed", "Fired when sx-validate blocks a request due to invalid form data."),
("sx:sseOpen", "Fired when an SSE connection is established."),
("sx:sseMessage", "Fired when an SSE message is received and swapped."),
("sx:sseError", "Fired when an SSE connection encounters an error."),
]
# ---------------------------------------------------------------------------
@@ -178,6 +198,7 @@ JS_API = [
("Sx.hydrate(root)", "Find and render all [data-sx] elements within root"),
("SxEngine.process(root)", "Process all sx attributes in the DOM subtree"),
("SxEngine.executeRequest(elt, verb, url)", "Programmatically trigger an sx request"),
("SxEngine.config.globalViewTransitions", "Enable View Transitions API globally for all swaps (default: false)"),
]
# ---------------------------------------------------------------------------
@@ -855,4 +876,110 @@ ATTR_DETAILS: dict[str, dict] = {
' "")'
),
},
# --- New attributes ---
"sx-replace-url": {
"description": (
"Replace the current URL in the browser location bar using replaceState "
"instead of pushState. The URL changes but no new history entry is created, "
"so the back button still goes to the previous page."
),
"demo": "ref-replace-url-demo",
"example": (
'(button :sx-get "/reference/api/time"\n'
' :sx-target "#ref-replurl-result"\n'
' :sx-swap "innerHTML"\n'
' :sx-replace-url "true"\n'
' "Load (replaces URL)")'
),
},
"sx-disabled-elt": {
"description": (
"CSS selector for elements to disable during the request. "
"The matched elements have their disabled property set to true when the "
"request starts, and restored to false when the request completes (success or error). "
"Useful for preventing double-submits on forms."
),
"demo": "ref-disabled-elt-demo",
"example": (
'(button :sx-get "/reference/api/slow-echo"\n'
' :sx-target "#ref-diselt-result"\n'
' :sx-swap "innerHTML"\n'
' :sx-disabled-elt "this"\n'
' :sx-vals "{\\"q\\": \\"hello\\"}"\n'
' "Click (disables during request)")'
),
},
"sx-prompt": {
"description": (
"Show a window.prompt dialog before the request. "
"If the user cancels, the request is not sent. "
"The entered value is sent as the SX-Prompt request header."
),
"demo": "ref-prompt-demo",
"example": (
'(button :sx-get "/reference/api/prompt-echo"\n'
' :sx-target "#ref-prompt-result"\n'
' :sx-swap "innerHTML"\n'
' :sx-prompt "Enter your name:"\n'
' "Prompt & send")'
),
"handler": (
'(defhandler ref-prompt-echo (&key)\n'
' (let ((name (or (header "SX-Prompt") "anonymous")))\n'
' (span "Hello, " (strong name) "!")))'
),
},
"sx-params": {
"description": (
"Filter which form parameters are sent with the request. "
'Values: "*" (all, default), "none" (no params), '
'"not x,y" (exclude named params), or "x,y" (include only named params).'
),
"demo": "ref-params-demo",
"example": (
'(form :sx-post "/reference/api/echo-vals"\n'
' :sx-target "#ref-params-result"\n'
' :sx-swap "innerHTML"\n'
' :sx-params "name"\n'
' (input :type "text" :name "name" :placeholder "Name (sent)")\n'
' (input :type "text" :name "secret" :placeholder "Secret (filtered)")\n'
' (button :type "submit" "Submit (only name)"))'
),
},
"sx-sse": {
"description": (
"Connect to a Server-Sent Events endpoint for real-time server push. "
"The value is the URL to connect to. Use sx-sse-swap to specify which "
"SSE event name to listen for. Incoming data is swapped into the target "
"using the standard sx-swap strategy. The EventSource is automatically "
"closed when the element is removed from the DOM."
),
"demo": "ref-sse-demo",
"example": (
'(div :sx-sse "/reference/api/sse-time"\n'
' :sx-sse-swap "time"\n'
' :sx-target "#ref-sse-result"\n'
' :sx-swap "innerHTML"\n'
' (div :id "ref-sse-result"\n'
' "Waiting for SSE updates..."))'
),
},
"sx-sse-swap": {
"description": (
"Specifies the SSE event name to listen for on the parent sx-sse connection. "
'Defaults to "message" if not specified. Multiple sx-sse-swap elements can '
"listen for different event types on the same connection."
),
"demo": "ref-sse-demo",
"example": (
'(div :sx-sse "/events/stream"\n'
' (div :sx-sse-swap "notifications"\n'
' :sx-target "#notif-area" :sx-swap "beforeend"\n'
' "Listening for notifications...")\n'
' (div :sx-sse-swap "status"\n'
' :sx-target "#status-bar" :sx-swap "innerHTML"\n'
' "Listening for status updates..."))'
),
},
}