AtlatestRepositorysigil-web
1
# Web UI3
> Server-driven hypermedia: render SXML views, push `ui-*` updates to the4
> browser, and wire interactions with declarative `data-sg-*` attributes. The5
> server is the source of truth; the bundled client applies the DOM changes.7
```scheme8
(import (sigil web ui))9
```11
The same `ui-*` update vocabulary feeds both a broadcast (every client) and a12
per-request `ui-response` (the acting client). The wire events are `sigil:*`;13
the client is served by `with-sigil-ui` (or `sigil-web-ui-handler`).15
## Page Responses17
### http-response/page19
Respond with a complete HTML page from a title and a body. The default template20
supplies `<head>` (charset, viewport, `<title>`) and auto-injects the client21
script, so a page is script-wired with zero head plumbing.23
```scheme24
(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/sxml34
The escape hatch: serialize a full SXML page (you bring the whole `<html>`) to an35
HTML document. Prepends `<!DOCTYPE html>`, sets `text/html`.37
```scheme38
(http-response/sxml39
`(html (head (title "Hi")) (body (h1 "Hi"))))40
```42
Because you bring your own `<html>`, the client script is **not** auto-injected43
here (only `http-response/page` injects it). Add it yourself, either by pointing44
a `<script>` at the route `with-sigil-ui` serves, or by inlining the bundle with45
`sigil-web-ui-script`:47
```scheme48
(http-response/sxml49
`(html50
(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/sxml57
`(html58
(head (title "Hi")59
(script ,(sigil-web-ui-script)))60
(body (h1 "Hi"))))61
```63
### with-sigil-ui65
Middleware that serves the client script at `/js/sigil-web-ui.js`, so you never66
register that route by hand. Pairs with `http-response/page` injecting the tag.68
```scheme69
(-> (routes (router …))70
(with-sigil-ui)71
(with-logging)72
(with-not-found))73
```75
## UI Updates77
`ui-update` and friends build server-driven UI updates. Feed them to a broadcast78
(see `(sigil web live)`) or bundle them in a `ui-response` for the acting request.80
### ui-update82
Update a target element with HTML content. `content:` accepts a string, a single83
SXML node, or a list of SXML nodes (joined automatically).85
```scheme86
(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-remove105
Remove the target element. Shorthand for `(ui-update target: … mode: "remove")`.107
```scheme108
(ui-remove target: "#item-3")109
```111
### ui-class113
Add or remove CSS classes on a target.115
```scheme116
(ui-class target: "#panel" add: "visible active")117
(ui-class target: "#btn" remove: "loading" add: "done")118
```120
### ui-eval122
Run a limited client command (things HTML state can't express).124
```scheme125
(ui-eval cmd: "focus" target: "#input")126
(ui-eval cmd: "scroll-to" target: "#chat" position: "bottom")127
```129
### ui-redirect / ui-reload131
```scheme132
(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-fetch135
```137
### ui-flash139
Push a flash message (append into a container, default `#sg-flash-container`).140
Pass `target:` to append into a different container.142
```scheme143
(ui-flash type: 'success message: "Saved!" remove-after: 3000)144
(ui-flash type: 'error message: "Nope" target: "#form-errors")145
```147
Also available: `ui-css-reload`, `ui-js`, and `sg-flash-message` (the flash SXML148
without pushing it).150
## Responding to an Action152
### ui-response154
The response an action handler returns: a batch of `ui-*` updates the client155
applies. Same vocabulary you broadcast, so a handler reads as "the mutation is156
what everyone sees; this response is what the acting user sees." Call it with no157
updates for an empty acknowledgement (the common case when a broadcast already158
did the visible work). The optional `status:` is for HTTP hygiene only — it does159
not drive the UI; surface an error by targeting an error element.161
```scheme162
(ui-response) ; empty ack163
(ui-response (ui-update target: "#add-form" content: (add-form)))164
(ui-response status: 422165
(ui-update target: "#errors" content: (errors msgs)))166
```168
`ui-routes` (see the routing docs) lets a handler skip the empty `ui-response`169
and just end with its mutation.171
## UI Components173
`sg-*` helpers emit HTML with `data-sg-*` attributes; the client fetches the174
route and applies the result — you never write the fetch. Action helpers accept175
an HTTP-method **symbol** and default to `POST`.177
### sg-button179
```scheme180
(sg-button "Done" action: "/items/1/toggle") ; POST by default181
(sg-button "Load" action: "/data" method: GET)182
```184
### sg-link186
A link that fetches the `action:` route on click and applies the response,187
instead of navigating. When `target:` is set the client cancels the normal188
navigation, GETs `action:`, and applies what comes back: a plain HTML fragment is189
morphed into `target:`, while a `ui-response` batch is applied as its `ui-*`190
updates (those name their own targets, so `target:` is just the fragment191
fallback). With no `target:` it stays an ordinary link, so non-JS clients still192
navigate. Children are a string or a list of nodes.194
```scheme195
(sg-link "Edit" action: "/items/1/edit" target: "#item-1")196
```198
### sg-form200
```scheme201
(sg-form202
(list (sg-text-field name: "text" placeholder: "What needs doing?")203
(sg-submit-button label: "Add"))204
action: "/items" id: "add-form") ; POST by default205
```207
## Form Fields209
`sg-text-field`, `sg-email-field`, `sg-password-field`, `sg-hidden-field`,210
`sg-textarea-field`, `sg-select-field`, `sg-checkbox-field`, and the underlying211
`sg-input-field`. Non-string values are coerced (numbers stringified, `#f` omits212
the attribute); `label:`/`error:` wrap the field in a `<div>` with a `<label>`.214
```scheme215
(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
## Widgets222
- `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 Patterns231
### A form that resets itself and broadcasts a new row233
```scheme234
(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 row238
(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 form240
```242
### A full page, wired up, in one handler244
```scheme245
(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 app250
(-> (routes (router (route method: GET pattern: "/" handler: home-handler)))251
(with-sigil-ui) (with-logging) (with-not-found)))252
```254
For live lists and streams built on this vocabulary, see `(sigil web live)`255
(`live.md`).