Hiccup → HTML rendering and the small DSL for Datastar attributes.
Two responsibilities live here, deliberately kept apart from the kernel:
Serialise hiccup to HTML with Chassis.
The kernel works with hiccup data structures all the way through;
they are only stringified at the very edge, just before
patch-elements! writes to the wire. This keeps everything before
the wire pure, diff-able, and REPL-inspectable.
Generate Datastar attribute fragments — on, bind — that
tag a piece of UI with the wiring that lets the client post events
back to the right conversation and instance.
The cid only exists at request time, so the helpers consult a dynamic var bound by the http layer for the duration of a render.
Hiccup → HTML rendering and the small DSL for Datastar attributes. Two responsibilities live here, deliberately kept apart from the kernel: 1. **Serialise hiccup to HTML** with [Chassis](https://github.com/onionpancakes/chassis). The kernel works with hiccup data structures all the way through; they are only stringified at the very edge, just before `patch-elements!` writes to the wire. This keeps everything before the wire pure, diff-able, and REPL-inspectable. 2. **Generate Datastar attribute fragments** — `on`, `bind` — that tag a piece of UI with the wiring that lets the client post events back to the right conversation and instance. The cid only exists at request time, so the helpers consult a dynamic var bound by the http layer for the duration of a render.
The conversation id of the request currently being served. Bound by the http layer around every render so attribute helpers can build URLs pointing at the right SSE endpoint.
The conversation id of the request currently being served. Bound by the http layer around every render so attribute helpers can build URLs pointing at the right SSE endpoint.
The conversation being rendered, bound by the kernel for the duration
of one render-frame call. render-slot consults it to look up
embedded children by id.
Two-way bindings (s/bind) and event hooks (s/on) only need the
cid; only slot rendering needs the conversation, hence the separate
vars.
The conversation being rendered, bound by the kernel for the duration of one `render-frame` call. [[render-slot]] consults it to look up embedded children by id. Two-way bindings (`s/bind`) and event hooks (`s/on`) only need the cid; only slot rendering needs the conversation, hence the separate vars.
(back-button label)(back-button label attrs)Return a small Hiccup button wired to the conversation-level [:back]
effect.
(s/back-button "Back")
This intentionally does not take self: browser history rewind is a
conversation operation, not a component-local event. For wizard-style
Back buttons that answer a parent with a sentinel, keep using
(s/on self :click :as ...) from that component.
Return a small Hiccup button wired to the conversation-level `[:back]`
effect.
(s/back-button "Back")
This intentionally does not take `self`: browser history rewind is a
conversation operation, not a component-local event. For wizard-style
Back buttons that answer a parent with a sentinel, keep using
`(s/on self :click :as ...)` from that component.(back-url)URL the browser POSTs to for the conversation-level Back action.
URL the browser POSTs to for the conversation-level Back action.
(bind signal)Return an attribute map that two-way binds the named signal to the current element.
[:input (merge {:name "answer"} (s/bind :answer))]
Datastar's signal-defining attributes use the colon form
(data-bind:foo); the dash form would not be recognised.
Datastar 1.0 camel-cases data-bind:<key> by default. Clojure code
conventionally names signals with kebab-case keywords and reads the
POSTed signals back by the same keyword, so force Datastar's no-op
kebab case modifier to keep the wire key unchanged.
Return an attribute map that two-way binds the named signal to the
current element.
[:input (merge {:name "answer"} (s/bind :answer))]
Datastar's signal-defining attributes use the colon form
(`data-bind:foo`); the dash form would not be recognised.
Datastar 1.0 camel-cases `data-bind:<key>` by default. Clojure code
conventionally names signals with kebab-case keywords and reads the
POSTed signals back by the same keyword, so force Datastar's no-op
kebab case modifier to keep the wire key unchanged.(event-url iid route-event)URL the browser POSTs to for an event. Public so user code can build custom Datastar expressions that target the same endpoint.
route-event is either a keyword (:save) or a structured event
vector ([:pick-day day]). The path always contains the logical
event name; structured payloads ride in a small EDN query parameter so
the server can reconstruct {:event :pick-day :payload day} without
teaching Datastar about stube metadata.
URL the browser POSTs to for an event. Public so user code can build
custom Datastar expressions that target the same endpoint.
`route-event` is either a keyword (`:save`) or a structured event
vector (`[:pick-day day]`). The path always contains the logical
event name; structured payloads ride in a small EDN query parameter so
the server can reconstruct `{:event :pick-day :payload day}` without
teaching Datastar about stube metadata.(html tree)Render hiccup tree to an HTML string.
Render hiccup `tree` to an HTML string.
(local-bind self signal)Like bind, but scopes logical signal to this component instance.
:keep #{:answer}
[:input (s/local-bind self :answer)]
The browser sends :answer-<iid>; the conversation layer lifts that
value back onto :answer before the handler runs.
Like [[bind]], but scopes logical `signal` to this component instance.
:keep #{:answer}
[:input (s/local-bind self :answer)]
The browser sends `:answer-<iid>`; the conversation layer lifts that
value back onto `:answer` before the handler runs.(on self dom-event)(on self dom-event as-kw route-event)Return an attribute map that wires a real DOM event on the surrounding element to a server-side stube event.
Two arities:
(on self :submit) ;; DOM `submit` → POST .../submit
(on self :click :as :inc) ;; DOM `click` → POST .../inc
(on self :click :as [:pick item-id])
;; handler sees :event :pick,
;; :payload item-id
The first form is the common case where the DOM event name and the
route name happen to be the same (most often :submit on a form,
:input on a text field). The second form is the right one for any
click-triggered action: a button has no inc event of its own, only
click, so we need to listen on click and route to inc
separately.
Datastar registers listeners under the colon form
(data-on:<event>); the dash form data-on-<event> is reserved for
built-in pseudo-events (data-on-intersect, …) and would be silently
ignored. data-on:submit automatically calls preventDefault, so
forms never trigger a full-page reload.
The instance id and route event live in the URL path itself —
Datastar still ships every other signal as the request body, so
two-way bindings (s/bind) keep working unchanged.
Usage:
[:form (s/on self :submit) …]
[:button (s/on self :click :as :inc) "+"]
[:button (s/on self :click :as :dec) "−"]
Return an attribute map that wires a real DOM event on the
surrounding element to a server-side stube event.
Two arities:
(on self :submit) ;; DOM `submit` → POST .../submit
(on self :click :as :inc) ;; DOM `click` → POST .../inc
(on self :click :as [:pick item-id])
;; handler sees :event :pick,
;; :payload item-id
The first form is the common case where the DOM event name and the
route name happen to be the same (most often `:submit` on a form,
`:input` on a text field). The second form is the right one for any
click-triggered action: a button has no `inc` event of its own, only
`click`, so we need to listen on `click` and route to `inc`
separately.
Datastar registers listeners under the colon form
(`data-on:<event>`); the dash form `data-on-<event>` is reserved for
built-in pseudo-events (`data-on-intersect`, …) and would be silently
ignored. `data-on:submit` automatically calls `preventDefault`, so
forms never trigger a full-page reload.
The instance id and route event live in the URL path itself —
Datastar still ships every other signal as the request body, so
two-way bindings (`s/bind`) keep working unchanged.
Usage:
[:form (s/on self :submit) …]
[:button (s/on self :click :as :inc) "+"]
[:button (s/on self :click :as :dec) "−"]Query-string key used by event-url for structured event payloads.
Query-string key used by [[event-url]] for structured event payloads.
(render-slot self slot-key)(render-slot self slot-key lookup!)Inline the hiccup of an embedded child. Inside a parent's :render,
[:section (s/render-slot self :slot/header)]
expands to whatever the :ui/site-header instance currently renders,
and Chassis serialises both layers in one pass. No HTML escaping is
needed because we hand back hiccup, not a pre-rendered string.
The lookup arrow is:
slot-key → (:instance/children self)
→ child instance id
→ (instance *conv* child-iid)
→ child component definition's `:render`
Throws if *conv* is unbound or the slot is unknown. Returns the
default hidden placeholder when the child component has no :render
of its own.
Inline the hiccup of an embedded child. Inside a parent's `:render`,
[:section (s/render-slot self :slot/header)]
expands to whatever the `:ui/site-header` instance currently renders,
and Chassis serialises both layers in one pass. No HTML escaping is
needed because we hand back hiccup, not a pre-rendered string.
The lookup arrow is:
slot-key → (:instance/children self)
→ child instance id
→ (instance *conv* child-iid)
→ child component definition's `:render`
Throws if `*conv*` is unbound or the slot is unknown. Returns the
default hidden placeholder when the child component has no `:render`
of its own.(root-attrs self & attr-maps)Return an attribute map carrying self's instance id plus any other
attribute maps merged in. Replaces the recurring boilerplate
(merge {:id (:instance/id self)} (s/on self :submit) {:class "x"})
with
(s/root-attrs self (s/on self :submit) {:class "x"})
The id has to be on the root element of every component so Datastar's morph-by-id can locate the frame on subsequent renders.
Return an attribute map carrying `self`'s instance id plus any other
attribute maps merged in. Replaces the recurring boilerplate
(merge {:id (:instance/id self)} (s/on self :submit) {:class "x"})
with
(s/root-attrs self (s/on self :submit) {:class "x"})
The id has to be on the root element of every component so Datastar's
morph-by-id can locate the frame on subsequent renders.(upload-attrs self)Return form attributes for a zero-JS multipart upload.
[:form (s/upload-attrs self)
[:input {:type "file" :name "file"}]
[:button "Upload"]]
(s/upload-frame self)
The hidden iframe target prevents the browser from navigating away from the Datastar shell while the server handles the multipart POST.
Return form attributes for a zero-JS multipart upload.
[:form (s/upload-attrs self)
[:input {:type "file" :name "file"}]
[:button "Upload"]]
(s/upload-frame self)
The hidden iframe target prevents the browser from navigating away from
the Datastar shell while the server handles the multipart POST.(upload-frame self)Hidden iframe target used by upload-attrs.
Hidden iframe target used by [[upload-attrs]].
(upload-target self)Stable hidden iframe target name for upload forms owned by self.
Stable hidden iframe target name for upload forms owned by `self`.
(upload-url self)URL a multipart upload form POSTs to for self.
Uploads intentionally do not use Datastar's signal POST body: browser
file inputs need a normal multipart/form-data request. The HTTP
layer turns that request back into a regular :upload-received event
for this instance and pushes any resulting fragments over the already
open SSE stream.
URL a multipart upload form POSTs to for `self`. Uploads intentionally do not use Datastar's signal POST body: browser file inputs need a normal `multipart/form-data` request. The HTTP layer turns that request back into a regular `:upload-received` event for this instance and pushes any resulting fragments over the already open SSE stream.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |