Skip to content

Commit 74dbf16

Browse files
add clay+datastar article
1 parent efad4e4 commit 74dbf16

File tree

1 file changed

+259
-0
lines changed

1 file changed

+259
-0
lines changed
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
^:kindly/hide-code
2+
(ns scicloj.clay.webserver.datastar
3+
{:kindly/servable true
4+
:clay {:title "Serving webapps from your REPL"
5+
:quarto {:author :timothypratley
6+
:description "Using Clay's new webserver features and Datastar to build a chart with realtime server-push updates"
7+
:draft true
8+
:reference-location :margin
9+
:citation-location :margin
10+
:type :post
11+
:date "2026-01-10"
12+
:category :clay
13+
:tags [:clay :workflow]}}}
14+
(:require [hiccup.core :as hiccup]))
15+
16+
;; Clay converts a Clojure namespace into an HTML page and runs a web server to display it.
17+
;; The Clay server can do more than just serve static pages: we can define
18+
;; endpoints that handle requests and dynamically render content.
19+
;;
20+
;; This page demonstrates a server-driven web application using Datastar.
21+
;; The server manages both the rendering logic and the application state, while the browser handles
22+
;; user interactions and displays the results.
23+
;;
24+
;; > "The cosmos is within us. We are made of star-stuff. We are a way for the universe to know itself."
25+
;; >
26+
;; > — Carl Sagan
27+
;;
28+
;; All the code is in this namespace.
29+
;; The server is in your REPL.
30+
31+
;; ## First light from the REPL
32+
33+
;; I'll send this namespace to Clay by calling the "Clay: Make File Quarto" command in Calva.
34+
;; If you want to follow along, you can clone this repo and do the same.
35+
;; (Let's start stargazing.)
36+
;;
37+
;; Or if you prefer to watch first, here's a screencast:
38+
39+
^:kind/video ^:kindly/hide-code
40+
{:youtube-id "k98QI-EOHJA"
41+
:iframe-width "100%"}
42+
43+
;; ## The constellation of reactions
44+
45+
;; Here's the application: a reaction counter with emoji buttons.
46+
;; When we click the buttons, the chart updates in real time, showing the count for each emoji.
47+
;; Notice how if we open this page in multiple browser tabs, the counts update across all tabs
48+
;; simultaneously—this is the server pushing updates to all connected clients.
49+
50+
^:kind/hiccup
51+
[:div.d-flex.justify-content-center.my-5
52+
[:div.card.shadow {:style {:width "260px"}}
53+
[:div.card-body
54+
;; "reactions" is a target for replacement
55+
[:div {:id "reactions"
56+
:data-init "@get('/kindly-compute/scicloj.clay.webserver.datastar/react-html');"}]
57+
;; buttons that call the backend with an emoji
58+
[:div.d-flex.gap-2.mt-3
59+
[:button.btn.btn-outline-primary {:data-on:click "@post('/kindly-compute/scicloj.clay.webserver.datastar/react-html?emoji=👍');"} "👍"]
60+
[:button.btn.btn-outline-primary {:data-on:click "@post('/kindly-compute/scicloj.clay.webserver.datastar/react-html?emoji=🎉');"} "🎉"]
61+
[:button.btn.btn-outline-primary {:data-on:click "@post('/kindly-compute/scicloj.clay.webserver.datastar/react-html?emoji=❤️');"} "❤️"]]]]]
62+
63+
;; ::: {.callout-note}
64+
;; The buttons won't do anything unless you are running the Clay server.
65+
;; To do that, clone this repo and run [Clay make](https://scicloj.github.io/clay/#setup) on this file.
66+
;; :::
67+
68+
;; When we click a button, here's what happens:
69+
70+
;; 1. The browser makes an HTTP request to the server
71+
;; 2. The server updates its state and renders new HTML
72+
;; 3. Datastar receives the HTML and patches it into the page
73+
;;
74+
;; Datastar is a JavaScript library that intelligently updates the DOM without full page reloads.
75+
;; Rather than writing JavaScript callbacks, Datastar behaviors are simply data attributes in HTML.
76+
;; The `:data-on:click` attribute on our buttons tells Datastar which endpoint to call.
77+
;;
78+
;; This approach is ideal for Clay, where we naturally express UI as Hiccup data structures.
79+
;; The server owns both the state and the rendering logic, so we can reason about the entire
80+
;; application in Clojure, but still have interactive, responsive UI on the browser.
81+
82+
^:kind/hiccup
83+
[:script
84+
{:src "https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.7/bundles/datastar.js"
85+
:type "module"}]
86+
87+
;; Datastar is a small JavaScript library that adds interactivity to our HTML.
88+
;; It watches for `data-*` attributes in the DOM and responds to user interactions by making HTTP requests to our server.
89+
90+
;; ## State at the stellar core
91+
92+
;; We keep track of reactions in an atom:
93+
94+
(defonce reactions
95+
(atom {"👍" 8, "🎉" 4, "❤️" 2}))
96+
97+
;; This state lives on the server in the REPL process, not in the browser.
98+
;; It's the central source of truth.[^state]
99+
;; We can modify it from the REPL to test or debug the application.
100+
;; Clients request this state from the server.
101+
;;
102+
;; [^state]: **State** is the current data that defines how the application looks and behaves.
103+
104+
;; ## Charting star reactions
105+
106+
;; (Now to illuminate the reactions.)
107+
;;
108+
;; Let's create a hiccup representation of the chart:
109+
110+
(defn chart [rs]
111+
(let [maxv (max 1 (apply max (vals rs)))
112+
bar (fn [[label n]]
113+
[:div {:style {:display "flex" :alignItems "center" :gap "0.5rem"}}
114+
[:span label]
115+
[:div {:style {:background "#eee" :width "150px" :height "8px"}}
116+
[:div {:style {:background "#4ECDC4"
117+
:height "8px"
118+
:width (str (int (* 100 (/ n maxv))) "%")}}]]
119+
[:span n]])]
120+
[:div {:id "reactions"}
121+
[:h3 "Reactions"]
122+
(into [:div {:style {:display "grid" :gap "0.25rem"}}]
123+
(map bar rs))]))
124+
125+
;; The top-level `div` has `id=\"reactions\"`.
126+
;; This is crucial: Datastar uses element IDs to match the updated HTML with the existing DOM element, then morphs the DOM in place.[^morph]
127+
;; When Datastar receives new HTML, it looks for matching IDs and updates the content gracefully.
128+
;;
129+
;; [^morph]: **Morph** means to smoothly transform the DOM structure without destroying and recreating elements, preserving animations and focus.
130+
131+
;; ## Signals from the cosmos
132+
133+
;; We need the Clay server to listen for the Datastar request:
134+
135+
(defn ^:kindly/servable react-html [{:keys [emoji]}]
136+
(-> (if emoji
137+
(swap! reactions update emoji (fnil inc 0))
138+
@reactions)
139+
(chart)
140+
(hiccup/html)))
141+
142+
;; The `:kindly/servable` metadata marks this function as a remote endpoint that Clay will expose.[^servable-feature]
143+
;; The function is called with the emoji parameter from the URL, updates the shared state atom,
144+
;; and returns the new chart as HTML.
145+
;; (Each click is a signal received.)
146+
;;
147+
;; [^servable-feature]: See the [Clay documentation](https://scicloj.github.io/clay/clay_book.webserver.html) for details on `:kindly/servable` endpoints.
148+
149+
;; ::: {.callout-note}
150+
;; **Naming convention matters:** The function name ends in `html`.
151+
;; Clay uses this naming convention to determine whether to serve the result as HTML (not JSON).
152+
;; This keeps the response as a raw HTML fragment that Datastar can inject directly into the page.
153+
;; :::
154+
;;
155+
;; When the browser receives this HTML, Datastar finds the element with `id="reactions"` in both
156+
;; the old and new HTML, and morphs the DOM in-place.
157+
;; This means animations and focus are preserved rather than destroying and recreating the entire element.
158+
159+
;; ## Broadcasting to the cosmos
160+
161+
;; So far the client pulls data when a button is clicked.
162+
;; What if we want the server to push updates to all connected clients whenever the state changes?
163+
;; (Broadcasting to all the stars.)[^sse]
164+
;; For that we need a channel from server to client: Server Sent Events (SSE).
165+
;; The [Datastar Clojure SDK](https://github.com/starfederation/datastar-clojure)
166+
;; provides helpers to set up and manage these push connections.
167+
;;
168+
;; [^sse]: **Server Sent Events (SSE)** is a standard for opening a one-way channel from server to client, perfect for pushing real-time updates.
169+
170+
;; We track all open SSE connections in a set.
171+
;; When a client connects or disconnects, we add or remove it from this set.
172+
173+
(defonce *ds-connections
174+
(atom #{}))
175+
176+
;; This handler creates an SSE connection.
177+
;; The `:kindly/handler` metadata tells Clay to expose this function as an HTTP endpoint that takes a request and returns a response.[^handler-feature]
178+
;; The `->sse-response` helper returns the proper SSE response structure.
179+
;; When a client connects, we store its SSE generator.
180+
;; When it closes, we remove it.
181+
;;
182+
;; [^handler-feature]: This `:kindly/handler` feature is available in Clay 2.0.4 and later. See the [Clay documentation](https://scicloj.github.io/clay/clay_book.webserver.html) for details.
183+
184+
(require '[starfederation.datastar.clojure.adapter.http-kit :as d*a])
185+
186+
(defn ^:kindly/handler datastar [req]
187+
(d*a/->sse-response
188+
req
189+
{d*a/on-open (fn [sse-gen] (swap! *ds-connections conj sse-gen))
190+
d*a/on-close (fn [sse-gen _status] (swap! *ds-connections disj sse-gen))}))
191+
192+
;; Broadcast sends HTML to all connected clients.
193+
;; Datastar on the browser side will receive these patches and update the DOM.
194+
195+
(require '[starfederation.datastar.clojure.api :as d*])
196+
197+
(defn broadcast-elements! [elements]
198+
(doseq [c @*ds-connections]
199+
(d*/patch-elements! c elements {:id "reactions"})))
200+
201+
;; Now we create a watch callback that will broadcast the chart whenever the state changes:
202+
203+
(defn broadcast-chart! [_k _r _a b]
204+
(broadcast-elements! (hiccup/html (chart b))))
205+
206+
;; Attach a watch to the reactions atom.
207+
;; Whenever the atom changes, this callback fires, renders the new chart as HTML, and broadcasts it to all connected clients.
208+
209+
(defonce watch-reactions
210+
(add-watch reactions :chart #'broadcast-chart!))
211+
212+
;; Clojure watches are a built-in mechanism for reacting to state changes.[^watch]
213+
;; They take a reference type (atom, ref, agent, etc.) and a callback function that fires
214+
;; whenever that reference is modified.
215+
;; This is how we push updates to all clients automatically.
216+
;;
217+
;; [^watch]: **Watch** is a Clojure mechanism that lets you react to changes in a reference type by calling a function whenever it changes.
218+
219+
;; For example, we can test this from the REPL:
220+
221+
(comment
222+
(swap! reactions update "👍" inc))
223+
224+
;; When you evaluate this, the watch callback fires, broadcasts the updated chart as HTML, and all open browser windows receive and display the updated chart immediately.
225+
;; This demonstrates the full loop: REPL state change → watch callback → broadcast to clients → DOM updates in real time.
226+
227+
;; Now our page should start listening to the SSE stream:
228+
229+
^:kind/hiccup
230+
[:div {:data-init "@get('/kindly-compute/scicloj.clay.webserver.datastar/datastar')"}]
231+
232+
;; The `data-init` field tells Datastar to connect to the Server Sent Events handler,
233+
;; and listen for subsequent messages.
234+
;; SSE (Server Sent Events) is ideal here because we only need one-way communication from server to clients.
235+
;; Unlike WebSockets, SSE uses standard HTTP and requires minimal setup.
236+
;; Unlike polling, it's efficient—the server pushes updates only when the state changes.
237+
238+
;; Now when we open multiple pages and click in one,
239+
;; we can see the data update in all the pages.
240+
;; (A cosmos of observers.)
241+
242+
;; ## Reflecting on the skies
243+
244+
;; All the code for this page is written in Clojure.
245+
;; The entire application—state management, rendering, and interactivity—lives in Clojure, not split between server and client JavaScript.
246+
;;
247+
;; This style of web development can be very productive.
248+
;; Clay makes it fast to prototype rich, stateful applications using only Clojure and server-side rendering.
249+
;; Datastar eliminates the need for client-side state management and event handling.
250+
;; (One star, many observers.)
251+
;; The result: you reason about the whole application in one language and get interactive,
252+
;; responsive UX without touching JavaScript.
253+
;; (And so we return to the stars.)
254+
;;
255+
;; > "For small creatures such as we, the vastness is bearable only through love."
256+
;; >
257+
;; > — Carl Sagan
258+
;;
259+
;; If you love Clojure, I think you'll love Clay's new server features.

0 commit comments

Comments
 (0)