Web UI
Server-driven hypermedia: render SXML views, pushui-*updates to the browser, and wire interactions with declarativedata-sg-*attributes. The server is the source of truth; the bundled client applies the DOM changes.
(import (sigil web ui))The same ui-* update vocabulary feeds both a broadcast (every client) and a per-request ui-response (the acting client). The wire events are sigil:*; the client is served by with-sigil-ui (or sigil-web-ui-handler).
Page Responses
http-response/page
Respond with a complete HTML page from a title and a body. The default template supplies <head> (charset, viewport, <title>) and auto-injects the client script, so a page is script-wired with zero head plumbing.
(http-response/page "To-Do"
`(div (@ (class "app")) (h1 "To-Do") ,(todo-list)))
;; Custom status, or a custom shell (a (title body) -> full-page-sxml function):
(http-response/page "Not found" '(p "No such item.") status: 404)
(http-response/page "Docs" body template: my-template)http-response/sxml
The escape hatch: serialize a full SXML page (you bring the whole <html>) to an HTML document. Prepends <!DOCTYPE html>, sets text/html.
(http-response/sxml
`(html (head (title "Hi")) (body (h1 "Hi"))))Because you bring your own <html>, the client script is not auto-injected here (only http-response/page injects it). Add it yourself, either by pointing a <script> at the route with-sigil-ui serves, or by inlining the bundle with sigil-web-ui-script:
(http-response/sxml
`(html
(head (title "Hi")
;; served by with-sigil-ui:
(script (@ (src "/js/sigil-web-ui.js"))))
(body (h1 "Hi"))))
;; ...or inline the bundle, no route needed:
(http-response/sxml
`(html
(head (title "Hi")
(script ,(sigil-web-ui-script)))
(body (h1 "Hi"))))with-sigil-ui
Middleware that serves the client script at /js/sigil-web-ui.js, so you never register that route by hand. Pairs with http-response/page injecting the tag.
(-> (routes (router …))
(with-sigil-ui)
(with-logging)
(with-not-found))UI Updates
ui-update and friends build server-driven UI updates. Feed them to a broadcast (see (sigil web live)) or bundle them in a ui-response for the acting request.
ui-update
Update a target element with HTML content. content: accepts a string, a single SXML node, or a list of SXML nodes (joined automatically).
(ui-update target: "#messages" mode: "append"
content: `(div (@ (class "msg")) "Hello!"))mode: selects how the content lands (default "morph"):
| mode | effect |
|---|---|
morph | Morph the target element itself via idiomorph, in place (default). Content is a full element whose id matches the target — the common case. |
morph-inner | Morph the target's inner content via idiomorph. |
inner | Replace the target's innerHTML (no diffing). |
replace | Replace the target's outerHTML (no diffing). |
append | Append to the target's children. |
prepend | Prepend to the target's children. |
before / after | Insert before / after the target. |
remove | Remove the target element. |
ui-remove
Remove the target element. Shorthand for (ui-update target: … mode: "remove").
(ui-remove target: "#item-3")ui-class
Add or remove CSS classes on a target.
(ui-class target: "#panel" add: "visible active")
(ui-class target: "#btn" remove: "loading" add: "done")ui-eval
Run a limited client command (things HTML state can't express).
(ui-eval cmd: "focus" target: "#input")
(ui-eval cmd: "scroll-to" target: "#chat" position: "bottom")ui-redirect / ui-reload
(ui-redirect url: "/login")
(ui-reload) ; re-fetch the page and morph <body>
(ui-reload target: "#sidebar") ; morph one element from a re-fetchui-flash
Push a flash message (append into a container, default #sg-flash-container). Pass target: to append into a different container.
(ui-flash type: 'success message: "Saved!" remove-after: 3000)
(ui-flash type: 'error message: "Nope" target: "#form-errors")Also available: ui-css-reload, ui-js, and sg-flash-message (the flash SXML without pushing it).
Responding to an Action
ui-response
The response an action handler returns: a batch of ui-* updates the client applies. Same vocabulary you broadcast, so a handler reads as "the mutation is what everyone sees; this response is what the acting user sees." Call it with no updates for an empty acknowledgement (the common case when a broadcast already did the visible work). The optional status: is for HTTP hygiene only — it does not drive the UI; surface an error by targeting an error element.
(ui-response) ; empty ack
(ui-response (ui-update target: "#add-form" content: (add-form)))
(ui-response status: 422
(ui-update target: "#errors" content: (errors msgs)))ui-routes (see the routing docs) lets a handler skip the empty ui-response and just end with its mutation.
UI Components
sg-* helpers emit HTML with data-sg-* attributes; the client fetches the route and applies the result — you never write the fetch. Action helpers accept an HTTP-method symbol and default to POST.
sg-button
(sg-button "Done" action: "/items/1/toggle") ; POST by default
(sg-button "Load" action: "/data" method: GET)sg-link
A link that fetches the action: route on click and applies the response, instead of navigating. When target: is set the client cancels the normal navigation, GETs action:, and applies what comes back: a plain HTML fragment is morphed into target:, while a ui-response batch is applied as its ui-* updates (those name their own targets, so target: is just the fragment fallback). With no target: it stays an ordinary link, so non-JS clients still navigate. Children are a string or a list of nodes.
(sg-link "Edit" action: "/items/1/edit" target: "#item-1")sg-form
(sg-form
(list (sg-text-field name: "text" placeholder: "What needs doing?")
(sg-submit-button label: "Add"))
action: "/items" id: "add-form") ; POST by defaultForm Fields
sg-text-field, sg-email-field, sg-password-field, sg-hidden-field, sg-textarea-field, sg-select-field, sg-checkbox-field, and the underlying sg-input-field. Non-string values are coerced (numbers stringified, #f omits the attribute); label:/error: wrap the field in a <div> with a <label>.
(sg-text-field name: "title" value: some-value placeholder: "Title" autofocus: #t)
(sg-select-field name: "genre" options: '(("rock" . "Rock") ("jazz" . "Jazz")) value: "jazz")
(sg-submit-button label: "Save")Widgets
sg-flash-message type: message: (keys: remove-after:)— arole="alert"box.sg-loading-indicator— a spinner element toggled during actions.sg-modal/sg-modal-trigger/sg-modal-close—<dialog>-based modals.sg-data-table columns: rows: (keys: row-actions:)+sg-table-action.sg-paginator current-page: total-pages: base-url: (keys: target:).sg-sse children (keys: url:)— an SSE-subscribed container.
Common Patterns
A form that resets itself and broadcasts a new row
(define (create-handler req)
(let ((text (assoc-ref 'text (parse-form-data req))))
(when (and text (> (string-length text) 0))
(broadcast-send hub ; everyone: append the row
(ui-update target: "#list" mode: "append" content: (item-row (add-item! text)))))
(ui-response (ui-update target: "#add-form" content: (add-form))))) ; you: reset the formA full page, wired up, in one handler
(define (home-handler req)
(http-response/page "To-Do"
`(div (h1 "To-Do") ,(add-form) (ul (@ (id "list")) ,@(map item-row (all-items))))))
(define app
(-> (routes (router (route method: GET pattern: "/" handler: home-handler)))
(with-sigil-ui) (with-logging) (with-not-found)))For live lists and streams built on this vocabulary, see (sigil web live) (live.md).