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>
1382 lines
60 KiB
Python
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..."))'
|
|
),
|
|
},
|
|
}
|