(sigil web ui)
(sigil web ui) - Server-Driven Hypermedia Library
Enables dynamic, real-time web interfaces driven entirely from Sigil. The server is the source of truth; the client is a thin rendering layer.
Core concepts:
- Server-driven UI updates (ui-update, ui-remove, ui-eval, ui-redirect, ...): one vocabulary for both live broadcasts and per-request responses
ui-responsedelivers a batch of those updates to the acting request- Declarative HTML attributes (data-sg-*) for client interactions
Example:
(import (sigil web ui)
(sigil http))
;; Broadcast a UI update to every connected client
(broadcast-send hub (ui-update target: "#chat" mode: "append"
content: `(div (@ (class "msg")) "Hello!")))
;; Respond to just the acting request with the same vocabulary
(ui-response (ui-update target: "#results" mode: "inner"
content: (map render-item items)))Exports
sxml-list->htmlprocedureRender a list of SXML nodes to a single HTML string.
Saves you from hand-rolling (apply string-append (map sxml->xml xs)) whenever you have a collection of nodes (e.g. a list of rendered rows) that needs to become one fragment.
Example:
(sxml-list->html (map render-row items))ui-updateprocedureUpdate a target element with HTML content.
The workhorse UI update: it replaces, morphs, or inserts content at a target element. content: accepts a string, a single SXML node, or a list of SXML nodes (joined automatically). mode: selects how the content lands; the default "morph" diffs the target in place via idiomorph (preserving focus/scroll).
Parameters: target: CSS selector for the target element (e.g., "#chat") content: string, SXML node, or list of SXML nodes mode: how the content lands (default: "morph") settle: Milliseconds to wait after applying (for CSS transitions)
Modes: "morph" - Morph the target ELEMENT itself via idiomorph, in place (default). The content is a full element whose id matches the target — the common case. "morph-inner" - Morph the target's INNER content via idiomorph (the content is the target's new children). "replace" - Replace target's outerHTML (no diffing) "inner" - Replace target's innerHTML (no diffing) "append" - Append to target's children "prepend" - Prepend to target's children "before" - Insert before target "after" - Insert after target "remove" - Remove target element (no content needed)
Example:
(ui-update target: "#messages" mode: "append"
content: `(div (@ (class "msg")) "Hello!"))ui-removeprocedureFormat a UI remove update.
Removes the target element from the DOM. Shorthand for (ui-update target: TARGET mode: "remove").
Example:
(ui-remove target: "#notification")ui-classprocedureFormat a UI class update.
Adds or removes CSS classes on the target element.
Parameters: target: CSS selector for the target element add: Space-separated class names to add remove: Space-separated class names to remove
Example:
(ui-class target: "#panel" add: "visible active")
(ui-class target: "#btn" remove: "loading" add: "done")ui-evalprocedureFormat a UI eval update.
Executes a limited command on the client. Only use for things that can't be expressed as HTML state (focus, scroll).
Commands: "focus" - Focus the target element "scroll-to" - Scroll target into view (or to bottom with position: "bottom")
Example:
(ui-eval cmd: "focus" target: "#input")
(ui-eval cmd: "scroll-to" target: "#chat" position: "bottom")ui-redirectprocedureFormat a UI redirect update.
Redirects the browser to a new URL.
Example:
(ui-redirect url: "/login")ui-reloadprocedureFormat a UI reload update.
Tells the browser to re-fetch the current page and morph the result into the DOM using Idiomorph. Preserves scroll position, form state, and focus. Useful for live-coding: redefine a view function, then send a reload to see the update immediately.
Parameters: target: CSS selector for the element to reload (default: "body")
Example:
(ui-reload) ; reload full body
(ui-reload target: "#sidebar") ; reload specific elementui-css-reloadprocedureFormat a UI css-reload update.
Reloads CSS stylesheets in the browser without a full page reload. If href: is provided, only stylesheets matching that substring are reloaded. Otherwise all stylesheets are reloaded.
Example:
(ui-css-reload) ; reload all stylesheets
(ui-css-reload href: "styles.css") ; reload specific stylesheetui-jsprocedureFormat a UI js update.
Executes JavaScript code in all connected browsers.
Example:
(ui-js code: "console.log('hello')")sigil-ui-headersprocedureBuild Sigil-UI headers for merge operations.
Returns a dict of headers to include in an HTTP response.
Example:
(http-response
status: 200
headers: (dict-merge (sigil-ui-headers target: "#results" mode: "append")
(dict content-type: "text/html"))
body: "<div>New item</div>")sigil-ui-responseprocedureDEPRECATED. Prefer ui-response with a ui-update: (ui-response (ui-update target: "#chat" mode: "append" content: ...))
Create a single-fragment HTML response carried via Sigil-UI-Merge-* headers. This is the pre-unification actor-response mechanism, kept as a shim so existing consumers keep working; new code should return ui-response batches (the same ui-* vocabulary you broadcast).
Example:
(sigil-ui-response target: "#chat" mode: "append"
content: `(div (@ (class "msg")) "Hello!"))sigil-ui-redirectprocedureCreate a redirect response using Sigil-UI header.
For JavaScript-enhanced clients, this triggers a client-side redirect. For non-JS clients, falls back to standard HTTP redirect.
Example:
(sigil-ui-redirect url: "/dashboard")ui-responseprocedureRespond to the acting request with a batch of UI updates.
This is THE response an action handler returns: any number of ui-* updates, applied by the client on arrival. It's the same vocabulary you broadcast to every client, so a handler reads as "the mutation is what everyone sees; this response is what the acting user sees."
Call with no updates to return an empty response — the common case when the visible change already went out over a broadcast and the actor needs nothing extra.
The optional status: keyword sets the HTTP status (default 200), purely for transport hygiene (logs, proxies, API clients). The status does not drive the UI: the client applies the updates regardless. Surface an error to the user like any other update, by targeting an error element.
Example:
(ui-response) ; broadcast handled it
(ui-response (ui-update target: "#add-form" content: (add-form)))
(ui-response status: 422
(ui-update target: "#errors" content: (errors msgs)))ui-routesprocedureCombine hypermedia route handlers, defaulting their responses.
Like routes (first non-#f handler wins), but a handler that returns a value which isn't an HTTP response is taken to mean "handled, nothing extra for the acting user" and becomes an empty (ui-response). So an action handler can just end with its live-collection mutation:
(define (toggle-handler req)
(live-update! todos (item-id req)
(lambda (i) (dict-set i done?: (not (dict-ref i done?:))))))Gather your hypermedia routes under ui-routes and keep other kinds (a JSON API, a webhook) under plain routes/router, which never coerce. #f still falls through, and explicit responses pass through untouched, so ui-routes composes inside routes just like router:
(routes
(ui-routes (router …ui…) (live-routes todos)) ; forgiving
(router …api…)) ; untouchedhttp-response/sxmlprocedureSerialize a full SXML page to an HTML-document response.
The low-level escape hatch: you bring the whole page (including <head>), and this prepends the <!DOCTYPE html>, serializes with sxml->html, and sets Content-Type: text/html. Use http-response/page for the common case where you only have a title and body.
Example:
(http-response/sxml
`(html (head (title "Hi")) (body (h1 "Hi"))))default-page-templateprocedureThe default page template used by http-response/page.
A minimal HTML shell: charset + viewport meta, a <title> from title, the Sigil Web UI client scripts (so data-sg-* actions and live updates work with zero head plumbing), and body spliced into <body>. body may be a single SXML node or a list of nodes.
Write your own (title body) -> full-page-sxml function and pass it as template: to http-response/page to customize the shell.
http-response/pageprocedureRespond with a full HTML page from a title and body.
The streamlined common case. Wraps body in template (default: default-page-template, a working script-wired shell) and returns an HTML-document response. A beginner writes (http-response/page "To-Do" body) and gets a complete page with head, viewport, and the UI client wired up. Override the shell with template: (a (title body) -> sxml function), or drop to http-response/sxml to supply the whole page.
body may be a single SXML node or a list of nodes.
Example:
(http-response/page "To-Do" (todo-page items))
(http-response/page "Not found" '(p "No such item.") status: 404)optional-attrsprocedureBuild an SXML attribute list, filtering out #f values.
Takes alternating name/value pairs and returns a list of (name value) entries suitable for splicing into SXML (@...) forms. Pairs where the value is #f are omitted.
Example:
`(div (@ (class "main")
,@(optional-attrs
'id my-id
'data-target target
'disabled (and disabled "disabled"))))sg-buttonprocedureCreate a button that triggers an action.
method: accepts an HTTP-method symbol (POST, GET, ...) and defaults to POST (actions are almost always POST); a "post" string still works.
Example:
(sg-button "Like" action: "/api/like" ; POST by default
loading: "opacity-50")sg-linkprocedureCreate a link that morphs content into a target.
Provides SPA-style navigation while preserving standard link behavior for non-JS clients. Children can be a string or a list of SXML nodes.
Example:
(sg-link '("Introduction") action: "/lesson/1" target: "#content")
(sg-link "Click here" action: "/page")sg-formprocedureCreate a form with action handling.
method: accepts an HTTP-method symbol (POST, GET, ...) and defaults to POST; a "post" string still works.
Example:
(sg-form (list
(sg-email-field name: "email")
(sg-password-field name: "password")
(sg-submit-button "Login"))
action: "/api/login" ; POST by default
target: "#result" error-target: "#errors")sg-sseprocedureCreate an SSE-connected container.
Example:
(sg-sse '((div (@ (id "messages")))) url: "/events/chat" id: "chat")sg-input-fieldprocedureCreate an input field of any type.
Handles label wrapping, error display, and value coercion. Non-string values are auto-coerced: numbers become strings, #f omits the value attribute.
Example:
(sg-input-field type: "text" name: "title" label: "Title"
value: some-value required: #t)sg-text-fieldprocedureCreate a text input field.
sg-email-fieldprocedureCreate an email input field.
sg-password-fieldprocedureCreate a password input field.
sg-textarea-fieldprocedureCreate a textarea field.
Value is auto-coerced: numbers become strings, #f becomes "".
sg-select-fieldprocedureCreate a select dropdown field.
Options can be:
- List of strings: ("a" "b" "c")
- List of (value . label) pairs: (("a" . "Option A") ("b" . "Option B"))
Value is auto-coerced for comparison: numbers become strings.
sg-checkbox-fieldprocedureCreate a checkbox field.
sg-submit-buttonprocedureCreate a submit button.
sg-flash-messageprocedureCreate a flash message notification.
Returns a div with role="alert" for accessibility. The type: controls the CSS class (sg-flash-success, sg-flash-error, etc.). When remove-after: is set, the element auto-removes after that many milliseconds.
Example:
(sg-flash-message type: 'success message: "Saved!" remove-after: 3000)
(sg-flash-message type: 'error message: "Failed to save")ui-flashprocedurePush a flash message as a UI update.
Wraps ui-morph with mode "append" to add a flash message to a container element. Defaults to targeting "#sg-flash-container".
Example:
(ui-flash type: 'success message: "Record updated" remove-after: 3000)sg-loading-indicatorprocedureCreate a loading indicator element.
Returns a div with class "sg-loading". When active: is #t, adds the "active" class. Uses aria-hidden for accessibility.
Example:
(sg-loading-indicator id: "spinner")
(sg-loading-indicator id: "spinner" active: #t)sg-modalprocedureCreate a modal dialog using the HTML <dialog> element.
Example:
(sg-modal (list (p "Are you sure?")
(sg-modal-close "Cancel"))
id: "confirm" title: "Confirm")sg-modal-triggerprocedureCreate a button that opens a modal dialog.
Example:
(sg-modal-trigger "Open Settings" target: "#settings-modal")sg-modal-closeprocedureCreate a button that closes the nearest modal dialog.
Example:
(sg-modal-close "Cancel")sg-table-actionprocedureCreate a table action descriptor for use with sg-data-table.
Example:
(sg-table-action "Edit" action: "/users/{id}/edit")
(sg-table-action "Delete" action: "/users/{id}"
method: "delete" confirm: "Delete this user?")sg-data-tableprocedureCreate a data table with headers, rows, and optional actions.
Example:
(sg-data-table columns: '((name "Name") (email "Email"))
rows: users
row-actions: (list
(sg-table-action "Edit" action: "/users/{id}/edit")
(sg-table-action "Delete" action: "/users/{id}"
method: "delete"
confirm: "Delete?")))sg-paginatorprocedureCreate a pagination navigator.
Renders prev/next links and page numbers. Links use sg-link internally for SSE morphing. Current page is displayed as a <span>. Ellipsis shown when page window doesn't cover full range.
Example:
(sg-paginator current-page: 3 total-pages: 10
base-url: "/users" target: "#user-list"
params: '((sort . "name")))sigil-web-ui-scriptprocedureReturn the Sigil Web UI JavaScript library as a string.
Use this to include the script in static builds or serve it from a custom route.
Example:
(http-response/text 200 (sigil-web-ui-script))sigil-web-ui-handlerprocedureCreate an HTTP handler that serves the Sigil Web UI script.
Example:
(router
(route 'GET "/js/sigil-web-ui.js" (sigil-web-ui-handler))
...)sigil-web-ui-headprocedureGenerate SXML for including Sigil Web UI in a page head.
Parameters: idiomorph-url: URL for idiomorph library (default: CDN) script-url: URL for sigil-web-ui.js (default: /js/sigil-web-ui.js)
Example:
(html
(head
,@(sigil-web-ui-head)
(title "My App"))
(body ...))with-sigil-uiprocedureMiddleware that serves the Sigil Web UI client script.
A chain-style wrapper: it intercepts GET <path> (default /js/sigil-web-ui.js, the URL sigil-web-ui-head points at) and serves the bundled client, passing every other request through. This is the middleware counterpart to http-response/page auto-injecting the script tag: add (with-sigil-ui) to your chain and you never register the JS route by hand.
Example:
(-> (routes (router ...))
(with-sigil-ui)
(with-logging)
(with-not-found))sse-morphvariable(No description)
sse-removevariable(No description)
sse-classvariable(No description)
sse-evalvariable(No description)
sse-redirectvariable(No description)
sse-reloadvariable(No description)
sse-css-reloadvariable(No description)
sse-jsvariable(No description)
sse-flashvariable(No description)
sse-response-batchvariable(No description)