Add live-DX ergonomics: page helpers, ui-* rename, method defaults, list content
A. http-response/sxml + http-response/page (default template auto-injects <head> + sigil-web-ui-head; template: override) — streamlined full-page responses with zero head plumbing. B. sg-button/sg-form default method: to 'POST and accept method symbols (via method->attr); route method defaults to 'POST with normalize-method so string/case differences still match. sg-link stays GET-only. D. content: in ui-morph / sigil-ui-response now accepts a string, a single SXML node, or a list of nodes (joined); sxml-list->html exported. No more hand-rolled (apply string-append (map sxml->xml ...)). E. Rename the server-driven UI-update family sse- -> ui- (transport-neutral): ui-morph/ui-remove/ui-class/ui-eval/ui-redirect/ui-reload/ui-css-reload/ ui-js/ui-flash, and sse-response-batch -> ui-response. Every old sse- name kept as a deprecated alias through 1.0. Wire events stay sigil: (no client JS change).
src/sigil/web/routes.sgl | 46 +++++++++++++++++------
src/sigil/web/ui.sgl | 346 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------
2 files changed, 288 insertions(+), 104 deletions(-)src/sigil/web/routes.sglmodified
;;; Example:;;; (define app;;; (router;;; (route GET "/" home-handler);;; (route GET "/users/:id" user-handler);;; (route POST "/api/login" login-handler);;; (route ANY "/static/*path" static-handler)));;; (route method: GET pattern: "/" handler: home-handler);;; (route method: GET pattern: "/users/:id" handler: user-handler);;; (route method: POST pattern: "/api/login" handler: login-handler);;; (route method: ANY pattern: "/static/*path" handler: static-handler)));;;;;; `method:` defaults to POST when omitted.(define-library (sigil web routes) (import (sigil core) ;; Route Record ;; ============================================================ ;;; A route consists of a method, pattern, and handler ;;; A route consists of a method, pattern, and handler. ;;; ;;; `method:` is an HTTP-method symbol (`GET`, `POST`, `ANY`, ...) and ;;; defaults to `POST` when omitted. A `"post"` string is accepted too and ;;; normalized at match time. (define-struct route (method) ; Symbol: 'GET, 'POST, 'ANY, etc. (pattern) ; String: "/users/:id" or "/static/*path" (handler)) ; Procedure: (request) -> response (method default: 'POST) ; Symbol: 'GET, 'POST, 'ANY, etc. (or string) (pattern) ; String: "/users/:id" or "/static/*path" (handler)) ; Procedure: (request) -> response ;; ============================================================ ;; Pattern Matching ;; Router ;; ============================================================ ;;; Normalize an HTTP method to an upcased symbol. ;;; Accepts a symbol ('GET, 'post) or string ("get", "POST"). (define (normalize-method m) (cond ((symbol? m) (string->symbol (string-upcase (symbol->string m)))) ((string? m) (string->symbol (string-upcase m))) (else m))) ;;; Does a route's method match a request's method? ;;; 'ANY matches everything; otherwise compare normalized methods so ;;; symbol/string and case differences don't matter. (define (method-matches? route-meth req-meth) (or (eq? route-meth 'ANY) (eq? (normalize-method route-meth) (normalize-method req-meth)))) ;;; Create a router from a list of routes ;;; Returns a handler function: (request) -> response or #f (define (make-router routes) (route-meth (route-method r)) (pattern (route-pattern r)) (handler (route-handler r))) (if (or (eq? route-meth 'ANY) (eq? route-meth method)) (if (method-matches? route-meth method) (let ((params (match-pattern (parse-pattern pattern) path))) (if params ;; Match! Add params to request context and call handler ;; Method didn't match, try next route (loop (cdr routes))))))))) ;;; Convenience macro-like function for creating routers ;;; Usage: (router (route 'GET "/" handler1) (route 'POST "/api" handler2)) ;;; Convenience function for creating routers. ;;; Usage: (router (route method: GET pattern: "/" handler: handler1) ;;; (route method: POST pattern: "/api" handler: handler2)) (define (router . routes) (: any? ... -> procedure?) (make-router routes))src/sigil/web/ui.sglmodified
;;; The server is the source of truth; the client is a thin rendering layer.;;;;;; Core concepts:;;; - SSE events for real-time updates (sigil:morph, sigil:eval, sigil:redirect);;; - Server-driven UI updates (ui-morph, ui-eval, ui-redirect, ...) that;;; feed both live SSE broadcasts and per-request responses;;; - HTTP headers for single-target responses (Sigil-UI-Merge-*);;; - Declarative HTML attributes (data-sg-*) for client interactions;;;;;; (import (sigil web ui);;; (sigil http));;;;;; ;; Send an SSE event to update a target;;; (write-chunk (sse-morph target: "#chat" mode: "append";;; content: `(div (@ (class "msg")) "Hello!")));;; ;; Broadcast a UI update to every connected client;;; (broadcast-send hub (ui-morph target: "#chat" mode: "append";;; content: `(div (@ (class "msg")) "Hello!")));;;;;; ;; Return HTML with merge headers;;; ;; Return HTML with merge headers (reaches only the acting request);;; (sigil-ui-response target: "#results" mode: "inner";;; content: `(ul ,@(map render-item items)));;; content: (map render-item items));;; ```(define-library (sigil web ui) (sigil resources)) (export ;; SSE event formatters ;; Server-driven UI updates (transport-neutral names) ui-morph ui-remove ui-class ui-eval ui-redirect ui-reload ui-css-reload ui-js ui-flash ui-response ;; Deprecated aliases (sse-* names, kept through 1.0) sse-morph sse-remove sse-class sse-reload sse-css-reload sse-js sse-flash sse-response-batch ;; SXML rendering sxml-list->html ;; Page/response helpers http-response/sxml http-response/page default-page-template ;; Response helpers sigil-ui-headers sigil-ui-response sigil-ui-redirect sse-response-batch ;; Component helpers sg-button ;; Flash messages flash-message sse-flash ;; Loading indicator sg-loading-indicator (begin ;; ============================================================ ;; SSE Event Formatters ;; SXML Rendering Helpers ;; ============================================================ ;;; Render a list of SXML nodes to a single HTML string. ;;; ;;; Saves you from hand-rolling `(apply string-append (map sxml->xml xs))` ;;; whenever you have a collection of nodes (e.g. a list of rendered rows) ;;; that needs to become one fragment. ;;; ;;; Example: ;;; ```scheme ;;; (sxml-list->html (map render-row items)) ;;; ``` (define (sxml-list->html nodes) (: list? -> string?) (apply string-append (map sxml->xml nodes))) ;; Coerce a `content:` value into an HTML string. ;; string -> passed through unchanged ;; #f -> #f (caller omits the content) ;; () -> "" (empty fragment) ;; list of nodes -> each node serialized and concatenated ;; single SXML node-> serialized ;; A single element is `(tag ...)` (car is a symbol); a list of nodes is ;; `((tag ...) (tag ...) ...)` (car is itself a list). This lets callers ;; pass `(map render-item items)` directly with no map/append ceremony. (define (content->html content) (cond ((not content) #f) ((string? content) content) ((null? content) "") ((pair? (car content)) (sxml-list->html content)) (else (sxml->xml content)))) ;; ============================================================ ;; Server-Driven UI Updates ;; ============================================================ ;; ;; These produce SSE-formatted strings for the line-based wire protocol ;; (`event: sigil:<type>` / `data: ...`). They are transport-neutral from ;; the author's point of view: the same builder feeds both a live SSE ;; broadcast (reaching every client) and a `ui-response` batch (reaching ;; only the acting request). The client applies whichever arrives. ;; ;; These produce SSE-formatted strings for the line-based protocol. ;; Format: ;; event: sigil:<type> ;; data: field value ;; data: html <line1> ;; data: html <line2> ;; <blank line> ;; Historical note: these were named `sse-*`. The `ui-*` names are the ;; idiom now; every `sse-*` name remains as a deprecated alias through 1.0. ;;; Format an SSE morph event. ;;; Format a UI morph update. ;;; ;;; Morphs HTML content into a target element. The content can be ;;; SXML (converted to HTML) or a string. ;;; Morphs HTML content into a target element. `content:` accepts a string, ;;; a single SXML node, or a list of SXML nodes (joined automatically). ;;; ;;; Parameters: ;;; target: CSS selector for the target element (e.g., "#chat") ;;; content: SXML or HTML string to morph ;;; content: string, SXML node, or list of SXML nodes to morph ;;; mode: Morph mode (default: "morph") ;;; settle: Milliseconds to wait after morph (for CSS transitions) ;;; ;;; ;;; Example: ;;; ```scheme ;;; (sse-morph target: "#messages" mode: "append" ;;; content: `(div (@ (class "msg")) "Hello!")) ;;; (ui-morph target: "#messages" mode: "append" ;;; content: `(div (@ (class "msg")) "Hello!")) ;;; ``` (define (sse-morph (keys: (target #f) (content #f) (mode "morph") (settle #f))) (let ((html (cond ((string? content) content) (content (sxml->xml content)) (else #f)))) (define (ui-morph (keys: (target #f) (content #f) (mode "morph") (settle #f))) (let ((html (content->html content))) (string-append "event: sigil:morph\n" "data: target " target "\n" "") "\n"))) ;;; Format an SSE remove event. ;;; Format a UI remove update. ;;; ;;; Removes the target element from the DOM. Shorthand for ;;; `(sse-morph target: TARGET mode: "remove")`. ;;; `(ui-morph target: TARGET mode: "remove")`. ;;; ;;; Example: ;;; ```scheme ;;; (sse-remove target: "#notification") ;;; (ui-remove target: "#notification") ;;; ``` (define (sse-remove (keys: (target #f))) (sse-morph target: target mode: "remove")) (define (ui-remove (keys: (target #f))) (ui-morph target: target mode: "remove")) ;;; Format an SSE class event. ;;; Format a UI class update. ;;; ;;; Adds or removes CSS classes on the target element. ;;; ;;; ;;; Example: ;;; ```scheme ;;; (sse-class target: "#panel" add: "visible active") ;;; (sse-class target: "#btn" remove: "loading" add: "done") ;;; (ui-class target: "#panel" add: "visible active") ;;; (ui-class target: "#btn" remove: "loading" add: "done") ;;; ``` (define (sse-class (keys: (target #f) (add #f) (remove #f))) (define (ui-class (keys: (target #f) (add #f) (remove #f))) (string-append "event: sigil:class\n" "data: target " target "\n" lines) "\n"))) ;;; Format an SSE eval event. ;;; Format a UI eval update. ;;; ;;; Executes a limited command on the client. Only use for things ;;; that can't be expressed as HTML state (focus, scroll). ;;; ;;; Example: ;;; ```scheme ;;; (sse-eval cmd: "focus" target: "#input") ;;; (sse-eval cmd: "scroll-to" target: "#chat" position: "bottom") ;;; (ui-eval cmd: "focus" target: "#input") ;;; (ui-eval cmd: "scroll-to" target: "#chat" position: "bottom") ;;; ``` (define (sse-eval (keys: (cmd #f) (define (ui-eval (keys: (cmd #f) (target #f) (position #f))) (string-append "") "\n")) ;;; Format an SSE redirect event. ;;; Format a UI redirect update. ;;; ;;; Redirects the browser to a new URL. ;;; ;;; Example: ;;; ```scheme ;;; (sse-redirect url: "/login") ;;; (ui-redirect url: "/login") ;;; ``` (define (sse-redirect (keys: (url #f))) (define (ui-redirect (keys: (url #f))) (string-append "event: sigil:redirect\n" "data: url " url "\n" "\n")) ;;; Format an SSE reload event. ;;; Format a UI reload update. ;;; ;;; Tells the browser to re-fetch the current page and morph the ;;; result into the DOM using Idiomorph. Preserves scroll position, ;;; ;;; Example: ;;; ```scheme ;;; (sse-reload) ; reload full body ;;; (sse-reload target: "#sidebar") ; reload specific element ;;; (ui-reload) ; reload full body ;;; (ui-reload target: "#sidebar") ; reload specific element ;;; ``` (define (sse-reload (keys: (target "body"))) (define (ui-reload (keys: (target "body"))) (string-append "event: sigil:reload\n" "data: target " target "\n" "\n")) ;;; Format an SSE css-reload event. ;;; Format a UI css-reload update. ;;; ;;; Reloads CSS stylesheets in the browser without a full page reload. ;;; If `href:` is provided, only stylesheets matching that substring ;;; ;;; Example: ;;; ```scheme ;;; (sse-css-reload) ; reload all stylesheets ;;; (sse-css-reload href: "styles.css") ; reload specific stylesheet ;;; (ui-css-reload) ; reload all stylesheets ;;; (ui-css-reload href: "styles.css") ; reload specific stylesheet ;;; ``` (define (sse-css-reload (keys: (href #f))) (define (ui-css-reload (keys: (href #f))) (string-append "event: sigil:css-reload\n" (if href "") "\n")) ;;; Format an SSE js event. ;;; Format a UI js update. ;;; ;;; Executes JavaScript code in all connected browsers. ;;; ;;; Example: ;;; ```scheme ;;; (sse-js code: "console.log('hello')") ;;; (ui-js code: "console.log('hello')") ;;; ``` (define (sse-js (keys: (code #f))) (define (ui-js (keys: (code #f))) (let ((lines (string-split code "\n"))) (string-append "event: sigil:js\n" ;;; Create an HTML response with Sigil-UI merge headers. ;;; ;;; Convenience helper that combines headers and body. ;;; A fragment-over-plain-HTTP response: reaches only the acting request ;;; (not a broadcast). `content:` accepts a string, a single SXML node, ;;; or a list of SXML nodes (joined automatically). ;;; ;;; Example: ;;; ```scheme (content #f) (mode "morph") (status 200))) (let ((html (if (string? content) content (sxml->xml content)))) (let ((html (or (content->html content) ""))) (http-response status: status headers: (dict Sigil-UI-Redirect: url) body: "")) ;;; Create a batch SSE response for multi-target updates. ;;; Bundle UI updates into a single response for the acting request. ;;; ;;; Returns SSE-formatted events in a single response. Useful when ;;; one action needs to update multiple targets. ;;; Pairs with `sigil-ui-response` (which carries one HTML fragment via ;;; headers); `ui-response` carries any number of `ui-*` updates in the ;;; body, applied by the client on arrival. Call with no arguments to ;;; return an empty response when the visible effect goes out over a ;;; broadcast instead and the actor needs no direct change. ;;; ;;; Example: ;;; ```scheme ;;; (sse-response-batch ;;; (sse-morph target: "#sidebar" content: new-sidebar) ;;; (sse-morph target: "#main" content: new-main) ;;; (sse-eval cmd: "focus" target: "#input")) ;;; (ui-response ;;; (ui-morph target: "#sidebar" content: new-sidebar) ;;; (ui-morph target: "#main" content: new-main) ;;; (ui-eval cmd: "focus" target: "#input")) ;;; ;;; (ui-response) ; nothing to do on the actor's side ;;; ``` (define (sse-response-batch . events) (define (ui-response . updates) (http-response status: 200 headers: (dict content-type: "text/event-stream" cache-control: "no-cache") body: (string-join events ""))) body: (string-join updates ""))) ;; ============================================================ ;; Full-Page Response Helpers ;; ============================================================ ;;; Serialize a full SXML page to an HTML-document response. ;;; ;;; The low-level escape hatch: you bring the whole page (including ;;; `<head>`), and this prepends the `<!DOCTYPE html>`, serializes with ;;; `sxml->html`, and sets `Content-Type: text/html`. Use `http-response/page` ;;; for the common case where you only have a title and body. ;;; ;;; Example: ;;; ```scheme ;;; (http-response/sxml ;;; `(html (head (title "Hi")) (body (h1 "Hi")))) ;;; ``` (define (http-response/sxml page (keys: (status 200))) (http-response status: status headers: (dict content-type: "text/html; charset=utf-8") body: (string-append "<!DOCTYPE html>\n" (sxml->html page)))) ;;; The default page template used by `http-response/page`. ;;; ;;; A minimal HTML shell: charset + viewport meta, a `<title>` from ;;; `title`, the Sigil Web UI client scripts (so `data-sg-*` actions and ;;; live updates work with zero head plumbing), and `body` spliced into ;;; `<body>`. `body` may be a single SXML node or a list of nodes. ;;; ;;; Write your own `(title body) -> full-page-sxml` function and pass it ;;; as `template:` to `http-response/page` to customize the shell. (define (default-page-template title body) `(html (head (meta (@ (charset "utf-8"))) (meta (@ (name "viewport") (content "width=device-width, initial-scale=1"))) (title ,title) ,@(sigil-web-ui-head)) (body ,@(if (and (pair? body) (pair? (car body))) body (list body))))) ;;; Respond with a full HTML page from a title and body. ;;; ;;; The streamlined common case. Wraps `body` in `template` (default: ;;; `default-page-template`, a working script-wired shell) and returns an ;;; HTML-document response. A beginner writes `(http-response/page "To-Do" body)` ;;; and gets a complete page with head, viewport, and the UI client wired ;;; up. Override the shell with `template:` (a `(title body) -> sxml` ;;; function), or drop to `http-response/sxml` to supply the whole page. ;;; ;;; `body` may be a single SXML node or a list of nodes. ;;; ;;; Example: ;;; ```scheme ;;; (http-response/page "To-Do" (todo-page items)) ;;; (http-response/page "Not found" '(p "No such item.") status: 404) ;;; ``` (define (http-response/page title body (keys: (status 200) (template default-page-template))) (http-response/sxml (template title body) status: status)) ;; ============================================================ ;; Deprecated aliases — `sse-*` UI-update names ;; ============================================================ ;; ;; The UI-update family was renamed `sse-*` -> `ui-*` (the updates are ;; transport-neutral, not SSE-specific). These aliases keep existing code ;; working through 1.0; prefer the `ui-*` names in new code. (define sse-morph ui-morph) (define sse-remove ui-remove) (define sse-class ui-class) (define sse-eval ui-eval) (define sse-redirect ui-redirect) (define sse-reload ui-reload) (define sse-css-reload ui-css-reload) (define sse-js ui-js) (define sse-response-batch ui-response) ;; ============================================================ (cons (list name val) acc) acc)))))) ;; Coerce an HTTP-method value to the string used in a data-sg-method ;; attribute. Accepts a symbol (GET/POST/...) or a string ("post"); the ;; client upcases it before fetching, so either case works on the wire. (define (method->attr m) (cond ((symbol? m) (symbol->string m)) ((string? m) m) (else "POST"))) ;; Coerce a value for use in an HTML value attribute. ;; #f -> #f (omit attribute), numbers -> string, strings pass through. (define (coerce-field-value val) ;;; Create a button that triggers an action. ;;; ;;; `method:` accepts an HTTP-method symbol (`POST`, `GET`, ...) and ;;; defaults to `POST` (actions are almost always POST); a `"post"` string ;;; still works. ;;; ;;; Example: ;;; ```scheme ;;; (sg-button "Like" action: "/api/like" method: "post" ;;; (sg-button "Like" action: "/api/like" ; POST by default ;;; loading: "opacity-50") ;;; ``` (define (sg-button label (keys: (action #f) (method "post") (method 'POST) (target #f) (mode #f)Showing the first 500 of 591 diff lines for this file. This diff is INCOMPLETE; read the file or clone the repository for the rest.