Commita497d540Recorded5 Jul 2026Repositorysigil-web
Refresh sigil-web docs to the current API; add live.md
Message
The docs/ were from 2026-03-31 and documented the deprecated names (sse-*, sigil-ui-response, sse-response-batch) and the pre-default-POST, positional-route API. Rewrite to the current surface, verified against the exports on this branch.
- ui.md: http-response/page + /sxml, with-sigil-ui, ui-update (+ the mode table incl. morph/morph-inner), ui-remove/class/eval/redirect/reload/flash, ui-response (sole actor-response builder, optional status:), current sg-* (symbol methods, POST default, positional labels), widgets, and a short deprecated-aliases note.
- routing.md: keyword route (method symbols, POST default), router/routes, ui-routes, path-param, the middleware table incl. with-sigil-ui. Fixed the positional (route GET "/" h) examples that never actually worked.
- live.md (new): the whole (sigil web live) module — live-collection, live-stream, live-add!/update!/remove!, live-get/all, live-push!, live-list-view, live-subscribe, live-routes, on-change:, with a collection-free feed pattern.
Style matches sigil-http/docs/http.md (terse, ### per function + short example, ## Common Patterns). Docs build clean (advisory >200-line warnings only, as with http.md).
Changed
docs/live.md | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
docs/routing.md | 63 ++++++++++++++++++++++---------
docs/ui.md | 362 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------------------------------------------------------
3 files changed, 379 insertions(+), 235 deletions(-)Diff
docs/live.mdadded
@@ -0,0 +1,189 @@
+1
# Live Collections & Streams+2
+3
> Push server-driven UI updates to every connected browser over SSE, with no+4
> hand-written hub, event route, snapshot, or heartbeat.+5
+6
```scheme+7
(import (sigil web live))+8
```+9
+10
Two building blocks:+11
+12
- A **`live-stream`** is a push channel clients subscribe to. You `live-push!`+13
`ui-*` updates and every subscriber applies them. No store, no render — the+14
general case (a chat feed, a room log, notifications).+15
- A **`live-collection`** is a `live-stream` plus an id-keyed store and a+16
per-item render function. Mutating it broadcasts the right DOM patch+17
automatically: a new row appends, an updated row morphs in place by id, a+18
removed row is deleted by id.+19
+20
A mutation/push is what **everyone** sees; a handler's return value is what the+21
**acting user** sees (usually an empty `ui-response`, or nothing under+22
`ui-routes`).+23
+24
The client is served by `with-sigil-ui` and injected by `http-response/page`;+25
each stream's endpoint is registered with `live-routes`.+26
+27
## Collections+28
+29
### live-collection+30
+31
Create a collection: a stream whose subscribers see a list of rendered rows kept+32
in sync as you mutate it. The `render` function's top-level element MUST carry an+33
`id` — the collection targets that id when it morphs or removes the row.+34
+35
```scheme+36
(define todos+37
(live-collection render: item-row+38
container: "#todo-list"+39
path: "/events"))+40
```+41
+42
- `render:` — `item -> SXML` for one row (its top element has an `id`).+43
- `container:` — CSS id-selector of the list element rows live in.+44
- `path:` — the SSE path the page subscribes to (default `"/events"`).+45
- `on-change:` — optional, see below.+46
+47
### live-add! / live-update! / live-remove!+48
+49
Mutate the store and broadcast the matching patch to every client. They return+50
the affected item (or `#f`), **not** an HTTP response.+51
+52
```scheme+53
(live-add! todos (dict text: "Buy milk" done?: #f)) ; broadcast: append row+54
(live-update! todos id (lambda (i) (dict-set i done?: #t))) ; broadcast: morph row by id+55
(live-remove! todos id) ; broadcast: remove row by id+56
```+57
+58
The collection assigns the `id:` field on `live-add!`. `live-update!` takes a+59
function `item -> item` (dicts are immutable; return a new one with `dict-set`).+60
+61
### live-get / live-all+62
+63
Read accessors (for a handler that needs current state, e.g. an inline edit+64
form). Read-only.+65
+66
```scheme+67
(live-get todos id) ; => item or #f+68
(live-all todos) ; => items in insertion order+69
```+70
+71
### live-list-view+72
+73
Render the collection's list: the `<ul>` (or container) with the current rows,+74
plus the hidden element that subscribes the page to the stream. Drop it in a body.+75
+76
```scheme+77
(http-response/page "To-Do"+78
`(div (h1 "To-Do") ,(add-form) ,(live-list-view todos)))+79
```+80
+81
### on-change:+82
+83
An optional `(items -> ui-update or list)` on `live-collection`, broadcast after+84
**every** mutation. For UI derived from the whole list — a count, a summary. It+85
receives only the item list; per-action events go through `live-push!`.+86
+87
```scheme+88
(define todos+89
(live-collection render: item-row container: "#todo-list" path: "/events"+90
on-change: (lambda (items)+91
(ui-update target: "#count" mode: "inner"+92
content: (count-label (length items))))))+93
```+94
+95
## Streams+96
+97
### live-stream+98
+99
A bare push channel — hub + SSE route + lazy heartbeat + subscription element,+100
with no store or render. `on-connect:` is an optional thunk returning an initial+101
`ui-update` string sent to each new subscriber (a snapshot); a plain feed omits+102
it, so late joiners see only future pushes.+103
+104
```scheme+105
(define log (live-stream path: "/log"))+106
```+107
+108
### live-push!+109
+110
Send a `ui-update` (or a list of them) to every subscriber of a stream **or** a+111
collection.+112
+113
```scheme+114
(live-push! log (ui-update target: "#log" mode: "append"+115
content: `(li "Item added")))+116
```+117
+118
### live-subscribe+119
+120
The hidden `data-sg-sse` element that subscribes a page to a stream (or a+121
collection). `live-list-view` includes one for its collection; use+122
`live-subscribe` directly for a bare stream.+123
+124
```scheme+125
`(div (h2 "Activity") (ul (@ (id "log"))) ,(live-subscribe log))+126
```+127
+128
## Routing+129
+130
### live-routes+131
+132
Return a handler that serves the SSE endpoint for a stream or a collection.+133
Combine it with your app's routes.+134
+135
```scheme+136
(define app+137
(-> (ui-routes+138
(router …item routes…)+139
(live-routes todos) ; registers /events+140
(live-routes log)) ; registers /log+141
(with-sigil-ui) (with-logging) (with-not-found)))+142
```+143
+144
The heartbeat starts lazily on the first connection (inside the server's async+145
context), so `main` is just `(with-async (http-serve app port: 8080))` — no+146
explicit start call.+147
+148
## Common Patterns+149
+150
### CRUD handlers over a live collection+151
+152
Under `ui-routes`, each handler is the mutation; the acting user gets an empty+153
acknowledgement while everyone gets the broadcast.+154
+155
```scheme+156
(define (create-handler req)+157
(live-add! todos (dict text: (form-text req) done?: #f))+158
(ui-response (ui-update target: "#add-form" content: (add-form)))) ; reset the form+159
+160
(define (toggle-handler req)+161
(live-update! todos (item-id req)+162
(lambda (i) (dict-set i done?: (not (dict-ref i done?:))))))+163
+164
(define (delete-handler req)+165
(live-remove! todos (item-id req)))+166
```+167
+168
### A collection-free feed (chat / room log / notifications)+169
+170
The general case: a bare `live-stream` and `live-push!`. No store, no render —+171
just push a `ui-update` on each event.+172
+173
```scheme+174
(define chat (live-stream path: "/chat"))+175
+176
;; page body: (ul (@ (id "messages"))) + (live-subscribe chat)+177
+178
(define (say-handler req)+179
(live-push! chat+180
(ui-update target: "#messages" mode: "append"+181
content: `(li ,(form-text req))))+182
(ui-response))+183
```+184
+185
### Derived summary UI+186
+187
Use `on-change:` for anything computed from the whole list (a count, totals, an+188
"all done" flag); it re-broadcasts after every mutation, so the summary stays in+189
sync without touching the handlers.docs/routing.mdmodified
@@ -18,19 +18,21 @@ The `(sigil web)` module re-exports all routing, middleware, cookie, and static
18
19
## Routes 20
−21
Define routes with an HTTP method, URL pattern, and handler function. Handlers receive a request and return a response (or `#f` for no match).+21
Define routes with an HTTP method, URL pattern, and handler. Handlers receive a+22
request and return a response (or `#f` for no match). `method:` takes an+23
HTTP-method **symbol** and defaults to `POST` when omitted (a `"post"` string is+24
accepted too). 25
26
```scheme 27
(define app 28
(router−26
(route GET "/" home-handler)−27
(route GET "/about" about-handler)−28
(route POST "/api/login" login-handler)))+29
(route method: GET pattern: "/" handler: home-handler)+30
(route method: GET pattern: "/about" handler: about-handler)+31
(route method: POST pattern: "/api/login" handler: login-handler))) 32
``` 33
−31
Available method constants: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `OPTIONS`, `HEAD`, `ANY`.−32
−33
`ANY` matches all HTTP methods.+34
Available method symbols: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `OPTIONS`,+35
`HEAD`, `ANY`. `ANY` matches all HTTP methods. 36
37
## Pattern Matching 38
@@ -44,8 +46,8 @@ Route patterns support three segment types:
46
47
```scheme 48
(router−47
(route GET "/users/:id" user-handler)−48
(route GET "/files/*path" file-handler))+49
(route method: GET pattern: "/users/:id" handler: user-handler)+50
(route method: GET pattern: "/files/*path" handler: file-handler)) 51
``` 52
53
### Path Parameters@@ -66,23 +68,49 @@ Extract parameters from matched routes using `path-param`:
68
69
## Route Combination 70
−69
`routes` combines multiple handlers into one. The first handler to return a non-`#f` response wins.+71
`routes` combines multiple handlers into one. The first handler to return a+72
non-`#f` response wins. 73
74
```scheme 75
(define api-routes 76
(router−74
(route GET "/api/users" list-users)−75
(route POST "/api/users" create-user)))+77
(route method: GET pattern: "/api/users" handler: list-users)+78
(route method: POST pattern: "/api/users" handler: create-user))) 79
80
(define page-routes 81
(router−79
(route GET "/" home-page)−80
(route GET "/about" about-page)))+82
(route method: GET pattern: "/" handler: home-page)+83
(route method: GET pattern: "/about" handler: about-page))) 84
85
(define app 86
(routes api-routes page-routes)) 87
``` 88
+89
### ui-routes+90
+91
`ui-routes` is a `routes` analog for hypermedia handlers: it combines handlers+92
the same way, but a handler that returns a value which **isn't** an HTTP response+93
is taken as "handled, nothing extra for the acting user" and becomes an empty+94
`(ui-response)`. So an action handler can end with just its mutation, no trailing+95
`(ui-response)`:+96
+97
```scheme+98
(define (toggle-handler req)+99
(live-update! todos (item-id req)+100
(lambda (i) (dict-set i done?: (not (dict-ref i done?:)))))) ; no ui-response needed+101
+102
(define app+103
(-> (routes+104
(ui-routes (router …hypermedia…)) ; non-response returns -> empty ack+105
(router …json-api…)) ; plain routes never coerce+106
(with-sigil-ui) (with-logging) (with-not-found)))+107
```+108
+109
`#f` still falls through (a missing-id mutation → 404 via `with-not-found`), and+110
explicit responses pass through untouched. Keep JSON APIs and webhooks under+111
plain `routes`/`router`, which never coerce. `ui-routes` is exported from+112
`(sigil web ui)`.+113
114
## Middleware 115
116
Middleware wraps a handler to add cross-cutting concerns. Two styles are supported.@@ -106,6 +134,7 @@ Chain-style wrappers take a handler and return a wrapped handler, ideal for use
134
| `with-cors` | `allow-origin:`, `allow-methods:`, `allow-headers:` | Add CORS headers (default: allow all origins) | 135
| `with-content-type` | `default:` | Set default Content-Type (default: `"text/html"`) | 136
| `with-not-found` | `body:` | Return 404 if handler returns `#f` |+137
| `with-sigil-ui` | `path:` | Serve the client script at `/js/sigil-web-ui.js` (from `(sigil web ui)`) | 138
139
```scheme 140
;; Custom CORS settings@@ -177,7 +206,7 @@ Serve files from a directory with automatic MIME type detection.
206
;; Use in a router with a wildcard pattern 207
(define app 208
(-> (routes−180
(router (route GET "/" home-handler))+209
(router (route method: GET pattern: "/" handler: home-handler)) 210
(make-static-handler "public/" 211
'((prefix . "/static")))) 212
(with-not-found)))@@ -218,8 +247,8 @@ Path traversal attacks (`..`) are blocked by `safe-path?`.
247
(define app 248
(-> (routes 249
(router−221
(route GET "/" home-handler)−222
(route GET "/users/:id" user-handler))+250
(route method: GET pattern: "/" handler: home-handler)+251
(route method: GET pattern: "/users/:id" handler: user-handler)) 252
(make-static-handler "public/")) 253
(with-logging) 254
(with-cors)docs/ui.mdmodified
@@ -1,314 +1,240 @@
−1
# UI+1
# Web UI 2
−3
> Server-driven UI components and real-time SSE updates.+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−6
(import (sigil web ui)−7
(sigil web)−8
(sigil http))+8
(import (sigil web ui)) 9
``` 10
−11
The `(sigil web ui)` module provides server-side rendering of interactive components. The server is the source of truth; the client is a thin rendering layer powered by a small JavaScript library.+11
The same `ui-*` update vocabulary feeds both a broadcast (every client) and a+12
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`). 14
−13
## Setup+15
## Page Responses 16
−15
Include the client library in your HTML head and add a route to serve it:+17
### http-response/page+18
+19
Respond with a complete HTML page from a title and a body. The default template+20
supplies `<head>` (charset, viewport, `<title>`) and auto-injects the client+21
script, so a page is script-wired with zero head plumbing. 22
23
```scheme−18
(define (layout title body)−19
(sxml->html−20
`(html−21
(head−22
,@(sigil-web-ui-head)−23
(title ,title))−24
(body ,@body))))+24
(http-response/page "To-Do"+25
`(div (@ (class "app")) (h1 "To-Do") ,(todo-list))) 26
−26
(define app−27
(router−28
(route GET "/js/sigil-web-ui.js" (sigil-web-ui-handler))−29
...))+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
``` 31
−32
`sigil-web-ui-head` returns script tags for the idiomorph library and the Sigil UI script. Customize URLs with `idiomorph-url:` and `script-url:` keywords.+32
### http-response/sxml 33
−34
## SSE Updates+34
The escape hatch: serialize a full SXML page (you bring the whole `<html>`) to an+35
HTML document. Prepends `<!DOCTYPE html>`, sets `text/html`. 36
−36
Server-Sent Events allow pushing real-time updates from the server. Each helper produces an SSE-formatted string to send via chunked response.+37
```scheme+38
(http-response/sxml+39
`(html (head (title "Hi")) (body (h1 "Hi"))))+40
``` 41
−38
### sse-morph+42
### with-sigil-ui 43
−40
Morph HTML content into a target element.+44
Middleware that serves the client script at `/js/sigil-web-ui.js`, so you never+45
register that route by hand. Pairs with `http-response/page` injecting the tag. 46
47
```scheme−43
(sse-morph target: "#messages" mode: "append"−44
content: `(div (@ (class "msg")) "New message!"))+48
(-> (routes (router …))+49
(with-sigil-ui)+50
(with-logging)+51
(with-not-found)) 52
``` 53
−47
| Mode | Description |−48
|------|-------------|−49
| `"morph"` | Intelligent diff/patch via idiomorph (default) |−50
| `"replace"` | Replace target's outerHTML |−51
| `"inner"` | Replace target's innerHTML |−52
| `"append"` | Append to target's children |−53
| `"prepend"` | Prepend to target's children |−54
| `"before"` | Insert before target |−55
| `"after"` | Insert after target |+54
## UI Updates 55
−57
The `settle:` keyword adds a delay in milliseconds after morphing, useful for CSS transitions.+56
`ui-update` and friends build server-driven UI updates. Feed them to a broadcast+57
(see `(sigil web live)`) or bundle them in a `ui-response` for the acting request. 58
−59
### sse-remove+59
### ui-update 60
−61
Remove an element from the DOM.+61
Update a target element with HTML content. `content:` accepts a string, a single+62
SXML node, or a list of SXML nodes (joined automatically). 63
64
```scheme−64
(sse-remove target: "#notification")+65
(ui-update target: "#messages" mode: "append"+66
content: `(div (@ (class "msg")) "Hello!")) 67
``` 68
−67
### sse-class−68
−69
Add or remove CSS classes on a target element.+69
`mode:` selects how the content lands (default `"morph"`): 70
−71
```scheme−72
(sse-class target: "#panel" add: "visible active")−73
(sse-class target: "#btn" remove: "loading" add: "done")−74
```+71
| mode | effect |+72
|---------------|---------------------------------------------------------------|+73
| `morph` | Morph the target **element itself** via idiomorph, in place (default). Content is a full element whose id matches the target — the common case. |+74
| `morph-inner` | Morph the target's **inner** content via idiomorph. |+75
| `inner` | Replace the target's innerHTML (no diffing). |+76
| `replace` | Replace the target's outerHTML (no diffing). |+77
| `append` | Append to the target's children. |+78
| `prepend` | Prepend to the target's children. |+79
| `before` / `after` | Insert before / after the target. |+80
| `remove` | Remove the target element. | 81
−76
### sse-eval+82
### ui-remove 83
−78
Execute a limited client command (focus, scroll).+84
Remove the target element. Shorthand for `(ui-update target: … mode: "remove")`. 85
86
```scheme−81
(sse-eval cmd: "focus" target: "#input")−82
(sse-eval cmd: "scroll-to" target: "#chat" position: "bottom")+87
(ui-remove target: "#item-3") 88
``` 89
−85
### sse-redirect+90
### ui-class 91
−87
Redirect the browser to a new URL.+92
Add or remove CSS classes on a target. 93
94
```scheme−90
(sse-redirect url: "/login")+95
(ui-class target: "#panel" add: "visible active")+96
(ui-class target: "#btn" remove: "loading" add: "done") 97
``` 98
−93
## Response Helpers−94
−95
### sigil-ui-response+99
### ui-eval 100
−97
Create an HTML response with merge headers for single-target updates.+101
Run a limited client command (things HTML state can't express). 102
103
```scheme−100
(sigil-ui-response target: "#results" mode: "inner"−101
content: `(ul ,@(map render-item items)))+104
(ui-eval cmd: "focus" target: "#input")+105
(ui-eval cmd: "scroll-to" target: "#chat" position: "bottom") 106
``` 107
−104
### sigil-ui-redirect−105
−106
Trigger a client-side redirect (falls back to HTTP 302 for non-JS clients).+108
### ui-redirect / ui-reload 109
110
```scheme−109
(sigil-ui-redirect url: "/dashboard")+111
(ui-redirect url: "/login")+112
(ui-reload) ; re-fetch the page and morph <body>+113
(ui-reload target: "#sidebar") ; morph one element from a re-fetch 114
``` 115
−112
### sse-response-batch+116
### ui-flash 117
−114
Send multiple SSE events in one response for multi-target updates.+118
Push a flash message (append into a container, default `#sg-flash-container`). 119
120
```scheme−117
(sse-response-batch−118
(sse-morph target: "#sidebar" content: new-sidebar)−119
(sse-morph target: "#main" content: new-content)−120
(sse-eval cmd: "focus" target: "#search"))+121
(ui-flash type: 'success message: "Saved!" remove-after: 3000) 122
``` 123
−123
## Form Fields+124
Also available: `ui-css-reload`, `ui-js`, and `flash-message` (the flash SXML+125
without pushing it). 126
−125
Form field helpers generate SXML with labels, error display, and standard HTML attributes.−126
−127
| Helper | HTML type |−128
|--------|-----------|−129
| `sg-text-field` | `<input type="text">` |−130
| `sg-email-field` | `<input type="email">` |−131
| `sg-password-field` | `<input type="password">` |−132
| `sg-hidden-field` | `<input type="hidden">` |−133
| `sg-textarea-field` | `<textarea>` |−134
| `sg-select-field` | `<select>` |−135
| `sg-checkbox-field` | `<input type="checkbox">` |−136
| `sg-submit-button` | `<button type="submit">` |−137
−138
Common keywords shared by most fields:−139
−140
| Keyword | Description |−141
|---------|-------------|−142
| `name:` | Input name attribute |−143
| `value:` | Current value |−144
| `label:` | Label text (wraps in `<label>`) |−145
| `placeholder:` | Placeholder text |−146
| `required:` | Add required attribute |−147
| `disabled:` | Add disabled attribute |−148
| `error:` | Error message (shown in `<span>`) |−149
| `class:` | CSS class |−150
| `id:` | Element ID |+127
## Responding to an Action 128
−152
```scheme−153
(sg-text-field name: "username" label: "Username"−154
placeholder: "Enter name" required: #t)+129
### ui-response 130
−156
(sg-select-field name: "role" label: "Role"−157
options: '(("admin" . "Admin")−158
("user" . "User"))−159
value: "user")+131
The response an action handler returns: a batch of `ui-*` updates the client+132
applies. Same vocabulary you broadcast, so a handler reads as "the mutation is+133
what everyone sees; this response is what the acting user sees." Call it with no+134
updates for an empty acknowledgement (the common case when a broadcast already+135
did the visible work). The optional `status:` is for HTTP hygiene only — it does+136
not drive the UI; surface an error by targeting an error element. 137
−161
(sg-submit-button label: "Save" loading: "opacity-50")+138
```scheme+139
(ui-response) ; empty ack+140
(ui-response (ui-update target: "#add-form" content: (add-form)))+141
(ui-response status: 422+142
(ui-update target: "#errors" content: (errors msgs))) 143
``` 144
−164
## Interactive Components+145
`ui-routes` (see the routing docs) lets a handler skip the empty `ui-response`+146
and just end with its mutation. 147
−166
### sg-button+148
## Components 149
−168
Create a button that triggers a server action via `data-sg-*` attributes.+150
`sg-*` helpers emit HTML with `data-sg-*` attributes; the client fetches the+151
route and applies the result — you never write the fetch. Action helpers accept+152
an HTTP-method **symbol** and default to `POST`.+153
+154
### sg-button 155
156
```scheme−171
(sg-button "Like" action: "/api/like" method: "post"−172
target: "#count" loading: "opacity-50")+157
(sg-button "Done" action: "/items/1/toggle") ; POST by default+158
(sg-button "Load" action: "/data" method: GET) 159
``` 160
161
### sg-link 162
−177
Create a link that morphs content into a target (SPA-style navigation).+163
A link that morphs a fetched fragment into `target:` (falls back to normal+164
navigation for non-JS clients). Children are a string or a list of nodes. 165
166
```scheme−180
(sg-link "Introduction" action: "/lesson/1" target: "#content")+167
(sg-link "Edit" action: "/items/1/edit" target: "#item-1") 168
``` 169
170
### sg-form 171
−185
Create a form with action handling and optional error targeting.−186
−187
```scheme−188
(sg-form (list−189
(sg-email-field name: "email" label: "Email")−190
(sg-password-field name: "password" label: "Password")−191
(sg-submit-button label: "Login"))−192
action: "/api/login" method: "post"−193
target: "#result" error-target: "#errors")−194
```−195
−196
### sg-sse−197
−198
Create an SSE-connected container that receives real-time updates.−199
172
```scheme−201
(sg-sse '((div (@ (id "messages"))))−202
url: "/events/chat" id: "chat-container")+173
(sg-form+174
(list (sg-text-field name: "text" placeholder: "What needs doing?")+175
(sg-submit-button label: "Add"))+176
action: "/items" id: "add-form") ; POST by default 177
``` 178
−205
## Higher-Level Components+179
## Form Fields 180
−207
### Modal Dialogs+181
`sg-text-field`, `sg-email-field`, `sg-password-field`, `sg-hidden-field`,+182
`sg-textarea-field`, `sg-select-field`, `sg-checkbox-field`, and the underlying+183
`sg-input-field`. Non-string values are coerced (numbers stringified, `#f` omits+184
the attribute); `label:`/`error:` wrap the field in a `<div>` with a `<label>`. 185
186
```scheme−210
;; Define a modal−211
(sg-modal (list−212
(p "Are you sure you want to delete this item?")−213
(sg-button "Delete" action: "/api/delete/42" method: "delete")−214
(sg-modal-close "Cancel"))−215
id: "confirm-modal" title: "Confirm Delete")−216
−217
;; Trigger button−218
(sg-modal-trigger "Delete Item" target: "#confirm-modal")+187
(sg-text-field name: "title" value: some-value placeholder: "Title" autofocus: #t)+188
(sg-select-field name: "genre" options: '(("rock" . "Rock") ("jazz" . "Jazz")) value: "jazz")+189
(sg-submit-button label: "Save") 190
``` 191
−221
### Data Tables−222
−223
Render tabular data with optional row actions. Column field values are extracted from dicts or alists. Action URLs support `{field}` placeholders interpolated from row data.−224
−225
```scheme−226
(sg-data-table−227
columns: '((name "Name") (email "Email"))−228
rows: users−229
row-actions: (list−230
(sg-table-action "Edit" action: "/users/{id}/edit"−231
method: "get")−232
(sg-table-action "Delete" action: "/users/{id}"−233
method: "delete"−234
confirm: "Delete this user?"))−235
empty-message: "No users found.")−236
```+192
## Widgets 193
−238
### Paginator+194
- `flash-message type: message: (keys: remove-after:)` — a `role="alert"` box.+195
- `sg-loading-indicator` — a spinner element toggled during actions.+196
- `sg-modal` / `sg-modal-trigger` / `sg-modal-close` — `<dialog>`-based modals.+197
- `sg-data-table columns: rows: (keys: row-actions:)` + `sg-table-action`.+198
- `sg-paginator current-page: total-pages: base-url: (keys: target:)`.+199
- `sg-sse children (keys: url:)` — an SSE-subscribed container. 200
−240
Render pagination navigation with prev/next links and page numbers. Uses `sg-link` internally for SSE morphing.+201
## Deprecated Aliases 202
−242
```scheme−243
(sg-paginator current-page: 3 total-pages: 10−244
base-url: "/users" target: "#user-list"−245
params: '((sort . "name")))−246
```+203
The UI-update family shipped as `sse-*`; the current names are `ui-*`. These+204
aliases still work (same bindings) through 1.0 — prefer the `ui-*` names in new+205
code: 206
−248
Keywords: `current-page:`, `total-pages:`, `base-url:`, `target:`, `params:` (extra query params), `window-size:` (pages shown around current, default 2).+207
- `sse-morph` → `ui-update`; `sse-remove`/`sse-class`/`sse-eval`/`sse-redirect`/+208
`sse-reload`/`sse-css-reload`/`sse-js`/`sse-flash` → the matching `ui-*`.+209
- `sse-response-batch` → `ui-response`.+210
- `sigil-ui-response target: content: (keys: (mode) (status))` is the older+211
single-fragment actor response (HTML body + `Sigil-UI-Merge-*` headers). It+212
still works; prefer `ui-response` with a `ui-update` for one consistent model. 213
−250
### Flash Messages+214
## Common Patterns 215
−252
Display temporary notifications with auto-removal.+216
### A form that resets itself and broadcasts a new row 217
218
```scheme−255
;; Create a flash message element−256
(flash-message type: 'success message: "Record saved!"−257
remove-after: 3000)−258
−259
;; Send via SSE (appends to #sg-flash-container)−260
(sse-flash type: 'error message: "Validation failed")+219
(define (create-handler req)+220
(let ((text (assoc-ref 'text (parse-form-data req))))+221
(when (and text (> (string-length text) 0))+222
(broadcast-send hub ; everyone: append the row+223
(ui-update target: "#list" mode: "append" content: (item-row (add-item! text)))))+224
(ui-response (ui-update target: "#add-form" content: (add-form))))) ; you: reset the form 225
``` 226
−263
Types: `'info`, `'success`, `'error`, `'warning`. Each gets a CSS class like `sg-flash-success`.−264
−265
### Loading Indicator+227
### A full page, wired up, in one handler 228
229
```scheme−268
(sg-loading-indicator id: "spinner")−269
(sg-loading-indicator id: "spinner" active: #t)−270
```−271
−272
## Common Patterns−273
−274
### Form with Validation Errors+230
(define (home-handler req)+231
(http-response/page "To-Do"+232
`(div (h1 "To-Do") ,(add-form) (ul (@ (id "list")) ,@(map item-row (all-items)))))) 233
−276
```scheme−277
(define (login-form . errors)−278
(sg-form (list−279
(sg-email-field name: "email" label: "Email"−280
error: (assoc-ref 'email errors #f))−281
(sg-password-field name: "password" label: "Password"−282
error: (assoc-ref 'password errors #f))−283
(sg-submit-button label: "Sign In"))−284
action: "/login" method: "post"−285
target: "#login-form"))−286
−287
(define (login-handler request)−288
(let ((errors (validate-login request)))−289
(if errors−290
(sigil-ui-response target: "#login-form" mode: "morph"−291
content: (login-form errors))−292
(sigil-ui-redirect url: "/dashboard"))))+234
(define app+235
(-> (routes (router (route method: GET pattern: "/" handler: home-handler)))+236
(with-sigil-ui) (with-logging) (with-not-found))) 237
``` 238
−295
### Real-Time Updates−296
−297
```scheme−298
;; Page with SSE connection−299
(define (chat-page request)−300
(http-response/html 200−301
(layout "Chat"−302
(list−303
(sg-sse '((div (@ (id "messages"))))−304
url: "/events/chat")−305
'(div (@ (id "sg-flash-container")))))))−306
−307
;; SSE handler pushes updates−308
(define (chat-event-handler request)−309
(sse-response-batch−310
(sse-morph target: "#messages" mode: "append"−311
content: `(div (@ (class "msg")) ,new-message))−312
(sse-eval cmd: "scroll-to" target: "#messages"−313
position: "bottom")))−314
```+239
For live lists and streams built on this vocabulary, see `(sigil web live)`+240
(`live.md`).