taia.ds renders UI on the server. A component is a pure render function plus a state machine; the browser holds the component's state as Datastar signals, sends them back with every event, and morphs the server's answer into the page. Long-lived components keep an SSE stream open so the server can push.
io.github.danbunea/taia.ds {:mvn/version "0.1.0"}
The jar brings its own datastar.js (v1.0.0-RC.8) and sse-reconnect.js as classpath resources.
Your application owns the routes. There are two handlers and three route shapes; the path
parameters must be called :component, :component-id, :event (and optionally :controller).
(require '[taia.ds.datastar.interface :as ds])
["/ds"
["/nds"
["/:component/:component-id/:controller/:event" {:handler ds/handler-component-event}]
["/:component/:component-id/:event" {:handler ds/handler-component-event}]]
["/nds-connect"
["/:component/:component-id/:event" {:handler ds/handler-connected-component-connect}]]]
Around them, the middleware the handlers rely on:
| Request key | Set by | Why |
|---|---|---|
:path-params | your router | which component, instance and event |
:body-params (JSON) on POST, [:params :datastar] on GET | muuntaja / wrap-params | the signal store round-trip |
[:params :logged-user :user/id] | your auth middleware | SSE connections are keyed per user |
:app-config (optional) | you | forwarded into every component ctx |
handler-connected-component-connect is async only ([req respond raise]), so run an async
Ring adapter (ring.adapter.jetty with :async? true). Serve the assets with
(wrap-resource handler "datastar") plus wrap-content-type, and load them in your page head:
[:script {:type "module" :src "/js/datastar.js"}]
[:script {:src "/js/sse-reconnect.js" :defer true}]
The complete reference wiring is bases/examples/src/taia/ds/examples/core.clj:
about sixty lines of reitit, muuntaja and Jetty.
(ds/set-component-search-paths! ["my.app.components"])
A request for counter.counter then resolves my.app.components.counter.counter (the render fn,
named counter) and my.app.components.counter.counter-controller (a var called machine).
Prefixes are tried in order; the result is cached per component until the paths change.
Optionally, at startup, on a background thread:
(ds/warm-component-cache! ["home.home" "chat.chat"]) ; pay the namespace-load cost at boot, not on the first click
(ds/set-observer! my-metrics-fn) ; event timings + SSE open/close as plain maps
my/app/components/counter/counter.clj ; (ns my.app.components.counter.counter)
my/app/components/counter/counter_controller.clj ; (ns my.app.components.counter.counter-controller)
(ns my.app.components.counter.counter
(:require [taia.ds.datastar.interface :as ds]))
(defn counter [{:keys [component-id memory] :as ctx}]
[:div {:id component-id :data-signals (ds/ctx->data-signals ctx)}
[:p (str (:count memory 0))]
[:button {"data-on:click" (str "@post('/ds/nds/counter.counter/" component-id "/increment')")} "+"]])
(ns my.app.components.counter.counter-controller)
(def machine
{:id :counter :initial :displayed
:states {:displayed {:on {:INIT {:actions (fn [ctx _] (update-in ctx [:memory :count] #(or % 0)))}
:INCREMENT {:actions (fn [ctx _] (update-in ctx [:memory :count] inc))}}}}})
[:div {:id "c1" "data-init" "@post('/ds/nds/counter.counter/c1/init')"} "loading…"]
Datastar fires INIT when the element appears; the response (the render) replaces the element with the same id. Every click POSTs the signal store, the machine transitions, the render fn runs, the element is morphed. Read Components for the full contract, then run the examples.
Can you improve this documentation?Edit on GitHub
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 |