Files
rose-ash/sx/content/pages.py
giles 71c2003a60 OCaml evaluator for page dispatch + handler aser, 83/83 Playwright tests
Major architectural change: page function dispatch and handler execution
now go through the OCaml kernel instead of the Python bootstrapped evaluator.

OCaml integration:
- Page dispatch: bridge.eval() evaluates SX URL expressions (geography, marshes, etc.)
- Handler aser: bridge.aser() serializes handler responses as SX wire format
- _ensure_components loads all .sx files into OCaml kernel (spec, web adapter, handlers)
- defhandler/defpage registered as no-op special forms so handler files load
- helper IO primitive dispatches to Python page helpers + IO handlers
- ok-raw response format for SX wire format (no double-escaping)
- Natural list serialization in eval (no (list ...) wrapper)
- Clean pipe: _read_until_ok always sends io-response on error

SX adapter (aser):
- scope-emit!/scope-peek aliases to avoid CEK special form conflict
- aser-fragment/aser-call: strings starting with "(" pass through unserialized
- Registered cond-scheme?, is-else-clause?, primitive?, get-primitive in kernel
- random-int, parse-int as kernel primitives; json-encode, into via IO bridge

Handler migration:
- All IO calls converted to (helper "name" args...) pattern
- request-arg, request-form, state-get, state-set!, now, component-source etc.
- Fixed bare (effect ...) in island bodies leaking disposer functions as text
- Fixed lower-case → lower, ~search-results → ~examples/search-results

Reactive islands:
- sx-hydrate-islands called after client-side navigation swap
- force-dispose-islands-in for outerHTML swaps (clears hydration markers)
- clear-processed! platform primitive for re-hydration

Content restructuring:
- Design, event bridge, named stores, phase 2 consolidated into reactive overview
- Marshes split into overview + 5 example sub-pages
- Nav links use sx-get/sx-target for client-side navigation

Playwright test suite (sx/tests/test_demos.py):
- 83 tests covering hypermedia demos, reactive islands, marshes, spec explorer
- Server-side rendering, handler interactions, island hydration, navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 17:22:51 +00:00

1382 lines
60 KiB
Python

"""Documentation content for the sx docs site.
Data structures consumed by helpers.py for pages that need server-side data.
Navigation is defined in nav-data.sx (the single source of truth).
"""
from __future__ import annotations
# ---------------------------------------------------------------------------
# Reference: Attributes
# ---------------------------------------------------------------------------
REQUEST_ATTRS = [
("sx-get", "Issue a GET request to the given URL", True),
("sx-post", "Issue a POST request to the given URL", True),
("sx-put", "Issue a PUT request to the given URL", True),
("sx-delete", "Issue a DELETE request to the given URL", True),
("sx-patch", "Issue a PATCH request to the given URL", True),
]
BEHAVIOR_ATTRS = [
("sx-trigger", "Specifies the event that triggers the request. Modifiers: once, changed, delay:<time>, from:<selector>, intersect, revealed, load, every:<time>", True),
("sx-target", "CSS selector for the target element to update", True),
("sx-swap", "How to swap the response: outerHTML, innerHTML, afterend, beforeend, afterbegin, beforebegin, delete, none", True),
("sx-swap-oob", "Out-of-band swap — update elements elsewhere in the DOM by ID", True),
("sx-select", "CSS selector to pick a fragment from the response", True),
("sx-confirm", "Shows a confirmation dialog before issuing the request", True),
("sx-push-url", "Push the request URL into the browser location bar", True),
("sx-sync", "Synchronization strategy for requests from this element", True),
("sx-encoding", "Set the encoding for the request (e.g. multipart/form-data)", True),
("sx-headers", "Add headers to the request as a JSON string", True),
("sx-include", "Include additional element values in the request", True),
("sx-vals", "Add values to the request as a JSON string", True),
("sx-media", "Only enable this element when the media query matches", True),
("sx-disable", "Disable sx processing on this element and its children", True),
("sx-on:*", "Inline event handler — e.g. sx-on:click runs JavaScript on event", True),
("sx-boost", "Progressively enhance all links and forms in a container with AJAX navigation. Value can be a target selector.", True),
("sx-preload", "Preload content on hover/focus for instant response on click", True),
("sx-preserve", "Preserve element across swaps — keeps DOM state, event listeners, and scroll position", True),
("sx-indicator", "CSS selector for a loading indicator element to show/hide during requests", True),
("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 = [
("sx-retry", "Exponential backoff retry on request failure", True),
("data-sx", "Client-side rendering — evaluate the sx source in this attribute and render into the element", True),
("data-sx-env", "Provide environment variables as JSON for data-sx rendering", True),
]
# ---------------------------------------------------------------------------
# Reference: Headers
# ---------------------------------------------------------------------------
REQUEST_HEADERS = [
("SX-Request", "true", "Set on every sx-initiated request"),
("SX-Current-URL", "URL", "The current URL of the browser"),
("SX-Target", "CSS selector", "The target element for the response"),
("SX-Components", "~comp1,~comp2,...", "Component names the client already has cached"),
("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)"),
]
# ---------------------------------------------------------------------------
# Reference: Events
# ---------------------------------------------------------------------------
EVENTS = [
("sx:beforeRequest", "Fired before an sx request is issued. Call preventDefault() to cancel."),
("sx:afterRequest", "Fired after a successful sx response is received."),
("sx:afterSwap", "Fired after the response has been swapped into the DOM."),
("sx:responseError", "Fired on HTTP error responses (4xx, 5xx)."),
("sx:requestError", "Fired when the request fails to send (network error, abort)."),
("sx:validationFailed", "Fired when sx-validate blocks a request due to invalid form data."),
("sx:clientRoute", "Fired after successful client-side routing (no server request)."),
("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."),
]
# ---------------------------------------------------------------------------
# Reference: JS API
# ---------------------------------------------------------------------------
JS_API = [
("Sx.parse(text)", "Parse a single s-expression from text"),
("Sx.parseAll(text)", "Parse multiple s-expressions from text"),
("Sx.eval(expr, env)", "Evaluate an expression in the given environment"),
("Sx.render(expr, env)", "Render an expression to DOM nodes"),
("Sx.renderToString(expr, env)", "Render an expression to an HTML string (requires sx-test.js)"),
("Sx.renderComponent(name, kwargs, env)", "Render a named component with keyword arguments"),
("Sx.loadComponents(text)", "Parse and register component definitions"),
("Sx.getEnv()", "Get the current component environment"),
("Sx.mount(target, expr, env)", "Mount an expression into a DOM element"),
("Sx.update(target, newEnv)", "Re-render an element with new environment data"),
("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)"),
]
# ---------------------------------------------------------------------------
# Primitives
# ---------------------------------------------------------------------------
PRIMITIVES = {
"Arithmetic": ["+", "-", "*", "/", "mod", "sqrt", "pow", "abs", "floor", "ceil", "round", "min", "max"],
"Comparison": ["=", "!=", "<", ">", "<=", ">="],
"Logic": ["not", "and", "or"],
"String": ["str", "upper", "lower", "trim", "split", "join", "index-of", "starts-with?", "ends-with?", "replace", "substring"],
"Collections": ["list", "dict", "len", "first", "last", "rest", "nth", "cons", "append", "keys", "vals", "merge", "assoc", "range", "concat", "reverse", "sort", "flatten", "zip"],
"Higher-Order": ["map", "map-indexed", "filter", "reduce", "some", "every?", "for-each"],
"Predicates": ["nil?", "number?", "string?", "list?", "dict?", "empty?", "contains?", "odd?", "even?", "zero?"],
"Type Conversion": ["int", "float", "number"],
}
# ---------------------------------------------------------------------------
# Example items for delete demo
# ---------------------------------------------------------------------------
DELETE_DEMO_ITEMS = [
("1", "Implement dark mode"),
("2", "Fix login bug"),
("3", "Write documentation"),
("4", "Deploy to production"),
("5", "Add unit tests"),
]
# ---------------------------------------------------------------------------
# Static data for new examples
# ---------------------------------------------------------------------------
SEARCH_LANGUAGES = [
"Python", "JavaScript", "TypeScript", "Rust", "Go", "Java", "C", "C++",
"Ruby", "Elixir", "Haskell", "Clojure", "Scala", "Kotlin", "Swift",
"Zig", "OCaml", "Lua", "Perl", "PHP",
]
PROFILE_DEFAULT = {"name": "Ada Lovelace", "email": "ada@example.com", "role": "Engineer"}
BULK_USERS = [
{"id": "1", "name": "Alice Chen", "email": "alice@example.com", "status": "active"},
{"id": "2", "name": "Bob Rivera", "email": "bob@example.com", "status": "inactive"},
{"id": "3", "name": "Carol Zhang", "email": "carol@example.com", "status": "active"},
{"id": "4", "name": "Dan Okafor", "email": "dan@example.com", "status": "inactive"},
{"id": "5", "name": "Eve Larsson", "email": "eve@example.com", "status": "active"},
]
VALUE_SELECT_DATA = {
"Languages": ["Python", "JavaScript", "Rust", "Go"],
"Frameworks": ["Quart", "FastAPI", "React", "Svelte"],
"Databases": ["PostgreSQL", "Redis", "SQLite", "MongoDB"],
}
EDIT_ROW_DATA = [
{"id": "1", "name": "Widget A", "price": "19.99", "stock": "142"},
{"id": "2", "name": "Widget B", "price": "24.50", "stock": "89"},
{"id": "3", "name": "Widget C", "price": "12.00", "stock": "305"},
{"id": "4", "name": "Widget D", "price": "45.00", "stock": "67"},
]
# ---------------------------------------------------------------------------
# Reference: Header detail pages
# ---------------------------------------------------------------------------
HEADER_DETAILS: dict[str, dict] = {
# --- Request Headers ---
"SX-Request": {
"direction": "request",
"description": (
"Sent on every sx-initiated request. Allows the server to distinguish "
"AJAX partial requests from full page loads, and return the appropriate "
"response format (fragment vs full page)."
),
"example": (
';; Server-side: check for sx request\n'
'(if (header "SX-Request")\n'
' ;; Return a fragment\n'
' (div :class "result" "Partial content")\n'
' ;; Return full page\n'
' (~full-page-layout ...))'
),
},
"SX-Current-URL": {
"direction": "request",
"description": (
"Sends the browser's current URL so the server knows where the user is. "
"Useful for server-side logic that depends on context — e.g. highlighting "
"the current nav item, or returning context-appropriate content."
),
"example": (
';; Server reads the current URL to decide context\n'
'(let ((url (header "SX-Current-URL")))\n'
' (nav\n'
' (a :href "/docs" :class (if (starts-with? url "/docs") "active" "") "Docs")\n'
' (a :href "/api" :class (if (starts-with? url "/api") "active" "") "API")))'
),
},
"SX-Target": {
"direction": "request",
"description": (
"Tells the server which element will receive the response. "
"The server can use this to tailor the response — for example, "
"returning different content depending on whether the target is "
"a sidebar, modal, or main panel."
),
"example": (
';; Server checks target to decide response format\n'
'(let ((target (header "SX-Target")))\n'
' (if (= target "#sidebar")\n'
' (~compact-summary :data data)\n'
' (~full-detail :data data)))'
),
},
"SX-Components": {
"direction": "request",
"description": (
"Comma-separated list of component names the client already has cached. "
"The server can skip sending defcomp definitions the client already knows, "
"reducing response size. This is the component caching protocol."
),
"example": (
';; Client sends: SX-Components: ~card,~shared:layout/nav-link,~footer\n'
';; Server omits those defcomps from the response.\n'
';; Only new/changed components are sent.\n'
'(response\n'
' :components (filter-new known-components)\n'
' :content (~page-content))'
),
},
"SX-Css": {
"direction": "request",
"description": (
"Sends the CSS classes or hash the client already has. "
"The server uses this to send only new CSS rules the client needs, "
"avoiding duplicate rule injection. Part of the on-demand CSS protocol."
),
"example": (
';; Client sends hash of known CSS classes\n'
';; Server compares and only returns new classes\n'
'(let ((client-css (header "SX-Css")))\n'
' (set-header "SX-Css-Add"\n'
' (join "," (diff new-classes client-css))))'
),
},
"SX-History-Restore": {
"direction": "request",
"description": (
"Set to \"true\" when the browser restores a page from history (back/forward). "
"The server can use this to return cached content or skip side effects "
"that should only happen on initial navigation."
),
"example": (
';; Skip analytics on history restore\n'
'(when (not (header "SX-History-Restore"))\n'
' (track-page-view url))\n'
'(~page-content :data data)'
),
},
"SX-Css-Hash": {
"direction": "both",
"description": (
"Request: 8-character hash of the client's known CSS class set. "
"Response: hash of the cumulative CSS set after this response. "
"Client stores the response hash and sends it on the next request, "
"enabling efficient CSS delta tracking."
),
"example": (
';; Request header: SX-Css-Hash: a1b2c3d4\n'
';; Server compares hash to decide if CSS diff needed\n'
';;\n'
';; Response header: SX-Css-Hash: e5f6g7h8\n'
';; Client stores new hash for next request'
),
},
"SX-Prompt": {
"direction": "request",
"description": (
"Contains the value entered by the user in a window.prompt() dialog, "
"triggered by the sx-prompt attribute. Allows collecting a single text "
"input without a form."
),
"example": (
';; Button triggers a prompt dialog\n'
'(button :sx-get "/api/rename"\n'
' :sx-prompt "Enter new name:"\n'
' "Rename")\n'
'\n'
';; Server reads the prompted value\n'
'(let ((name (header "SX-Prompt")))\n'
' (span "Renamed to: " (strong name)))'
),
"demo": "ref-header-prompt-demo",
},
# --- Response Headers ---
"SX-Css-Add": {
"direction": "response",
"description": (
"Comma-separated list of new CSS class names added by this response. "
"The client injects the corresponding CSS rules into the document. "
"Only classes the client doesn't already have are included."
),
"example": (
';; Server response includes new CSS classes\n'
';; SX-Css-Add: bg-emerald-500,text-white,rounded-xl\n'
';;\n'
';; Client automatically injects rules for these\n'
';; classes from the style dictionary.'
),
},
"SX-Trigger": {
"direction": "response",
"description": (
"Dispatch custom DOM event(s) on the target element after the response "
"is received. Can be a simple event name or JSON for multiple events "
"with detail data. Useful for coordinating UI updates across components."
),
"example": (
';; Simple event\n'
';; SX-Trigger: itemAdded\n'
';;\n'
';; Multiple events with data\n'
';; SX-Trigger: {"itemAdded": {"id": 42}, "showNotification": {"message": "Saved!"}}\n'
';;\n'
';; Listen in SX:\n'
'(div :sx-on:itemAdded "this.querySelector(\'.count\').textContent = event.detail.id")'
),
"demo": "ref-header-trigger-demo",
},
"SX-Trigger-After-Swap": {
"direction": "response",
"description": (
"Like SX-Trigger, but fires after the DOM swap completes. "
"Use this when your event handler needs to reference the new DOM content "
"that was just swapped in."
),
"example": (
';; Server signals that new content needs initialization\n'
';; SX-Trigger-After-Swap: contentReady\n'
';;\n'
';; Client initializes after swap\n'
'(div :sx-on:contentReady "initCharts(this)")'
),
},
"SX-Trigger-After-Settle": {
"direction": "response",
"description": (
"Like SX-Trigger, but fires after the DOM has fully settled — "
"scripts executed, transitions complete. The latest point to react "
"to a response."
),
"example": (
';; SX-Trigger-After-Settle: animationReady\n'
';;\n'
';; Trigger animations after everything has settled\n'
'(div :sx-on:animationReady "this.classList.add(\'fade-in\')")'
),
},
"SX-Retarget": {
"direction": "response",
"description": (
"Override the target element for this response. The server can redirect "
"content to a different element than what the client specified in sx-target. "
"Useful for error messages or redirecting content dynamically."
),
"example": (
';; Client targets a form result area\n'
'(form :sx-post "/api/save"\n'
' :sx-target "#result" ...)\n'
'\n'
';; Server redirects errors to a different element\n'
';; SX-Retarget: #error-banner\n'
'(div :class "error" "Validation failed")'
),
"demo": "ref-header-retarget-demo",
},
"SX-Reswap": {
"direction": "response",
"description": (
"Override the swap strategy for this response. The server can change "
"how content is inserted regardless of what the client specified in sx-swap. "
"Useful when the server decides the swap mode based on the result."
),
"example": (
';; Client expects innerHTML swap\n'
'(button :sx-get "/api/check"\n'
' :sx-target "#panel" :sx-swap "innerHTML" ...)\n'
'\n'
';; Server overrides to append instead\n'
';; SX-Reswap: beforeend\n'
'(div :class "notification" "New item added")'
),
},
"SX-Redirect": {
"direction": "response",
"description": (
"Redirect the browser to a new URL using full page navigation. "
"Unlike sx-push-url which does client-side history, this triggers "
"a real browser navigation — useful after form submissions like login or checkout."
),
"example": (
';; After successful login, redirect to dashboard\n'
';; SX-Redirect: /dashboard\n'
';;\n'
';; Server handler:\n'
'(when (valid-credentials? user pass)\n'
' (set-header "SX-Redirect" "/dashboard")\n'
' (span "Redirecting..."))'
),
},
"SX-Refresh": {
"direction": "response",
"description": (
"Set to \"true\" to reload the current page. "
"A blunt tool — useful when server-side state has changed significantly "
"and a partial update won't suffice."
),
"example": (
';; After a major state change, force refresh\n'
';; SX-Refresh: true\n'
';;\n'
';; Server handler:\n'
'(when (deploy-complete?)\n'
' (set-header "SX-Refresh" "true")\n'
' (span "Deployed — refreshing..."))'
),
},
"SX-Location": {
"direction": "response",
"description": (
"Trigger client-side navigation: fetch the given URL, swap it into "
"#main-panel, and push to browser history. Like clicking an sx-boosted link, "
"but triggered from the server. Can be a URL string or JSON with options."
),
"example": (
';; Simple: navigate to a page\n'
';; SX-Location: /docs/introduction\n'
';;\n'
';; With options:\n'
';; SX-Location: {"path": "/(language.(doc.intro))", "target": "#sidebar", "swap": "innerHTML"}'
),
},
"SX-Replace-Url": {
"direction": "response",
"description": (
"Replace the current URL using history.replaceState without creating "
"a new history entry. Useful for normalizing URLs after redirects, "
"or updating the URL to reflect server-resolved state."
),
"example": (
';; Normalize URL after slug resolution\n'
';; SX-Replace-Url: /docs/introduction\n'
';;\n'
';; Server handler:\n'
'(let ((canonical (resolve-slug slug)))\n'
' (set-header "SX-Replace-Url" canonical)\n'
' (~doc-content :slug canonical))'
),
},
}
# ---------------------------------------------------------------------------
# Reference: Event detail pages
# ---------------------------------------------------------------------------
EVENT_DETAILS: dict[str, dict] = {
"sx:beforeRequest": {
"description": (
"Fired on the triggering element before an sx request is issued. "
"Call event.preventDefault() to cancel the request entirely. "
"Useful for validation, confirmation, or conditional request logic."
),
"example": (
';; Cancel request if form is empty\n'
'(form :sx-post "/api/save"\n'
' :sx-target "#result"\n'
' :sx-on:sx:beforeRequest "if (!this.querySelector(\'input\').value) event.preventDefault()"\n'
' (input :name "data" :placeholder "Required")\n'
' (button :type "submit" "Save"))'
),
"demo": "ref-event-before-request-demo",
},
"sx:afterRequest": {
"description": (
"Fired on the triggering element after a successful sx response is received, "
"before the swap happens. event.detail contains the response status. "
"Use this for logging, analytics, or pre-swap side effects."
),
"example": (
';; Log successful requests\n'
'(button :sx-get "/api/data"\n'
' :sx-target "#result"\n'
' :sx-on:sx:afterRequest "console.log(\'Response received\', event.detail)"\n'
' "Load data")'
),
"demo": "ref-event-after-request-demo",
},
"sx:afterSwap": {
"description": (
"Fired on the triggering element after the response content has been "
"swapped into the DOM. event.detail contains the target element and swap "
"style. Use this to initialize UI on newly inserted content."
),
"example": (
';; Run code after content is swapped in\n'
'(button :sx-get "/api/items"\n'
' :sx-target "#item-list"\n'
' :sx-on:sx:afterSwap "console.log(\'Swapped into\', event.detail.target)"\n'
' "Load items")'
),
"demo": "ref-event-after-swap-demo",
},
"sx:responseError": {
"description": (
"Fired when the server responds with an HTTP error (4xx or 5xx). "
"event.detail contains the status code and response text. "
"Use this for error handling, showing notifications, or retry logic."
),
"example": (
';; Show error notification\n'
'(div :sx-on:sx:responseError "alert(\'Error: \' + event.detail.status)"\n'
' (button :sx-get "/api/risky"\n'
' :sx-target "#result"\n'
' "Try it")\n'
' (div :id "result"))'
),
"demo": "ref-event-response-error-demo",
},
"sx:requestError": {
"description": (
"Fired when the request fails to send — typically a network error, "
"DNS failure, or CORS issue. Unlike sx:responseError, no HTTP response "
"was received at all. Aborted requests (e.g. from sx-sync) do not fire this event."
),
"example": (
';; Handle network failures\n'
'(div :sx-on:sx:requestError "this.querySelector(\'.status\').textContent = \'Offline\'"\n'
' (button :sx-get "/api/data"\n'
' :sx-target "#result"\n'
' "Load")\n'
' (span :class "status")\n'
' (div :id "result"))'
),
"demo": "ref-event-request-error-demo",
},
"sx:validationFailed": {
"description": (
"Fired when sx-validate is set and the form fails HTML5 validation. "
"The request is not sent. Use this to show custom validation UI "
"or highlight invalid fields."
),
"example": (
';; Highlight invalid fields\n'
'(form :sx-post "/api/save"\n'
' :sx-validate "true"\n'
' :sx-on:sx:validationFailed "this.classList.add(\'shake\')"\n'
' (input :type "email" :required "true" :name "email"\n'
' :placeholder "Email (required)")\n'
' (button :type "submit" "Save"))'
),
"demo": "ref-event-validation-failed-demo",
},
"sx:clientRoute": {
"description": (
"Fired on the swap target after successful client-side routing. "
"No server request was made — the page was rendered entirely in the browser "
"from component definitions the client already has. "
"event.detail contains the pathname. Use this to update navigation state, "
"analytics, or other side effects that should run on client-only navigation. "
"The event bubbles, so you can listen on document.body."
),
"example": (
';; Pages with no :data are client-routable.\n'
';; sx-boost containers try client routing first.\n'
';; On success, sx:clientRoute fires on the swap target.\n'
'(nav :sx-boost "#main-panel"\n'
' (a :href "/(etc.(essay))" "Essays")\n'
' (a :href "/(etc.(plan))" "Plans"))\n'
'\n'
';; Listen in body.js:\n'
';; document.body.addEventListener("sx:clientRoute",\n'
';; function(e) { updateNav(e.detail.pathname); })'
),
"demo": "ref-event-client-route-demo",
},
"sx:sseOpen": {
"description": (
"Fired when a Server-Sent Events connection is successfully established. "
"Use this to update connection status indicators."
),
"example": (
';; Show connected status\n'
'(div :sx-sse "/api/stream"\n'
' :sx-on:sx:sseOpen "this.querySelector(\'.status\').textContent = \'Connected\'"\n'
' (span :class "status" "Connecting...")\n'
' (div :id "messages"))'
),
"demo": "ref-event-sse-open-demo",
},
"sx:sseMessage": {
"description": (
"Fired when an SSE message is received and swapped into the DOM. "
"event.detail contains the message data. Fires for each individual message."
),
"example": (
';; Count received messages\n'
'(div :sx-sse "/api/stream"\n'
' :sx-sse-swap "update"\n'
' :sx-on:sx:sseMessage "this.dataset.count = (parseInt(this.dataset.count||0)+1)"\n'
' (span :class "count" "0") " messages received")'
),
"demo": "ref-event-sse-message-demo",
},
"sx:sseError": {
"description": (
"Fired when an SSE connection encounters an error or is closed unexpectedly. "
"Use this to show reconnection status or fall back to polling."
),
"example": (
';; Show disconnected status\n'
'(div :sx-sse "/api/stream"\n'
' :sx-on:sx:sseError "this.querySelector(\'.status\').textContent = \'Disconnected\'"\n'
' (span :class "status" "Connecting...")\n'
' (div :id "messages"))'
),
"demo": "ref-event-sse-error-demo",
},
}
# ---------------------------------------------------------------------------
# Reference: Attribute detail pages
# ---------------------------------------------------------------------------
ATTR_DETAILS: dict[str, dict] = {
# --- Request Attributes ---
"sx-get": {
"description": (
"Issues a GET request to the given URL when triggered. "
"The response HTML is swapped into the target element. "
"This is the most common sx attribute — use it for loading content, "
"navigation, and any read operation."
),
"demo": "ref-get-demo",
"example": (
'(button :sx-get "/(geography.(hypermedia.(reference.(api.time))))"\n'
' :sx-target "#ref-get-result"\n'
' :sx-swap "innerHTML"\n'
' "Load server time")'
),
"handler": (
'(defhandler ref-time (&key)\n'
' (let ((now (format-time (now) "%H:%M:%S")))\n'
' (span :class "text-stone-800 text-sm"\n'
' "Server time: " (strong now))))'
),
},
"sx-post": {
"description": (
"Issues a POST request to the given URL. "
"Form values from the enclosing form (or sx-include target) are sent as the request body. "
"Use for creating resources, submitting forms, and any write operation."
),
"demo": "ref-post-demo",
"example": (
'(form :sx-post "/(geography.(hypermedia.(reference.(api.greet))))"\n'
' :sx-target "#ref-post-result"\n'
' :sx-swap "innerHTML"\n'
' (input :type "text" :name "name"\n'
' :placeholder "Your name")\n'
' (button :type "submit" "Greet"))'
),
"handler": (
'(defhandler ref-greet (&key)\n'
' (let ((name (or (form-data "name") "stranger")))\n'
' (span :class "text-stone-800 text-sm"\n'
' "Hello, " (strong name) "!")))'
),
},
"sx-put": {
"description": (
"Issues a PUT request to the given URL. "
"Used for full replacement updates of a resource. "
"Form values are sent as the request body."
),
"demo": "ref-put-demo",
"example": (
'(button :sx-put "/(geography.(hypermedia.(reference.(api.status))))"\n'
' :sx-target "#ref-put-view"\n'
' :sx-swap "innerHTML"\n'
' :sx-vals "{\\"status\\": \\"published\\"}"\n'
' "Publish")'
),
"handler": (
'(defhandler ref-status (&key)\n'
' (let ((status (or (form-data "status") "unknown")))\n'
' (span :class "text-stone-700 text-sm"\n'
' "Status: " (strong status) " — updated via PUT")))'
),
},
"sx-delete": {
"description": (
"Issues a DELETE request to the given URL. "
"Commonly paired with sx-confirm for a confirmation dialog, "
'and sx-swap "delete" to remove the element from the DOM.'
),
"demo": "ref-delete-demo",
"example": (
'(button :sx-delete "/(geography.(hypermedia.(reference.(api.(item.1)))))"\n'
' :sx-target "#ref-del-1"\n'
' :sx-swap "delete"\n'
' "Remove")'
),
"handler": (
'(defhandler ref-delete (&key item-id)\n'
' ;; Empty response — swap "delete" removes the target\n'
' "")'
),
},
"sx-patch": {
"description": (
"Issues a PATCH request to the given URL. "
"Used for partial updates — only changed fields are sent. "
"Form values are sent as the request body."
),
"demo": "ref-patch-demo",
"example": (
'(button :sx-patch "/(geography.(hypermedia.(reference.(api.theme))))"\n'
' :sx-vals "{\\"theme\\": \\"dark\\"}"\n'
' :sx-target "#ref-patch-val"\n'
' :sx-swap "innerHTML"\n'
' "Dark")'
),
"handler": (
'(defhandler ref-theme (&key)\n'
' (let ((theme (or (form-data "theme") "unknown")))\n'
' (str theme)))'
),
},
# --- Behavior Attributes ---
"sx-trigger": {
"description": (
"Specifies which DOM event triggers the request. "
"Defaults to 'click' for most elements and 'submit' for forms. "
"Supports modifiers: once, changed, delay:<time>, from:<selector>, "
"intersect, revealed, load, every:<time>. "
"Multiple triggers can be comma-separated."
),
"demo": "ref-trigger-demo",
"example": (
'(input :type "text" :name "q"\n'
' :placeholder "Type to search..."\n'
' :sx-get "/(geography.(hypermedia.(reference.(api.trigger-search))))"\n'
' :sx-trigger "input changed delay:300ms"\n'
' :sx-target "#ref-trigger-result"\n'
' :sx-swap "innerHTML")'
),
"handler": (
'(defhandler ref-trigger-search (&key)\n'
' (let ((q (or (request-arg "q") "")))\n'
' (if (empty? q)\n'
' (span "Start typing to trigger a search.")\n'
' (span "Results for: " (strong q)))))'
),
},
"sx-target": {
"description": (
"CSS selector identifying which element receives the response content. "
'Defaults to the element itself. Use "closest <selector>" to find '
"the nearest ancestor matching the selector."
),
"demo": "ref-target-demo",
"example": (
';; Two buttons targeting different elements\n'
'(button :sx-get "/(geography.(hypermedia.(reference.(api.time))))"\n'
' :sx-target "#ref-target-a"\n'
' :sx-swap "innerHTML"\n'
' "Update Box A")\n'
'\n'
'(button :sx-get "/(geography.(hypermedia.(reference.(api.time))))"\n'
' :sx-target "#ref-target-b"\n'
' :sx-swap "innerHTML"\n'
' "Update Box B")'
),
},
"sx-swap": {
"description": (
"Controls how the response is swapped into the target element. "
"Values: innerHTML (default), outerHTML, afterend, beforeend, "
"afterbegin, beforebegin, delete, none."
),
"demo": "ref-swap-demo",
"example": (
';; Append to the end of a list\n'
'(button :sx-get "/(geography.(hypermedia.(reference.(api.swap-item))))"\n'
' :sx-target "#ref-swap-list"\n'
' :sx-swap "beforeend"\n'
' "beforeend")\n'
'\n'
';; Prepend to the start\n'
'(button :sx-get "/(geography.(hypermedia.(reference.(api.swap-item))))"\n'
' :sx-target "#ref-swap-list"\n'
' :sx-swap "afterbegin"\n'
' "afterbegin")'
),
"handler": (
'(defhandler ref-swap-item (&key)\n'
' (let ((now (format-time (now) "%H:%M:%S")))\n'
' (div :class "text-sm text-violet-700"\n'
' "New item (" now ")")))'
),
},
"sx-swap-oob": {
"description": (
"Out-of-band swap — updates elements elsewhere in the DOM by ID, "
"outside the normal target. The server includes extra elements in "
"the response with sx-swap-oob attributes, and they are swapped "
"into matching elements in the page."
),
"demo": "ref-oob-demo",
"example": (
'(button :sx-get "/(geography.(hypermedia.(reference.(api.oob))))"\n'
' :sx-target "#ref-oob-main"\n'
' :sx-swap "innerHTML"\n'
' "Update both boxes")'
),
"handler": (
'(defhandler ref-oob (&key)\n'
' (let ((now (format-time (now) "%H:%M:%S")))\n'
' (<>\n'
' (span "Main updated at " now)\n'
' (div :id "ref-oob-side"\n'
' :sx-swap-oob "innerHTML"\n'
' (span "OOB updated at " now)))))'
),
},
"sx-select": {
"description": (
"CSS selector to pick a fragment from the response HTML. "
"Only the matching element is swapped into the target. "
"Useful for extracting part of a full-page response."
),
"demo": "ref-select-demo",
"example": (
'(button :sx-get "/(geography.(hypermedia.(reference.(api.select-page))))"\n'
' :sx-target "#ref-select-result"\n'
' :sx-select "#the-content"\n'
' :sx-swap "innerHTML"\n'
' "Load (selecting #the-content)")'
),
"handler": (
'(defhandler ref-select-page (&key)\n'
' (let ((now (format-time (now) "%H:%M:%S")))\n'
' (<>\n'
' (div :id "the-header" (h3 "Page header — not selected"))\n'
' (div :id "the-content"\n'
' (span "Selected fragment. Time: " now))\n'
' (div :id "the-footer" (p "Page footer — not selected")))))'
),
},
"sx-confirm": {
"description": (
"Shows a browser confirmation dialog before issuing the request. "
"The request is cancelled if the user clicks Cancel. "
"The value is the message shown in the dialog."
),
"demo": "ref-confirm-demo",
"example": (
'(button :sx-delete "/(geography.(hypermedia.(reference.(api.(item.confirm)))))"\n'
' :sx-target "#ref-confirm-item"\n'
' :sx-swap "delete"\n'
' :sx-confirm "Are you sure you want to delete this file?"\n'
' "Delete")'
),
},
"sx-push-url": {
"description": (
'Push the request URL into the browser location bar, enabling '
'back/forward navigation. Set to "true" to push the request URL, '
'or provide a custom URL string.'
),
"demo": "ref-pushurl-demo",
"example": (
'(a :href "/(geography.(hypermedia.(reference-detail.attributes.sx-get)))"\n'
' :sx-get "/(geography.(hypermedia.(reference-detail.attributes.sx-get)))"\n'
' :sx-target "#main-panel"\n'
' :sx-select "#main-panel"\n'
' :sx-swap "outerHTML"\n'
' :sx-push-url "true"\n'
' "sx-get page")'
),
},
"sx-sync": {
"description": (
"Controls synchronization of concurrent requests from the same element. "
'Strategies: "drop" (ignore new while in-flight), '
'"replace" (abort in-flight, send new), '
'"queue" (queue and send after current completes).'
),
"demo": "ref-sync-demo",
"example": (
'(input :type "text" :name "q"\n'
' :placeholder "Type quickly..."\n'
' :sx-get "/(geography.(hypermedia.(reference.(api.slow-echo))))"\n'
' :sx-trigger "input changed delay:100ms"\n'
' :sx-sync "replace"\n'
' :sx-target "#ref-sync-result"\n'
' :sx-swap "innerHTML")'
),
"handler": (
'(defhandler ref-slow-echo (&key)\n'
' (sleep 800)\n'
' (let ((q (or (request-arg "q") "")))\n'
' (span "Echo: " (strong q))))'
),
},
"sx-encoding": {
"description": (
"Sets the encoding type for the request body. "
'Use "multipart/form-data" for file uploads. '
"Defaults to application/x-www-form-urlencoded for forms."
),
"demo": "ref-encoding-demo",
"example": (
'(form :sx-post "/(geography.(hypermedia.(reference.(api.upload-name))))"\n'
' :sx-encoding "multipart/form-data"\n'
' :sx-target "#ref-encoding-result"\n'
' :sx-swap "innerHTML"\n'
' (input :type "file" :name "file")\n'
' (button :type "submit" "Upload"))'
),
"handler": (
'(defhandler ref-upload-name (&key)\n'
' (let ((name (or (file-name "file") "(no file)")))\n'
' (span "Received: " (strong name))))'
),
},
"sx-headers": {
"description": (
"Adds custom headers to the request as a JSON object string. "
"Useful for passing metadata like API keys or content types."
),
"demo": "ref-headers-demo",
"example": (
'(button :sx-get "/(geography.(hypermedia.(reference.(api.echo-headers))))"\n'
' :sx-headers \'{"X-Custom-Token": "abc123", "X-Request-Source": "demo"}\'\n'
' :sx-target "#ref-headers-result"\n'
' :sx-swap "innerHTML"\n'
' "Send with custom headers")'
),
"handler": (
'(defhandler ref-echo-headers (&key)\n'
' (let ((headers (request-headers :prefix "X-")))\n'
' (if (empty? headers)\n'
' (span "No custom headers received.")\n'
' (ul (map (fn (h)\n'
' (li (strong (first h)) ": " (last h)))\n'
' headers)))))'
),
},
"sx-include": {
"description": (
"Include values from additional elements in the request. "
"Takes a CSS selector. The matched element's form values "
"(inputs, selects, textareas) are added to the request."
),
"demo": "ref-include-demo",
"example": (
'(select :id "ref-inc-cat" :name "category"\n'
' (option :value "all" "All")\n'
' (option :value "books" "Books")\n'
' (option :value "tools" "Tools"))\n'
'\n'
'(button :sx-get "/(geography.(hypermedia.(reference.(api.echo-vals))))"\n'
' :sx-include "#ref-inc-cat"\n'
' :sx-target "#ref-include-result"\n'
' :sx-swap "innerHTML"\n'
' "Filter")'
),
"handler": (
'(defhandler ref-echo-vals (&key)\n'
' (let ((vals (request-args)))\n'
' (if (empty? vals)\n'
' (span "No values received.")\n'
' (ul (map (fn (v)\n'
' (li (strong (first v)) ": " (last v)))\n'
' vals)))))'
),
},
"sx-vals": {
"description": (
"Adds extra values to the request as a JSON object string. "
"These are merged with any form values. "
"Useful for passing additional data without hidden inputs."
),
"demo": "ref-vals-demo",
"example": (
'(button :sx-post "/(geography.(hypermedia.(reference.(api.echo-vals))))"\n'
' :sx-vals \'{"source": "demo", "page": "3"}\'\n'
' :sx-target "#ref-vals-result"\n'
' :sx-swap "innerHTML"\n'
' "Send with extra values")'
),
},
"sx-media": {
"description": (
"Only enables the sx attributes on this element when the given "
"CSS media query matches. When the media query does not match, "
"the element behaves as a normal HTML element."
),
"demo": "ref-media-demo",
"example": (
'(a :href "/(geography.(hypermedia.(reference-detail.attributes.sx-get)))"\n'
' :sx-get "/(geography.(hypermedia.(reference-detail.attributes.sx-get)))"\n'
' :sx-target "#main-panel"\n'
' :sx-select "#main-panel"\n'
' :sx-swap "outerHTML"\n'
' :sx-push-url "true"\n'
' :sx-media "(min-width: 768px)"\n'
' "sx navigation (desktop only)")'
),
},
"sx-disable": {
"description": (
"Disables sx processing on this element and all its children. "
"The element renders as normal HTML without any sx behavior. "
"Useful for opting out of sx in specific subtrees."
),
"demo": "ref-disable-demo",
"example": (
';; Left box: sx works normally\n'
';; Right box: sx-disable prevents any sx behavior\n'
'(div :sx-disable "true"\n'
' (button :sx-get "/(geography.(hypermedia.(reference.(api.time))))"\n'
' :sx-target "#ref-dis-b"\n'
' :sx-swap "innerHTML"\n'
' "Load")\n'
' ;; This button will NOT fire an sx request\n'
' )'
),
},
"sx-on:*": {
"description": (
"Inline event handler — attaches JavaScript to a DOM event. "
'The * is replaced by the event name (e.g. sx-on:click, sx-on:keydown). '
"The handler code runs as inline JavaScript with 'this' bound to the element."
),
"demo": "ref-on-demo",
"example": (
'(button\n'
' :sx-on:click "document.getElementById(\'ref-on-result\')\n'
' .textContent = \'Clicked at \' + new Date()\n'
' .toLocaleTimeString()"\n'
' "Click me")'
),
},
# --- Unique to sx ---
"sx-retry": {
"description": (
"Enables exponential backoff retry on request failure. "
'Set to "true" for default retry behavior (3 attempts, 1s/2s/4s delays) '
"or provide a custom retry count."
),
"demo": "ref-retry-demo",
"example": (
'(button :sx-get "/(geography.(hypermedia.(reference.(api.flaky))))"\n'
' :sx-target "#ref-retry-result"\n'
' :sx-swap "innerHTML"\n'
' :sx-retry "true"\n'
' "Call flaky endpoint")'
),
"handler": (
'(defhandler ref-flaky (&key)\n'
' (let ((n (inc-counter "ref-flaky")))\n'
' (if (!= (mod n 3) 0)\n'
' (error 503)\n'
' (span "Success on attempt " n "!"))))'
),
},
"data-sx": {
"description": (
"Client-side rendering — evaluates the s-expression source in this "
"attribute and renders the result into the element. No server request "
"is made. Useful for purely client-side UI and interactive components."
),
"demo": "ref-data-sx-demo",
"example": (
'(div :data-sx "(div :class \\"p-3 bg-violet-50 rounded\\"\n'
' (h3 :class \\"font-semibold\\" \\"Client-rendered\\")\n'
' (p \\"Evaluated in the browser.\\")")'
),
},
"data-sx-env": {
"description": (
"Provides environment variables as a JSON object for data-sx rendering. "
"These values are available as variables in the s-expression."
),
"demo": "ref-data-sx-env-demo",
"example": (
'(div\n'
' :data-sx "(div (h3 title) (p message))"\n'
' :data-sx-env \'{"title": "Dynamic", "message": "From env"}\')'
),
},
# --- New attributes ---
"sx-boost": {
"description": (
"Progressively enhance all descendant links and forms with AJAX navigation. "
"Links become sx-get requests with pushState, forms become sx-post/sx-get requests. "
"No explicit sx-* attributes needed on each link or form — just place sx-boost on a container. "
'The attribute value can be a CSS selector (e.g. sx-boost="#main-panel") to set '
"the default swap target for all boosted descendants. If set to \"true\", "
"each link/form must specify its own sx-target. "
"Pure pages (no server data dependencies) are rendered client-side without a server request."
),
"demo": "ref-boost-demo",
"example": (
';; Boost with configurable target\n'
'(nav :sx-boost "#main-panel"\n'
' (a :href "/(language.(doc.introduction))" "Introduction")\n'
' (a :href "/(language.(doc.components))" "Components")\n'
' (a :href "/(language.(doc.evaluator))" "Evaluator"))\n'
'\n'
';; All links swap into #main-panel automatically.\n'
';; Pure pages render client-side (no server request).'
),
},
"sx-preload": {
"description": (
"Preload the response in the background when the user hovers over or focuses "
"an element with sx-get. When they click, the cached response is used instantly "
"instead of making a new request. Cache entries expire after 30 seconds. "
'Values: "mousedown" (default, preloads on mousedown) or '
'"mouseover" (preloads earlier on hover with 100ms debounce).'
),
"demo": "ref-preload-demo",
"example": (
'(button :sx-get "/(geography.(hypermedia.(reference.(api.time))))"\n'
' :sx-target "#ref-preload-result"\n'
' :sx-swap "innerHTML"\n'
' :sx-preload "mouseover"\n'
' "Hover then click (preloaded)")'
),
"handler": (
'(defhandler ref-preload-time (&key)\n'
' (let ((now (format-time (now) "%H:%M:%S.%f")))\n'
' (span :class "text-stone-800 text-sm"\n'
' "Preloaded at: " (strong now))))'
),
},
"sx-preserve": {
"description": (
"Preserve an element across morph/swap operations. The element must have an id. "
"During morphing, the element is kept in place with its full DOM state intact — "
"event listeners, scroll position, video playback, user input, and any other state "
"are preserved. The incoming version of the element is discarded."
),
"demo": "ref-preserve-demo",
"example": (
'(div :id "my-player" :sx-preserve "true"\n'
' (video :src "/media/clip.mp4" :controls "true"\n'
' "Video playback is preserved across swaps."))'
),
},
"sx-indicator": {
"description": (
"Specifies a CSS selector for a loading indicator element. "
"The indicator receives the .sx-request class during the request, "
"and the class is removed when the request completes (success or error). "
"Use CSS to show/hide the indicator based on the .sx-request class."
),
"demo": "ref-indicator-demo",
"example": (
'(button :sx-get "/(geography.(hypermedia.(reference.(api.slow-echo))))"\n'
' :sx-target "#ref-indicator-result"\n'
' :sx-swap "innerHTML"\n'
' :sx-indicator "#ref-spinner"\n'
' "Load (slow)")\n'
'\n'
'(span :id "ref-spinner"\n'
' :class "hidden sx-request:inline text-violet-600 text-sm"\n'
' "Loading...")'
),
"handler": (
'(defhandler ref-indicator-slow (&key)\n'
' (sleep 1500)\n'
' (let ((now (format-time (now) "%H:%M:%S")))\n'
' (span "Loaded at " (strong now))))'
),
},
"sx-validate": {
"description": (
"Run browser constraint validation before sending the request. "
"If validation fails, the request is not sent and an sx:validationFailed "
"event is dispatched. Works with standard HTML5 validation attributes "
'(required, pattern, minlength, etc). Set to "true" for built-in validation, '
"or provide a function name for custom validation."
),
"demo": "ref-validate-demo",
"example": (
'(form :sx-post "/(geography.(hypermedia.(reference.(api.greet))))"\n'
' :sx-target "#ref-validate-result"\n'
' :sx-swap "innerHTML"\n'
' :sx-validate "true"\n'
' (input :type "email" :name "email"\n'
' :required "true"\n'
' :placeholder "Enter email (required)")\n'
' (button :type "submit" "Submit"))'
),
"handler": (
'(defhandler ref-validate-greet (&key)\n'
' (let ((email (or (form-data "email") "none")))\n'
' (span "Validated: " (strong email))))'
),
},
"sx-ignore": {
"description": (
"During morph/swap, this element and its subtree are completely skipped — "
"no attribute updates, no child reconciliation, no removal. "
"Unlike sx-preserve (which requires an id and preserves by identity), "
"sx-ignore works positionally and means 'don\\'t touch this subtree at all.'"
),
"demo": "ref-ignore-demo",
"example": (
'(div :sx-ignore "true"\n'
' (p "This content is never updated by morph/swap.")\n'
' (input :type "text" :placeholder "Type here — preserved"))'
),
},
"sx-optimistic": {
"description": (
"Apply a client-side preview of the expected result immediately, "
"then reconcile when the server responds. On error, the original state "
'is restored. Values: "remove" (hide the target), '
'"add-class:<name>" (add a CSS class), "disable" (disable the element).'
),
"demo": "ref-optimistic-demo",
"example": (
'(button :sx-delete "/(geography.(hypermedia.(reference.(api.(item.opt1)))))"\n'
' :sx-target "#ref-opt-item"\n'
' :sx-swap "delete"\n'
' :sx-optimistic "remove"\n'
' "Delete (optimistic)")'
),
"handler": (
'(defhandler ref-optimistic-delete (&key)\n'
' (sleep 800)\n'
' "")'
),
},
# --- 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 "/(geography.(hypermedia.(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 "/(geography.(hypermedia.(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 "/(geography.(hypermedia.(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 "/(geography.(hypermedia.(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 "/(geography.(hypermedia.(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..."))'
),
},
}