This is the third part of my “Lost Arts” Series. Starting with The Lost Arts of CLJS Frontend, expanding to Applying the Art of CLJS Frontend and probably ending in this.

The goal here is to share some tips that might help you on your journey to truly master CLJS Frontend, Browser-based Frontend specifically. Mastery requires Understanding, and the absolutely essential piece to understand is the DOM. Some JS helps, but thanks to CLJS you probably never need to actually write any yourself.

When I say essential I mean it. There is absolutely no doubt in my mind that you need to learn about the DOM. This is regardless of your chosen tech stack of choice. You need to know about it when using react, just as much as when using htmx. Some may tell you that you don’t, but IMHO that implies that you’ll never truly master frontend. I also think that it is much easier to understand any of those libs with a basic understanding of the DOM.

Of course there are far more things in a browser based Frontend than the DOM (e.g. CSS), but you have to start somewhere.

Disclaimer

I’m not saying that absolutely everyone needs to learn about the DOM. If you have chosen another field to master, then this post is not for you. You don’t need to understand the DOM if you are just using the frontend as a tool, and don’t want to spend actual time there. It is absolutely fine to just use some libraries and wire together some pre-made stuff to get the job done and don’t really care how.

I don’t fault anyone for not wanting to learn this stuff. It is extremely complex, too complex even, and will take a huge chunk of your life to master, but I write this for people genuinely curious about frontend with CLJS and all related technologies. It is not all terrible after all.

Browsers get a bad reputation, and many devs often scoff at frontend, implying that it is inferior technology. It is not, and don’t let anyone tell you otherwise. Everyone uses a Browser and gets value from it, regardless of what they may say.

Yes, there is a gigantic pile of historic baggage in browsers and some very weird choices made. Browsers are far from perfect, but compared to what I have seen over my long career, have never been in a better place. The platform is truly marvelous and I challenge anyone to show me something with the same reach and durability. It runs everywhere, and most websites written 20+ years still work, if they are actually still online that is.

I do not agree with people that want to throw it all away and start from scratch. I’m also not saying that there is no value in doing that. I’m aware of my bias, but the platform is here to stay, and we might as well take advantage of it. I’d even argue that it is required learning before you can build something “better”, even it that is just to know what not to do. Browsers truly are Jack of all trades, master of none. There are absolutely use cases where it doesn’t make sense to use it, but that is not what I’m here to talk about.

With all that out of the way, let’s get going.

DOM = Document Object Model

In the previous posts I showed a few pieces of code working with the DOM, but never explained how that all works in detail. Instead of repeating all the MDN DOM docs I’ll try to present everything in a CLJS context. Do however note that the MDN web docs are an absolutely fantastic resource for anything browser related, definitely do check them out. I use them as a reference constantly.

The DOM is the HTML document presented via a JS API. It isn’t actually JS, but the JS lets us talk to the native parts of the browser. Since I don’t want to bore you with much actual HTML, I’ll be using our trusty hiccup syntax instead. All it represents is a tree, where elements may have zero or more children but only ever one parent.

[:html
 [:head
  [:title "Hello World"]]
 [:body
  [:h1 "Hello World"]]]

So, here the html element has head and body as its children. The title element in the head element has one Text node as its only child. The same goes for the h1 element in the body.

So, when the server sends the above hiccup as actual HTML and the browser will use it to construct the DOM and render the page. We can then interact with the DOM via CLJS code, and modify the page to our hearts content.

Traversing and updating the DOM

Probably the first essential skill is learning how to actually access anything and how to traverse the tree. Since this all may become a bit abstract, you maybe want to follow along in a REPL.

If you don’t have a REPL setup yet, we can get one going quickly by running npx create-cljs-project dom-intro, then cd dom-intro and npx shadow-cljs browser-repl. Or the clj purists instead can do

mkdir dom-intro
cd dom-intro
echo '{:deps {thheller/shadow-cljs {:mvn/version "2.24.1"}}}' > deps.edn
echo '{}' > shadow-cljs.edn
clj -M -m shadow.cljs.devtools.cli browser-repl

After a short while it should end up opening a browser tab for you with the shadow-cljs REPL connected to it. I recommend opening the Browser Devtools for that tab, so that you can see the console and our changes reflected in the Elements tab. The HTML generated by shadow-cljs isn’t quite what we had above, but close enough.

It is important to note that the DOM is actually much more than just the HTML represented 1:1. So, for example we can access and change the title of the browser tab like this.

cljs.user=> js/document.title
"shadow-cljs browser-repl"
cljs.user=> (set! js/document -title "Hello from the REPL")
"Hello from the REPL"

You should see the title in your actual browser tab changing. We can also get the actual DOM title element like this

cljs.user=> js/document.head.firstChild
#object[HTMLTitleElement [object HTMLTitleElement]]

document.head is the shortcut to access the head element, and as described above the first and only child is the title element.

The result looks a bit like gibberish because it is, but the essential clue here is HTMLTitleElement, which you can look up via MDN: HTMLTitleElement. It doesn’t do anything beyond setting the browser tab title, and we already went over how to change that. Onto something more useful.

We can also reference elements by their id, so the [:div#app] or [:div {:id "app"}] from hiccup. The browser-repl happens to have that element, so lets get it.

cljs.user=> (js/document.getElementById "app")
#object[HTMLDivElement [object HTMLDivElement]]

Now, so we got our HTMLDivElement but for our purposes let us just reference the generic Element, since they all inherit from that and have mostly the same properties and methods. MDN lists all the properties, methods and events on the left. There you’ll also find the first interesting property: innerHTML.

We can use that to get and create some HTML our DOM. First lets store a reference to our #app div in the REPL, since we are going to use that for a bit.

cljs.user=> (def div (js/document.getElementById "app"))
#'cljs.user/div

Then lets check the current HTML contents of that div:

cljs.user=> (.-innerHTML div)
""

As expected the div doesn’t have any content, so an empty string is all we get. As a reminder the .-innerHTML is how we access JS properties in CLJS. It directly translates to div.innerHTML JS. js/document.body.innerHTML might be more interesting for now.

Let us add some content next:

cljs.user=> (set! div -innerHTML "Hello World")
"Hello World"
;; little alternate syntax with the exact same meaning
cljs.user=> (set! (.-innerHTML div) "Hello World")
"Hello World"

Which you should again see reflected in the Browser. set! is how we set properties in CLJS, so this translates to div.innerHTML = "Hello World";.

cljs.user=> (.-innerHTML div)
"Hello World"

As expected the #app div now contains this text. But it is still just text, so let us add some HTML instead.

cljs.user=> (set! div -innerHTML "<h1>Hello World</h1>")
"<h1>Hello World</h1>"

Once again the browser should reflect this change and now show the text slightly bigger, since the browser has some CSS styling for h1 elements. innerHTML is quick, but often impractical since any adjustments will delete all children it previously had, and then recreate the new structure. A very blunt tool, and sometimes we want to be more precise.

Luckily the DOM is full of neat little helper functions, so we can call things like insertAdjacentHTML.

cljs.user=> (.insertAdjacentHTML div "beforeend" "<h2>Magic!</h2>")
nil

As you can see in the browser or the REPL, we now have a new h2 element, and our h1 was preserved.

cljs.user=> (.-innerHTML div)
"<h1>Hello World</h1><h2>Magic!</h2>"

We can do something similar without the HTML string, and just create the element directly.

cljs.user=> (.appendChild div (doto (js/document.createElement "h3") (set! -innerHTML "Crazy!")))
nil

Again this reflected in the browser, and we can see it via innerHTML too. Slightly more verbose overall, but ultimately the most flexible. appendChild adds the new element as the last child, insertBefore is another essential method for inserting stuff in specific places.

cljs.user=> (.-innerHTML div)
"<h1>Hello World</h1><h2>Magic!</h2><h3>Crazy!</h3>"

Another essential thing to remember is that Elements can only have one parent element, which implies they can only be in one place in the DOM. It is totally fine and common to move things around, but don’t be surprised when it is gone from the old place.

cljs.user=> (.appendChild div (.-firstChild div))
#object[HTMLHeadingElement [object HTMLHeadingElement]]
cljs.user=> (.-innerHTML div)
"<h2>Magic!</h2><h3>Crazy!</h3><h1>Hello World</h1>"

I’ll stop showing REPL prompts now, but feel free to follow along.

I started this section talking about tree traversal, so lets get back to that. If you have an element already, like our div, you can traverse using the properties the DOM already provides. We already used .firstChild before, so using (.-firstChild div) will get us the h1. (aget (.-children div) 0) would to the same. children is an array-like property to get all the children of an element. This works nicely with CLJS aget. We can also use (.-nextSibling div) or (.-previousSibling div) to traverse “right” or “left” from our current element, and (.-parentElement div) to go “up”.

These are all very useful, and you’ll most certainly be using them at some point. However, the real powerhouses are querySelector and querySelectorAll. They let us access elements in a more declarative manner, which is more convenient and forgiving. The selector syntax is pretty powerful, but the basics I’ll highlight here can take you very far.

;; find the h3 in your div
(.querySelector div "h3")
;; find the same h3 from the document.body
(.querySelector js/document.body "h3")
;; find by id
(.querySelector js/document.body "#app")
;; find [:div.foo], doesn't exist yet
(.querySelector div ".foo")
;; find by attribute [:div {:data-ref "foo"}], also doesn't exist yet
(.querySelector div "[data-ref=foo]")
;; finds all elements with a :data-ref attribute, empty for now
(.querySelectorAll div "[data-ref]")

querySelector always returns nil or the first match in a depth-first search. querySelectorAll finds all of them, and returns an array-like, which means we can use aget and doseq and so on from CLJS. It’ll be empty if nothing matched. You already learned above how to create all these missing elements if you want a little exercise.

Quick Recap: We went over basic traversal of the DOM, updating HTML, creating elements, setting properties via set! and getting them via the (.-property thing) CLJS syntax. You can actually get very far with just these basics, true mastery comes from practice and studying MDN.

Events make everything interactive

I already used events in my previous posts, and they are the way the browser informs us about things happening. Most of them will come from something the user does, like clicking, scrolling, moving the mouse, pressing keys on the keyboard and so on. There is pretty much an event for everything, again MDN has you covered.

You’ve already seen how to add them.

(.addEventListener element-to-listen-to "the-event-name"
  (fn [the-event-that-was-dispatched]
    (js/console.log "my-event-fired" the-event-that-was-dispatched)))

The structure is always the same, doesn’t matter if you use the "click", "mousemove" or "keypress" events. And there really isn’t much more to say about them. Stuff happens all the time in the browser, you listen to the things you are interested in and act when triggered.

In general events (e.g. click) fire from the bottom up. So, when you click the h1 it’ll first check if there is an event handler on that h1. If not it’ll do the same for the containing div, and then the body, or whichever other structure you might have.

Event handlers may stop this traversal if needed via (.preventDefault e) and/or (.stopPropagation e). preventDefault is about stopping default browser behavior, such as navigating when clicking an a tag, or a form submit. It’ll not stop the traversal on its own, but stopPropagation will. You might need both. addEventListener also has an optional third argument, which might be useful in some instances.

It is also worth noting that there can be more than one listener for a particular event. Sometimes you’ll want to removeEventListener, but I’ll skip that here.

Word of Warning

As you might have noticed this is represented as an object-oriented mutable API. It doesn’t look like regular other CLJS code might. Here be dragons, but they are pretty tame in the end.

One sometimes scary thing to remember, that has driven me crazy when debugging, is that all “collection” types are also mutable. So, be careful if you (.querySelectorAll ...) or (.-children ...) something and modify that tree while traversing it, e.g. .remove an element or even adding one. It can save your sanity to use (into [] (.querySelectorAll ...)) first, since CLJS vectors do not behave that way. Not always required, but a useful thing to know.

This is all mutable so adding an event handler twice will mean everything will happen twice. That may not always be noticeable at first, but it’ll certainly come back to haunt your performance in some way. Hygiene is important, and you do need to clean up after yourself.

react & co

Of course, we have to mention react at some point. As you might have noticed the above may all appear a bit manual, and in fact react is one abstraction that wants to hide most of this from you. Instead, you describe what DOM should look like and react will figure out how to get it there for you. You get a bit more declarative syntax and less JS interop, which is fantastic but not free. Of course react is only one of many abstractions in this area, each with their own set of trade-offs.

Learning when to take advantage of them, and learning when NOT to is essential. Unfortunately, I cannot give you any specific advice here as the answer depends on so many things. I guess it takes some experience to figure this out. In general, I start using abstractions, like react, or my own tech, as soon as something goes beyond handling a couple event handlers or DOM elements. I have also gone back from using an abstraction to just plain DOM and interop, or written both and then decided.

Conclusion

The main lesson is that the more familiar you get with Tech X, the more you’ll see the trade-offs and how to take advantage of them. This counts for plain DOM, react or whatever else. This is also why I emphasized initially why I think learning about the DOM is so important. It is the basis for everything in the browser and I only scratched the surface here.

If you only know react, how are you ever gonna chose something that might be a better fit? In the end the good old “if all you have is a hammer, everything looks like a nail” applies. The strength of react lies its ecosystem, not the library itself. It can of course be fantastic not having to build something if it already exists, and nobody is saying that you should write everything from scratch.

Personal Note: My experiences with some react libs have lead me personally to abandon it entirely. I feel like we never got such an ecosystem in CLJS since we just adopted react so thoroughly. I can’t be the only one wanting more CLJS and less react?

I hope to have left you with some useful DOM basics to get you started in whatever you may want to do. I hope it might inspire some to write more DOM based CLJS libraries instead of yet-another-react-wrapper. I’ll definitely be writing some more myself.

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.