Datastar allows you to control a web page from the backend. HTTP responses are used to patch either the current dom or the signals present in the page.
There are 2 main ways to structure HTTP responses for Datastar:
text/html
or application/json
HTTP response to patch the DOM or
signalstext/event-stream
response and
send SSE events to patch the page.The Clojure SDK provides helpers when using the SSE option. It follows the
Architecture Decision Record
shared by all official SDKs. This ADR describes a general mechanism to
manage SSE streams called a ServerSentEventGenerator
and functions using this
SSE-Gen to send events formatted the way the Datastar expects them in the
browser.
When using the SDK you will invariably make use of 2 main namespaces, one for sending Datastar event, the other to make SSE ring responses.
starfederation.datastar.clojure.api
This is the main API of the core SDK. It provides several tools to work with SSE-Gens such as:
starfederation.datastar.clojure.adapter.XXX
The ring API provided by the adapter implementation you are using. It's main
role is to provide a ->sse-response
function that builds a ring response
tailored to your adapter.
In the following examples we'll be using
starfederation.datastar.clojure.adapter.http-kit
.
Let's start with a Datastar hello world. We start by requiring the 2 namespaces we'll need:
(require '[starfederation.datastar.clojure.api :as d*] ;; 1
'[starfederation.datastar.clojure.adapter.http-kit :as hk-gen]);; 2
We can imagine a page with the following HTML:
<div>
<button data-on-click="@get(/'say-hello')">Say hello</button>
<p id="hello-field"></p>
</div>
Here we have a button that will call the /'say-hello'
endpoint when clicked.
The handler for this endpoint would be:
(require '[some.hiccup.library :refer [html]])
(defn simple-hello [request] ;; 1
(hk-gen/->sse-response request ;; 2
{hk-gen/on-open ;; 3
(fn [sse-gen] ;; 4
(d*/patch-elements! sse-gen
(html [:p {:id "hello-field"} "Hello world!"])) ;; 5
(d*/close-sse! sse-gen))})) ;; 6
sse-gen
which is the SSE-gen for this responsepatch-elements
we send a HTML targeting the "hello-field"
elementIn the browser, when Datastar gets the patch it will morph the DOM to be:
<div>
<button data-on-click="@get(/'say-hello')">Say hello</button>
<p id="hello-field">Hello world!</p>
</div>
The previous example could have been accomplished without the use of the SDK since we only sent one patch. However, using SSE we could just as well chunk the response. Consider this handler:
(defn chunked-hello [request]
(hk-gen/->sse-response request
{hk-gen/on-open
(fn [sse-gen]
(d*/patch-elements! sse-gen
(html [:p {:id "hello-field"} "Hello"]))
(Thread/sleep 1000)
(d*/patch-elements! sse-gen
(html [:p {:id "hello-field"} "Hello world!"]))
(d*/close-sse! sse-gen))}))
Here we send 2 events. The first will morph the DOM into:
<div>
<button data-on-click="@get(/'say-hello')">Say hello</button>
<p id="hello-field">Hello</p>
</div>
The second:
<div>
<button data-on-click="@get(/'say-hello')">Say hello</button>
<p id="hello-field">Hello world!</p>
</div>
The example on the datastar homepage is build similarly to this. It helps illustrate the possibilities using SSE. We can sent multiple patches, do work between patches and we can keep the connection alive for however long we want.
Speaking of keeping the connection alive, a simple broadcast system can be implemented with the following code:
(def !connections (atom #{})) ;; 1
(defn subscribe-handler [request]
(hk-gen/->sse-response request
{hk-gen/on-open
(fn [sse-gen]
(swap! !connections conj sse-gen)) ;; 2
hk-gen/on-close
(fn [sse-gen status]
(swap! !connections disj sse-gen))})) ;; 3
(defn broadcast-elements! [elements] ;; 4
(doseq [c @!connections]
(d*/patch-elements! c elements)))
sse-gen
is added to !connections
sse-gen
from !connections
elements
to all connected browsersIn this example we do not automatically close the sse-gen
. It will be kept
alive until either the client closes the SSE connection or your code does
it somewhere else.
Long lived connections open interesting possibilities up. A common pattern when using Datastar is to keep one SSE stream opened and push updates when relevant events occurred on the server.
We can have a page setup this way:
<body data-on-load="/updates">
<div id="main">
Imagine a complex UI here
</div>
</body>
And code similar to our broadcast example:
;; Broadcasting logic
(def !connections (atom #{}))
(defn updates-handler [request]
(hk-gen/->sse-response request
{hk-gen/on-open
(fn [sse-gen]
(swap! !connections conj sse-gen))
hk-gen/on-close
(fn [sse-gen status]
(swap! !connections disj sse-gen))}))
(defn broadcast-frame! [frame]
(doseq [c @!connections]
(d*/patch-elements! c frame)))
;; Renders the whole main content of the page
(defn render-frame [state]
(html
[:div#main "Do something with the state here"])
;; The state the rendering is based on
(def !state (atom {:some-complex "state"}))
(add-watch !state ::watch
(fn [_k _ref old new]
(when-not (identical? old new)
(let [frame (render-frame new)]
(broadcast-frame! frame))))))
When this page loads, the /updates
endpoint is called setting up a long lived
SSE connection. When !state
changes we broadcast a re-render of the whole
content of the page.
Using fat updates instead of fine grained ones might seem wasteful at first. It however has several advantages:
To use compression in this example we just need to use an option of the
->sse-response
function. Our update-handler
would look like this:
(defn update-handler [request]
(hk-gen/->sse-response request
{hk-gen/on-open
(fn [sse-gen]
(swap! !connections conj sse-gen))
hk-gen/on-close
(fn [sse-gen status]
(swap! !connections disj sse-gen))
;; We add a write profile here to enable gzip compression
hk-gen/write-profile hk-gen/gzip-profile}))
For more about the compression option and this write profile concept, checkout the write profiles docs.
The examples presented here are contrived on purpose. For instance, you won't keep you app state in an atom and use a watch to broadcast changes. However these may help you get a feel for what is possible.
You can now learn more about each specific library we provide using the rest of the docs as well as the API docs.
There are already several open source projects using Clojure and Datastar that refine the patterns we presented here. I would encourage you to explore these projects, use or take inspiration from them.
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 |