Liking cljdoc? Tell your friends :D

dev.zeko.stube.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. 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 fragmentson, 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.
raw docstring

*base-path*clj

URL prefix for the current adapter mount. Standalone stube keeps this empty, while embedders can bind it to e.g. /widget so generated Datastar URLs stay inside the host route tree.

URL prefix for the current adapter mount.  Standalone stube keeps this
empty, while embedders can bind it to e.g. `/widget` so generated
Datastar URLs stay inside the host route tree.
sourceraw docstring

*cid*clj

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.
sourceraw docstring

*conv*clj

The conversation being rendered, bound by frame/render-frame for the duration of one render 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 `frame/render-frame` for
the duration of one render 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.
sourceraw docstring

*root-selector*clj

Selector targeted by the first frame render. The shell and embedded fragment render a matching element.

Selector targeted by the first frame render.  The shell and embedded
fragment render a matching element.
sourceraw docstring

back-buttonclj

(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: conversation-level history rewind is a conversation operation, not a component-local event. It walks :conv/history (not window.history) — the browser's Back button still works independently. 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`: conversation-level history
rewind is a conversation operation, not a component-local event.  It
walks `:conv/history` (not `window.history`) — the browser's Back
button still works independently.  For wizard-style Back buttons that
answer a parent with a sentinel, keep using
`(s/on self :click :as ...)` from that component.
sourceraw docstring

back-urlclj

(back-url)

URL the browser POSTs to for the conversation-level Back action.

URL the browser POSTs to for the conversation-level Back action.
sourceraw docstring

bindclj

(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.
sourceraw docstring

event-urlclj

(event-url target route-event)

URL the browser POSTs to for an event. Public so user code can build custom Datastar expressions that target the same endpoint.

target is either a bare instance-id string or an instance map.

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.

`target` is either a bare instance-id string or an instance map.

`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.
sourceraw docstring

halos-js-urlclj

(halos-js-url)

URL for the optional halos script in the current mount.

URL for the optional halos script in the current mount.
sourceraw docstring

htmlclj

(html tree)

Render hiccup tree to an HTML string.

Render hiccup `tree` to an HTML string.
sourceraw docstring

instance-idclj

(instance-id self)

Return the :instance/id carried on self, or throw with a clear message if the value is missing.

Helpers passing instance ids out into pure rendering code should reach for this rather than destructuring :instance/id directly — the framework owns the wire shape, and the public name is the stable seam.

Return the `:instance/id` carried on `self`, or throw with a clear
message if the value is missing.

Helpers passing instance ids out into pure rendering code should reach
for this rather than destructuring `:instance/id` directly — the
framework owns the wire shape, and the public name is the stable seam.
sourceraw docstring

local-bindclj

(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.
sourceraw docstring

local-signalclj

See [[dev.zeko.stube.conversation/local-signal]].
sourceraw docstring

onclj

(on self dom-event)
(on self dom-event as-kw route-event)
(on self dom-event as-kw route-event modifiers)

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) "−"]
[:input  (s/on self :input :as :search {:debounce "300ms"})]

The optional 5-arity modifiers map produces Datastar event modifiers in the attribute name (data-on:input__debounce.300ms). See on-target for the modifier rules.

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) "−"]
    [:input  (s/on self :input :as :search {:debounce "300ms"})]

The optional 5-arity `modifiers` map produces Datastar event
modifiers in the attribute name (`data-on:input__debounce.300ms`).
See [[on-target]] for the modifier rules.
sourceraw docstring

on-mountclj

(on-mount self label expr)

Return a Datastar data-init expression only before self is rendered.

Use this with preserve to construct a third-party widget once, then let later stube renders update the host element's attributes without re-running the widget constructor.

Return a Datastar `data-init` expression only before `self` is rendered.

Use this with [[preserve]] to construct a third-party widget once, then
let later stube renders update the host element's attributes without
re-running the widget constructor.
sourceraw docstring

on-parentclj

(on-parent self dom-event)
(on-parent self dom-event as-kw route-event)
(on-parent self dom-event as-kw route-event modifiers)

Like on-target, but routes the event to self's parent instance.

[:button (s/on-parent self :click :as [:open note-id]) "Open"]

Equivalent to (on-target (:instance/parent self) …), but the public name lets pure render helpers ride one stable seam instead of reaching for the instance-map's keys. Use this for the recurring pattern of a child rendering controls whose semantics belong to the parent (close button on a card, link inside a row that opens something in the owning desk, etc.).

Like [[on-target]], but routes the event to `self`'s parent instance.

    [:button (s/on-parent self :click :as [:open note-id]) "Open"]

Equivalent to `(on-target (:instance/parent self) …)`, but the public
name lets pure render helpers ride one stable seam instead of
reaching for the instance-map's keys.  Use this for the recurring
pattern of a child rendering controls whose semantics belong to the
parent (close button on a card, link inside a row that opens
something in the owning desk, etc.).
sourceraw docstring

on-targetclj

(on-target target dom-event)
(on-target target dom-event as-kw route-event)
(on-target target dom-event as-kw route-event modifiers)

Like on, but route the event to an explicit target instance instead of the component whose hiccup is being rendered.

[:button (s/on-target parent-iid :click :as [:open note-id]) "Open"]
[:button (s/on-target parent-self :click :as :open) "Open"]
[:input  (s/on-target target :input :as :search {:debounce "300ms"})]

target may be either a bare instance-id string or an instance map; the helper coerces it through instance-id.

The optional 5-arity modifiers map produces Datastar event modifiers in the attribute name (data-on:input__debounce.300ms). Values may be strings, numbers, keywords, or true for flag-only modifiers ({:stop true :prevent true}__prevent__stop). Map entries are sorted by key name for deterministic output; pass a vector of pairs to preserve caller order.

This is intentionally a narrow escape hatch for cross-instance controls such as links rendered inside one child that should notify a stable parent without answering/removing the child.

Like [[on]], but route the event to an explicit target instance
instead of the component whose hiccup is being rendered.

    [:button (s/on-target parent-iid :click :as [:open note-id]) "Open"]
    [:button (s/on-target parent-self :click :as :open) "Open"]
    [:input  (s/on-target target :input :as :search {:debounce "300ms"})]

`target` may be either a bare instance-id string or an instance map;
the helper coerces it through [[instance-id]].

The optional 5-arity `modifiers` map produces Datastar event modifiers
in the attribute name (`data-on:input__debounce.300ms`).  Values may be
strings, numbers, keywords, or `true` for flag-only modifiers
(`{:stop true :prevent true}` → `__prevent__stop`).  Map entries are
sorted by key name for deterministic output; pass a vector of pairs to
preserve caller order.

This is intentionally a narrow escape hatch for cross-instance controls
such as links rendered inside one child that should notify a stable
parent without answering/removing the child.
sourceraw docstring

on-unmountclj

(on-unmount self label expr)

Attach a JS expression that runs once when the host element is detached from the DOM.

Use this alongside preserve / on-mount to dispose third-party widgets cleanly:

[:div (merge (s/preserve self :editor)
             (s/on-mount   self :editor "el.cmView = new EditorView({parent:el})")
             (s/on-unmount self :editor "el.cmView?.destroy()"))]

The expression runs once, just before the host detaches, with el bound to the element (mirroring on-mount). The expression must be synchronous and idempotent; it should not emit events back to the server. Errors are logged to console and do not block the morph.

Implemented via a single document-wide MutationObserver installed by stube/preserve.js.

Attach a JS expression that runs once when the host element is
detached from the DOM.

Use this alongside [[preserve]] / [[on-mount]] to dispose third-party
widgets cleanly:

    [:div (merge (s/preserve self :editor)
                 (s/on-mount   self :editor "el.cmView = new EditorView({parent:el})")
                 (s/on-unmount self :editor "el.cmView?.destroy()"))]

The expression runs **once**, **just before** the host detaches,
with `el` bound to the element (mirroring [[on-mount]]).  The
expression must be synchronous and idempotent; it should not emit
events back to the server.  Errors are logged to `console` and do
not block the morph.

Implemented via a single document-wide MutationObserver installed
by `stube/preserve.js`.
sourceraw docstring

payload-query-paramclj

Query-string key used by event-url for structured event payloads.

Query-string key used by [[event-url]] for structured event payloads.
sourceraw docstring

preserveclj

(preserve self label)

Return attributes marking an element's children as externally owned.

[:div (merge (s/preserve self :editor)
             (s/on-mount self :editor "..."))]

stube's shell loads a small bridge that lets Datastar merge the marked element's attributes on each morph while skipping its child subtree. The label only needs to be unique within the rendered patch; use a stable keyword such as :editor or :chart.

Return attributes marking an element's children as externally owned.

    [:div (merge (s/preserve self :editor)
                 (s/on-mount self :editor "..."))]

stube's shell loads a small bridge that lets Datastar merge the marked
element's attributes on each morph while skipping its child subtree.
The label only needs to be unique within the
rendered patch; use a stable keyword such as `:editor` or `:chart`.
sourceraw docstring

preserve-js-urlclj

(preserve-js-url)

URL for stube's preserved-subtree bridge script in the current mount.

URL for stube's preserved-subtree bridge script in the current mount.
sourceraw docstring

render-slotclj

(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.
sourceraw docstring

root-attrsclj

(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. If an attr-map also contains :id, the framework id wins.

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.  If an
attr-map also contains `:id`, the framework id wins.
sourceraw docstring

sse-urlclj

(sse-url cid)

URL the shell uses to open the Datastar SSE stream for cid.

URL the shell uses to open the Datastar SSE stream for `cid`.
sourceraw docstring

ui-css-urlclj

(ui-css-url)

URL for the stock stylesheet in the current mount.

URL for the stock stylesheet in the current mount.
sourceraw docstring

upload-attrsclj

(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.
sourceraw docstring

upload-frameclj

(upload-frame self)

Hidden iframe target used by upload-attrs.

Hidden iframe target used by [[upload-attrs]].
sourceraw docstring

upload-targetclj

(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`.
sourceraw docstring

upload-urlclj

(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.
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close