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