Add sx-preserve/sx-ignore (morph skip), sx-indicator (loading element), sx-validate (form validation), sx-boost (progressive enhancement), sx-preload (hover prefetch with 30s cache), and sx-optimistic (instant UI preview with rollback). Move all from HTMX_MISSING_ATTRS to SX_UNIQUE_ATTRS with full ATTR_DETAILS docs and reference.sx demos. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
859 lines
36 KiB
Python
859 lines
36 KiB
Python
"""Documentation content for the sx docs site.
|
|
|
|
All page content as Python data structures, consumed by sx_components.py
|
|
to build s-expression page trees.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Navigation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
DOCS_NAV = [
|
|
("Introduction", "/docs/introduction"),
|
|
("Getting Started", "/docs/getting-started"),
|
|
("Components", "/docs/components"),
|
|
("Evaluator", "/docs/evaluator"),
|
|
("Primitives", "/docs/primitives"),
|
|
("CSS", "/docs/css"),
|
|
("Server Rendering", "/docs/server-rendering"),
|
|
]
|
|
|
|
REFERENCE_NAV = [
|
|
("Attributes", "/reference/attributes"),
|
|
("Headers", "/reference/headers"),
|
|
("Events", "/reference/events"),
|
|
("JS API", "/reference/js-api"),
|
|
]
|
|
|
|
PROTOCOLS_NAV = [
|
|
("Wire Format", "/protocols/wire-format"),
|
|
("Fragments", "/protocols/fragments"),
|
|
("Resolver I/O", "/protocols/resolver-io"),
|
|
("Internal Services", "/protocols/internal-services"),
|
|
("ActivityPub", "/protocols/activitypub"),
|
|
("Future", "/protocols/future"),
|
|
]
|
|
|
|
EXAMPLES_NAV = [
|
|
("Click to Load", "/examples/click-to-load"),
|
|
("Form Submission", "/examples/form-submission"),
|
|
("Polling", "/examples/polling"),
|
|
("Delete Row", "/examples/delete-row"),
|
|
("Inline Edit", "/examples/inline-edit"),
|
|
("OOB Swaps", "/examples/oob-swaps"),
|
|
("Lazy Loading", "/examples/lazy-loading"),
|
|
("Infinite Scroll", "/examples/infinite-scroll"),
|
|
("Progress Bar", "/examples/progress-bar"),
|
|
("Active Search", "/examples/active-search"),
|
|
("Inline Validation", "/examples/inline-validation"),
|
|
("Value Select", "/examples/value-select"),
|
|
("Reset on Submit", "/examples/reset-on-submit"),
|
|
("Edit Row", "/examples/edit-row"),
|
|
("Bulk Update", "/examples/bulk-update"),
|
|
("Swap Positions", "/examples/swap-positions"),
|
|
("Select Filter", "/examples/select-filter"),
|
|
("Tabs", "/examples/tabs"),
|
|
("Animations", "/examples/animations"),
|
|
("Dialogs", "/examples/dialogs"),
|
|
("Keyboard Shortcuts", "/examples/keyboard-shortcuts"),
|
|
("PUT / PATCH", "/examples/put-patch"),
|
|
("JSON Encoding", "/examples/json-encoding"),
|
|
("Vals & Headers", "/examples/vals-and-headers"),
|
|
("Loading States", "/examples/loading-states"),
|
|
("Request Abort", "/examples/sync-replace"),
|
|
("Retry", "/examples/retry"),
|
|
]
|
|
|
|
ESSAYS_NAV = [
|
|
("sx sucks", "/essays/sx-sucks"),
|
|
("Why S-Expressions", "/essays/why-sexps"),
|
|
("The htmx/React Hybrid", "/essays/htmx-react-hybrid"),
|
|
("On-Demand CSS", "/essays/on-demand-css"),
|
|
("Client Reactivity", "/essays/client-reactivity"),
|
|
("SX Native", "/essays/sx-native"),
|
|
("The SX Manifesto", "/essays/sx-manifesto"),
|
|
("Tail-Call Optimization", "/essays/tail-call-optimization"),
|
|
("Continuations", "/essays/continuations"),
|
|
]
|
|
|
|
MAIN_NAV = [
|
|
("Docs", "/docs/introduction"),
|
|
("Reference", "/reference/"),
|
|
("Protocols", "/protocols/wire-format"),
|
|
("Examples", "/examples/click-to-load"),
|
|
("Essays", "/essays/sx-sucks"),
|
|
]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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_UNIQUE_ATTRS = [
|
|
("sx-retry", "Exponential backoff retry on request failure", True),
|
|
("sx-boost", "Progressively enhance all links and forms in a container with AJAX navigation", 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),
|
|
("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"),
|
|
]
|
|
|
|
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"),
|
|
]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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:afterSettle", "Fired after the DOM has settled (scripts executed, etc)."),
|
|
("sx:responseError", "Fired on HTTP error responses (4xx, 5xx)."),
|
|
("sx:sendError", "Fired when the request fails to send (network error)."),
|
|
]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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"),
|
|
]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Primitives
|
|
# ---------------------------------------------------------------------------
|
|
|
|
PRIMITIVES = {
|
|
"Arithmetic": ["+", "-", "*", "/", "mod", "sqrt", "pow", "abs", "floor", "ceil", "round", "min", "max"],
|
|
"Comparison": ["=", "!=", "<", ">", "<=", ">="],
|
|
"Logic": ["not", "and", "or"],
|
|
"String": ["str", "upper", "lower", "trim", "split", "join", "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: 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"}\')'
|
|
),
|
|
},
|
|
|
|
# --- 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."
|
|
),
|
|
"demo": "ref-boost-demo",
|
|
"example": (
|
|
'(nav :sx-boost "true"\n'
|
|
' (a :href "/docs/introduction" "Introduction")\n'
|
|
' (a :href "/docs/components" "Components")\n'
|
|
' (a :href "/docs/evaluator" "Evaluator"))'
|
|
),
|
|
},
|
|
"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 "/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 "/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 "/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 "/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'
|
|
' "")'
|
|
),
|
|
},
|
|
}
|