AtlatestRepositorysigil-web
1# Web UI
2
3> Server-driven hypermedia: render SXML views, push `ui-*` updates to the
4> browser, and wire interactions with declarative `data-sg-*` attributes. The
5> server is the source of truth; the bundled client applies the DOM changes.
6
7```scheme
8(import (sigil web ui))
9```
11The same `ui-*` update vocabulary feeds both a broadcast (every client) and a
12per-request `ui-response` (the acting client). The wire events are `sigil:*`;
13the client is served by `with-sigil-ui` (or `sigil-web-ui-handler`).
15## Page Responses
17### http-response/page
19Respond with a complete HTML page from a title and a body. The default template
20supplies `<head>` (charset, viewport, `<title>`) and auto-injects the client
21script, so a page is script-wired with zero head plumbing.
23```scheme
24(http-response/page "To-Do"
25 `(div (@ (class "app")) (h1 "To-Do") ,(todo-list)))
27;; Custom status, or a custom shell (a (title body) -> full-page-sxml function):
28(http-response/page "Not found" '(p "No such item.") status: 404)
29(http-response/page "Docs" body template: my-template)
30```
32### http-response/sxml
34The escape hatch: serialize a full SXML page (you bring the whole `<html>`) to an
35HTML document. Prepends `<!DOCTYPE html>`, sets `text/html`.
37```scheme
38(http-response/sxml
39 `(html (head (title "Hi")) (body (h1 "Hi"))))
40```
42Because you bring your own `<html>`, the client script is **not** auto-injected
43here (only `http-response/page` injects it). Add it yourself, either by pointing
44a `<script>` at the route `with-sigil-ui` serves, or by inlining the bundle with
45`sigil-web-ui-script`:
47```scheme
48(http-response/sxml
49 `(html
50 (head (title "Hi")
51 ;; served by with-sigil-ui:
52 (script (@ (src "/js/sigil-web-ui.js"))))
53 (body (h1 "Hi"))))
55;; ...or inline the bundle, no route needed:
56(http-response/sxml
57 `(html
58 (head (title "Hi")
59 (script ,(sigil-web-ui-script)))
60 (body (h1 "Hi"))))
61```
63### with-sigil-ui
65Middleware that serves the client script at `/js/sigil-web-ui.js`, so you never
66register that route by hand. Pairs with `http-response/page` injecting the tag.
68```scheme
69(-> (routes (router …))
70 (with-sigil-ui)
71 (with-logging)
72 (with-not-found))
73```
75## UI Updates
77`ui-update` and friends build server-driven UI updates. Feed them to a broadcast
78(see `(sigil web live)`) or bundle them in a `ui-response` for the acting request.
80### ui-update
82Update a target element with HTML content. `content:` accepts a string, a single
83SXML node, or a list of SXML nodes (joined automatically).
85```scheme
86(ui-update target: "#messages" mode: "append"
87 content: `(div (@ (class "msg")) "Hello!"))
88```
90`mode:` selects how the content lands (default `"morph"`):
92| mode | effect |
93|---------------|---------------------------------------------------------------|
94| `morph` | Morph the target **element itself** via idiomorph, in place (default). Content is a full element whose id matches the target — the common case. |
95| `morph-inner` | Morph the target's **inner** content via idiomorph. |
96| `inner` | Replace the target's innerHTML (no diffing). |
97| `replace` | Replace the target's outerHTML (no diffing). |
98| `append` | Append to the target's children. |
99| `prepend` | Prepend to the target's children. |
100| `before` / `after` | Insert before / after the target. |
101| `remove` | Remove the target element. |
103### ui-remove
105Remove the target element. Shorthand for `(ui-update target: … mode: "remove")`.
107```scheme
108(ui-remove target: "#item-3")
109```
111### ui-class
113Add or remove CSS classes on a target.
115```scheme
116(ui-class target: "#panel" add: "visible active")
117(ui-class target: "#btn" remove: "loading" add: "done")
118```
120### ui-eval
122Run a limited client command (things HTML state can't express).
124```scheme
125(ui-eval cmd: "focus" target: "#input")
126(ui-eval cmd: "scroll-to" target: "#chat" position: "bottom")
127```
129### ui-redirect / ui-reload
131```scheme
132(ui-redirect url: "/login")
133(ui-reload) ; re-fetch the page and morph <body>
134(ui-reload target: "#sidebar") ; morph one element from a re-fetch
135```
137### ui-flash
139Push a flash message (append into a container, default `#sg-flash-container`).
140Pass `target:` to append into a different container.
142```scheme
143(ui-flash type: 'success message: "Saved!" remove-after: 3000)
144(ui-flash type: 'error message: "Nope" target: "#form-errors")
145```
147Also available: `ui-css-reload`, `ui-js`, and `sg-flash-message` (the flash SXML
148without pushing it).
150## Responding to an Action
152### ui-response
154The response an action handler returns: a batch of `ui-*` updates the client
155applies. Same vocabulary you broadcast, so a handler reads as "the mutation is
156what everyone sees; this response is what the acting user sees." Call it with no
157updates for an empty acknowledgement (the common case when a broadcast already
158did the visible work). The optional `status:` is for HTTP hygiene only — it does
159not drive the UI; surface an error by targeting an error element.
161```scheme
162(ui-response) ; empty ack
163(ui-response (ui-update target: "#add-form" content: (add-form)))
164(ui-response status: 422
165 (ui-update target: "#errors" content: (errors msgs)))
166```
168`ui-routes` (see the routing docs) lets a handler skip the empty `ui-response`
169and just end with its mutation.
171## UI Components
173`sg-*` helpers emit HTML with `data-sg-*` attributes; the client fetches the
174route and applies the result — you never write the fetch. Action helpers accept
175an HTTP-method **symbol** and default to `POST`.
177### sg-button
179```scheme
180(sg-button "Done" action: "/items/1/toggle") ; POST by default
181(sg-button "Load" action: "/data" method: GET)
182```
184### sg-link
186A link that fetches the `action:` route on click and applies the response,
187instead of navigating. When `target:` is set the client cancels the normal
188navigation, GETs `action:`, and applies what comes back: a plain HTML fragment is
189morphed into `target:`, while a `ui-response` batch is applied as its `ui-*`
190updates (those name their own targets, so `target:` is just the fragment
191fallback). With no `target:` it stays an ordinary link, so non-JS clients still
192navigate. Children are a string or a list of nodes.
194```scheme
195(sg-link "Edit" action: "/items/1/edit" target: "#item-1")
196```
198### sg-form
200```scheme
201(sg-form
202 (list (sg-text-field name: "text" placeholder: "What needs doing?")
203 (sg-submit-button label: "Add"))
204 action: "/items" id: "add-form") ; POST by default
205```
207## Form Fields
209`sg-text-field`, `sg-email-field`, `sg-password-field`, `sg-hidden-field`,
210`sg-textarea-field`, `sg-select-field`, `sg-checkbox-field`, and the underlying
211`sg-input-field`. Non-string values are coerced (numbers stringified, `#f` omits
212the attribute); `label:`/`error:` wrap the field in a `<div>` with a `<label>`.
214```scheme
215(sg-text-field name: "title" value: some-value placeholder: "Title" autofocus: #t)
216(sg-select-field name: "genre" options: '(("rock" . "Rock") ("jazz" . "Jazz")) value: "jazz")
217(sg-submit-button label: "Save")
218```
220## Widgets
222- `sg-flash-message type: message: (keys: remove-after:)` — a `role="alert"` box.
223- `sg-loading-indicator` — a spinner element toggled during actions.
224- `sg-modal` / `sg-modal-trigger` / `sg-modal-close` — `<dialog>`-based modals.
225- `sg-data-table columns: rows: (keys: row-actions:)` + `sg-table-action`.
226- `sg-paginator current-page: total-pages: base-url: (keys: target:)`.
227- `sg-sse children (keys: url:)` — an SSE-subscribed container.
229## Common Patterns
231### A form that resets itself and broadcasts a new row
233```scheme
234(define (create-handler req)
235 (let ((text (assoc-ref 'text (parse-form-data req))))
236 (when (and text (> (string-length text) 0))
237 (broadcast-send hub ; everyone: append the row
238 (ui-update target: "#list" mode: "append" content: (item-row (add-item! text)))))
239 (ui-response (ui-update target: "#add-form" content: (add-form))))) ; you: reset the form
240```
242### A full page, wired up, in one handler
244```scheme
245(define (home-handler req)
246 (http-response/page "To-Do"
247 `(div (h1 "To-Do") ,(add-form) (ul (@ (id "list")) ,@(map item-row (all-items))))))
249(define app
250 (-> (routes (router (route method: GET pattern: "/" handler: home-handler)))
251 (with-sigil-ui) (with-logging) (with-not-found)))
252```
254For live lists and streams built on this vocabulary, see `(sigil web live)`
255(`live.md`).