Add attribute detail pages with live demos for SX reference
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m45s

Per-attribute documentation pages at /reference/attributes/<slug> with:
- Live interactive demos (demo components in reference.sx)
- S-expression source code display
- Server handler code shown as s-expressions (defhandlers in handlers/reference.sx)
- Wire response display via OOB swaps on demo interaction
- Linked attribute names in the reference table

Covers all 20 implemented attributes (sx-get/post/put/delete/patch,
sx-trigger/target/swap/swap-oob/select/confirm/push-url/sync/encoding/
headers/include/vals/media/disable/on:*, sx-retry, data-sx, data-sx-env).

Also adds sx-on:* to BEHAVIOR_ATTRS, updates REFERENCE_NAV to link
/reference/attributes, and makes /reference/ an index page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 17:12:57 +00:00
parent a4377668be
commit 0c9dbd6657
8 changed files with 1331 additions and 8 deletions

View File

@@ -20,7 +20,7 @@ DOCS_NAV = [
]
REFERENCE_NAV = [
("Attributes", "/reference/"),
("Attributes", "/reference/attributes"),
("Headers", "/reference/headers"),
("Events", "/reference/events"),
("JS API", "/reference/js-api"),
@@ -107,6 +107,7 @@ BEHAVIOR_ATTRS = [
("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_UNIQUE_ATTRS = [
@@ -236,3 +237,481 @@ EDIT_ROW_DATA = [
{"id": "3", "name": "Widget C", "price": "12.00", "stock": "305"},
{"id": "4", "name": "Widget D", "price": "45.00", "stock": "67"},
]
# ---------------------------------------------------------------------------
# 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 "/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 "/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 "/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 "/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 "/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 "/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 "/reference/api/time"\n'
' :sx-target "#ref-target-a"\n'
' :sx-swap "innerHTML"\n'
' "Update Box A")\n'
'\n'
'(button :sx-get "/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 "/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 "/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 "/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 "/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 "/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 "/reference/attributes/sx-get"\n'
' :sx-get "/reference/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 "/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 "/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 "/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 "/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 "/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 "/reference/attributes/sx-get"\n'
' :sx-get "/reference/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 "/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 "/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"}\')'
),
},
}