sigildocs

Web UI

Server-driven hypermedia: render SXML views, push ui-* updates to the browser, and wire interactions with declarative data-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"):

modeeffect
morphMorph the target element itself via idiomorph, in place (default). Content is a full element whose id matches the target — the common case.
morph-innerMorph the target's inner content via idiomorph.
innerReplace the target's innerHTML (no diffing).
replaceReplace the target's outerHTML (no diffing).
appendAppend to the target's children.
prependPrepend to the target's children.
before / afterInsert before / after the target.
removeRemove 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-fetch

ui-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 default

Form 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:) — a role="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 form

A 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).