Lately there have been a lot of posts celebrating HTMX and how much “lighter” it is compared to CLJS frontends. Even an entire framework adopting it as the default. I feel like ClojureScript is often misrepresented as this SPA-only, super complex frontend technology that you should avoid. Of course Single Page Apps (SPA) especially with react based Server Side Rendering (SSR) are about as complex as it gets, but you don’t have to do that.

So, I want to highlight some techniques that seem to be lost in the debate, as most posts only seem to compare full-blown SPA with HTMX. You can absolutely build lighter frontends with CLJS. There is no doubt that trying to make everything an SPA is exponentially more difficult than something like Hiccup.

I am even going to advocate using Hiccup for the backend, as that is what I’ve been using for the last decade myself. It is certainly a gazillion times simpler than trying to wrangle SSR with react.

My Critique of HTMX

I do not like HTMX. This isn’t because the library itself is bad, it isn’t. It is however not a novel concept. The same thing has been done over and over in the last 20+ years I have been doing web development. The ideas vary but the concept of annotating the DOM in some manner, and having the JS look for it, stays the same. Heck that is what jQuery is, and it remains as one of the most used JS libs out there.

Where this falls apart is the “scaling” of the whole thing. You are basically given a library with a certain set of functionality. As long as you only need what is provided you are fine. However, as soon as you need something slightly more or different, you more and more start working around the thing. Maybe just some extra JS file, or maybe you are forced into the whole build tool setup you maybe tried to get away from in the first place. There is also the problem that these types of libs tend to grow over time. Company X might really want a certain feature, so the authors might be compelled to add it. However, given how the library is built everyone will get it now. Whether you need it or not doesn’t matter. The same of course could also work in reverse, and a feature you really want not getting adopted.

There are also some other things I don’t like, but I don’t want to go on too much of rant here since that really isn’t important.

What then?

The concept of annotating the DOM in some way is how we have done Web Development since JS was added to Browsers. That is absolutely useful and you should use it.

Nowadays, these might be called Progressive Web Apps (PWAs), but that often implies adding a bunch of other complicated stuff (Service Workers) that you might not actually need. So, I’ll try to refrain from calling things PWA, as even a simple search suggests this might be complicated.

I don’t know if there is actually an official term for what I’m trying to describe. At one point it might have been Progressive Enhancement, but then a bunch of other stuff was piled onto that, so now it means much more than I want to describe here.

A term I have been using for this is “grafting”. It is a technique in Horticulture, i.e. the art of science of growing trees (and other stuff).

Grafting is the act of placing a portion of one plant (bud or scion) into or on a stem, root, or branch of another (stock) in such a way that a union will be formed and the partners will continue to grow. The part of the combination that provides the root is called the stock; the added piece is called the scion.

I went a little overboard with the terminology in my recent tree-inspired libraries, but it fits just so perfectly I couldn’t resist.

The basic idea is to use something like Hiccup to generate your root/stock. You then annotate that tree (HTML) in certain places and the client (CLJS) can graft additional functionality to these places. Resulting in a much more dynamic tree (DOM) than you’d otherwise get.

Enter CLJS

On the surface this is exactly what HTMX does, however I want to present how to do this in CLJS in a scalable way, which takes you from tiny snippets of functionality to full-blown SPA if needed.

I will be using the shadow-graft library I wrote to do this, but at no point should you think that this is necessary. The same technique can be done in any language and without any additional libraries. This just happens to be a solution that has worked well for me for over a decade.

I’ll skip over most of what the README already describes, and instead focus on more practical examples. Since coming up with proper Examples is kinda difficult I’ll use the first HTMX example of Click to Edit, this is simple enough and there are many approaches to dealing with it. Since this is about a form, you can start simple and make it really complex. All depends on what you actually need.

Click to Edit: Take #1

So, first in all of this we need some root stock (HTML). I’ll skip over setting up a full server, and just focus on the Hiccup function used to generate our HTML.

(defn html-contact-card [req]
  (let [data (get-data-from req)]
    (html
      [:div
       [:div {:data-ref "display"}
        [:div [:label "First Name"] ": " (:first-name data)]
        [:div [:label "Last Name"] ": " (:last-name data)]
        [:div [:label "Email"] ": " (:mail data)]
        [:button {:data-ref "edit" :class "btn btn-primary"} "Click To Edit"]]
       
       [:form {:data-ref "form" :class "hidden"}
        [:div
         [:label "First Name"]
         [:input {:type "text" :name "firstName" :value (:first-name data)}]]
        [:div {:class "form-group"}
         [:label "Last Name"]
         [:input {:type "text" :name "lastName" :value (:last-name data)}]]
        [:div {:class "form-group"}
         [:label "Email Address"]
         [:input {:type "email" :name "email" :value (:mail data)}]]
        [:button {:class "btn"} "Submit"]
        [:button {:class "btn" :data-ref "cancel-edit"} "Cancel"]]]
      (graft "contact-form" :prev-sibling))))

Note that this is pretty much the HTML from the HTMX example, just with all HTMX attributes removed and hiccup-ified. I also opted to just always emit both, on simple forms such as this there really is no need to make an extra round trip to fetch the form. We can just hide what we don’t want to show initially, represented by the hidden css class.

Now, our client side code.

(defmethod graft/scion "contact-form" [opts container]
  ;; first get all the DOM parts we are interested in 
  (let [form (.querySelector container "[data-ref=form]")
        display (.querySelector container "[data-ref=display]")
        edit (.querySelector container "[data-ref=edit]")
        cancel-edit (.querySelector container "[data-ref=cancel-edit]")]
    
    ;; then hook it up
    ;; on clicking the edit button we want to hide the display and show the form
    (.addEventListener edit "click"
      (fn [e]
        (-> form .-classList (.remove "hidden"))
        (-> display .-classList (.add "hidden"))))

    ;; the reverse on cancel-edit clicks
    (.addEventListener cancel-edit "click"
      (fn [e]
        (-> display .-classList (.remove "hidden"))
        (-> form .-classList (.add "hidden"))))))

I’m deliberately trying to show how to do this with pure interop. This could be absolutely nicer with a library. Heck, there could be a helper function to do exactly what HTMX would do instead. The point is showing how to grow from absolute minimum to complex. I’d even argue that this is the simplest approach, no dependency on any library. Doing exactly and only what you tell it to.

So, the above will let us toggle between showing the contact display and the form. On submit we’ll just let the browser handle it and eventually end up with a full page reload. Not exactly perfect, but might be good enough already.

Click to Edit: Take #2

In the next step we might want to add the code to skip the full page reload, and instead handle the submit event in our code.

It is also a good point to move this into a regular reusable function. Since this is all parameterized via the :data-ref DOM markers, we can re-use this logic for pretty much any “Click to Edit” functionality we want. Nothing is coupled to exactly this form. As long as we want the basic principle we can use this function. Heck, someone could write a library for this so others can benefit as well.

;; extracted into a reusable function, which we just call from the graft
(defn click-to-edit [container]
  (let [form (.querySelector container "[data-ref=form]")
        display (.querySelector container "[data-ref=display]")
        edit (.querySelector container "[data-ref=edit]")
        cancel-edit (.querySelector container "[data-ref=cancel-edit]")]

    ;; then hook it up
    ;; on clicking the edit button we want to hide the display and show the form
    (.addEventListener edit "click"
      (fn [e]
        (.. form -classList (remove "hidden"))
        (.. display -classList (add "hidden"))))

    ;; the reverse on cancel-edit clicks
    (.addEventListener cancel-edit "click"
      (fn [e]
        (.. display -classList (remove "hidden"))
        (.. form -classList (add "hidden"))))
    
    ;; hook up the form submit
    (.addEventListener form "submit"
      (fn [e]
        ;; stop browser from handling the submit action
        (.preventDefault e)

        ;; do a PUT request instead
        (js-await [res (js/fetch (.-action form) #js {:method "PUT" :body (js/FormData. form)})]
          ;; you should probably handle errors, skipping for brevity
          (js-await [body (.text res)]
            ;; lets assume the server just replied with the result of html-contact-card
            ;; so only the HTML we previously displayed, we can just replace the content in the DOM
            (set! container -innerHTML body)
            
            ;; since we replaced the HTML above, all our references to display/edit/cancel-edit are now gone and will be GC'd
            ;; but fret not, we can just reapply the same logic to the still existing container element
            (click-to-edit container)))))))

(defmethod graft/scion "contact-form" [opts container]
  (click-to-edit container))

Not bad. We now have a simplified version of what HTMX does. Of course, we can make this much cleaner, but again, the point of this post is showing the progression of how to approach something like this. I’m skipping the server parts since I want to focus on CLJS, but they are the same you’d do in HTMX, only ever rendering basic HTML via Hiccup.

Click to Edit: Take #3

Forms are probably one of the more complex things in web development. You might want to add client side validation, dynamic fields, image uploads or whatever else. At some point your requirements may simply become too much to be handled by such a simple approach we have done above.

This is where react starts to make sense, but there is no need to replace all of our code and start a SPA from scratch. We can just write the form and let everything else still be just plain HTML generated by the server. I’ll use re-frame as the example here, again just to show that even with re-frame you don’t have to write a full SPA.

So, first lets adjust our hypothetical server since we no longer need it to generate the form for us.

(defn html-contact-card [req]
  (let [data (get-data-from req)]
    (html
      [:div
       [:div {:data-ref "display"}
        [:div [:label "First Name"] ": " (:first-name data)]
        [:div [:label "Last Name"] ": " (:last-name data)]
        [:div [:label "Email"] ": " (:mail data)]
        [:button {:data-ref "edit" :class "btn btn-primary"} "Click To Edit"]]]

      (graft "contact-form" :prev-sibling
        ;; let's pass a subset of our data to the scion, so it doesn't need to fetch it
        {:data (select-keys data [:id :first-name :last-name :mail])}))))

And then our heavily simplified Client Side.

(defn ui-contact-form []
  (let [{:keys [showing? id first-name last-name mail] :as data}
        @(re-frame/subscribe [:form-data])]

    (when showing?
      [:form {:on-submit #(re-frame/dispatch [:submit-form!])}
       [:div
        [:label "First Name"]
        [:input {:type "text" :name "firstName" :value first-name}]]
       [:div {:class "form-group"}
        [:label "Last Name"]
        [:input {:type "text" :name "lastName" :value last-name}]]
       [:div {:class "form-group"}
        [:label "Email Address"]
        [:input {:type "email" :name "email" :value mail}]]
       [:button {:class "btn"} "Submit"]
       [:button {:class "btn" :on-click #(re-frame/disatch [:cancel-form!])} "Cancel"]]
      )))

(defmethod graft/scion "contact-form" [{:keys [data] :as opts} container]
  (let [display (.querySelector container "[data-ref=display]")
        edit (.querySelector container "[data-ref=edit]")]

    ;; use data provided by server directly, no need to fetch it
    (re-frame/dispatch-sync [:initialize-form! data])

    ;; still need to hook up the click to edit button
    ;; but now instead of just showing the form we forward the event to re-frame
    (.addEventListener edit "click"
      (fn [e]
        ;; could of course handle that from inside the re-frame event handler
        (.. display -classList (add "hidden"))
        (re-frame/dispatch-sync [:show-form!])))

    ;; just initialize and render the form directly
    ;; we could of course delay this until the click above, but this is nice too
    (rdom/render [ui-contact-form] container)))

I omitted all the actual re-frame setup, event and form handling, since that is not the point of the post. I’m also not an actual re-frame user myself. The gist is to use the same “graft” technique we did above, but gradually introducing react or whatever other library you may want to use. The point of this is that this is all straightforward to integrate and grow over time.

Conclusion

I hope that the above shows how CLJS is perfectly capable of growing and adapting to any projects needs. Not everything has to be a full-blown SPA. Choose what makes sense for your project. Of course, there is absolutely nothing wrong with HTMX. It is also certainly an option to use HTMX and the approach I described together, nothing here is exclusive and that is sort of the point. The possibilities are all there, ready to use.

Yes, the initial setup is a bit more involved. Yes, you need a build step. Yes, the resulting build might be larger than stock HTMX. Yes, it will definitely require learning about the DOM and maybe some CLJS interop. These are all trade-offs, but I believe they are worth making since you end up with something that is much more flexible. I do think your time is better spent learning the DOM properly, instead of learning a library like HTMX. This is all of course very subjective. I have had 25+ years to learn about the DOM and JS, I have basically seen it all. For a beginner HTMX will almost certainly look less daunting and that is a fair assessment.

I’ll see about creating a repo demonstrating the full setup, but hopefully things are easy enough to follow without that. Update: You can find the demo in the follow-up post.

Feel free to ask me anything about the subject. I’m usually around in the clojurians Slack (@thheller in #shadow-cljs and #clojurescript), the clojurescript stackoverflow tag, ClojureVerse or ask.clojure.org and try to answer as many questions as time permits.