I just realized that the shadow-graft code itself could serve as a good example for the power of the DOM and CLJS. shadow-graft sort of provides the glue between the backend and frontend. I presented it in my The Lost Arts of CLJS Frontend post and its README. If you just use this library you might not realize how little code it actually is, and that the DOM does most of the work for us.

Quick Recap

On the server we generate some HTML via hiccup. We “annotate” it via graft, and potentially pass some data via EDN.

(defn server-rendered-html [req]
  (html
    [:div 
     [:h1 "Hello World"]
     (graft "grow-on-hover" :prev-sibling
       {:doesnt-need-any-data true
        :but-could #{:get "any" {:edn true}}})]))

On the CLJS client, we define the actual behavior via a defmethod.

(defmethod graft/scion "grow-on-hover" [data el]
  (.addEventListener el "mouseenter"
    (fn [e]
      (set! (.. el -style -transform) "scale(2.0)")))
  
  (.addEventListener el "mouseleave"
    (fn [e]
      (set! (.. el -style -transform) nil))))

An admittedly silly example, but the el is the actual h1 element, since that is the previous sibling of the graft itself, neatly expressed in the hiccup above. data we just ignore here, but it is the EDN data we passed.

What we end up with is the “Hello World” text on the page, which gets larger when we hover over it with the mouse. Yes, you could have done with just CSS and didn’t need CLJS at all. Coming up with examples is hard, so give me a break. ;)

Diving In

On the server we could have skipped the graft call, and just emitted the script tag it generates itself.

(defn server-rendered-html [req]
  (html
    [:div 
     [:h1 "Hello World"]
     [:script {:type "shadow/graft" :data-id "grow-on-hover" :data-ref "prev-sibling"}
      (pr-str {:doesnt-need-any-data true
               :but-could #{:get "any" {:edn true}}})]]))

We could have written this and got the same HTML. Don’t though because the data needs to be properly escaped. I did this in the eelchat example from this post since it is using Rum, which already escapes strings and ended up double encoding our HTML string otherwise. That is all there is to it on the server and can be done in any language.

On the client in the init function the library then just asks the DOM to find all the script tags with the type="shadow/graft" attribute.

 (-> (.querySelectorAll root-element "script[type=\"shadow/graft\"]") 
     (.forEach #(run-script-tag decoder %)))

It then calls the run-script-tag function for every script DOM element it finds. That in turn will get the .-innerHTML of the script tag itself and parse it via the supplied decoder, which I skipped over showing here but would be the cljs.reader/read-string function, for turning the raw text into our beloved CLJS datastructures. It then also gets the referenced DOM element via get-dom-ref, which again is just some very basic DOM interop. Together with the data and the referenced element we call our multi-method and onto the next script tag iteration.

That is it pretty much. There is some supplementary code for hot-reload and code-splitting, but we often don’t need that. As that isn’t really DOM related I’ll skip talking about it here.

Conclusion

This is not a lot of code. If you remove the comments and indent everything a little more compact you probably get 10 lines of code. This might not be the best way to do stuff, but it has worked well for me in the past. This is also nothing new, it has been done in very many variations by many people over the years.

This is also not very complex at all. You could totally remove it and come up with something that fits better for you. Prefer the HTMX+hyperscript style of annotating the DOM elements themselves? I’m confident that you can write something that looks for hx-* attributes and does stuff based on them yourself. Maybe even a :_ style hyperscript attribute parser but with a EDN twist? I just wanted the elements themselves to stay mostly clean, and do the rest in actual code.

Actually looking over this code I noticed some things to improve, but I have used this technique for 10+ years, and it has properly done its job for many millions of pageviews. It doesn’t need to be more complicated.

Server Side Rendering (SSR) with Client-Side Hydration, React Server Components (RSC) and similar are certainly interesting areas to explore, and may absolutely have use cases, but until something is “discovered” that is as simple as the above I’ll stick with that.