Hot reloading CSS can be as easy as
(require .. :reload-all)
followed by
(spit css-file css)
, at least if you add a file watcher.
😀
I’m using shadow-cljs for almost everything I do. Its hot-reloading of code is so damn good that I have grown addicted to it and I now need moar hot reloading everywhere in my life. shadow-cljs also hot-reloads css files, did you know?
However, unless you hand-code the actual .css
files,
what remains is something that will rebuild them when their source code
changes. A poor, poor man uses SASS or some similar huge
dependency for this. But a rich, poor man uses data instead of text. As
a Clojurian rich man (but I am repeating myself) I use the Garden CSS library.
Garden gives me a way to express CSS in Hiccup-ish data. It also comes
with a bunch of nice CSS utilities such as unit conversion and color
manipulation, making me miss SASS even less.
Since Garden is a library that uses Clojure data and produces CSS
text from it, I can choose to let it generate CSS files, or to inject
the CSS in [:style ...]
tags in the hiccup making up my
views. Or both. I most often chose both. When injecting into style tags,
I get shadow-cljs hot reloading for it. But, in most of the projects I
am involved in, generating CSS files from Clojure data needs to happen
on the JVM/Clojure side of things, and shadow-cljs takes care of ClojureScript reloading,
not Clojure.
While a project is young I can put all my styles in the same file and rely on shadow-cljs-hooks/garden. For reasons that are unclear to me the hook does not work when the styles are split up on several files, and the project is a bit abandoned. When splitting the namespace up, I have so far been using a tedious and involved setup of Calva custom REPL commands and my not-so-reliable memory to reload my style namespaces Until I decided I had had enough of it. In theory, garden-watcher should solve it, but I couldn’t figure out how to hold it correctly.
What to do? Here, my rich, poor man’s solution:
The ingredients
Beholder, the Krell Clojure file watcher as a library
Some (very little) config
Some (very little) code
The config
In the shadow-cljs.edn build for my app:
:build-hooks [(something.some-ns.style.garden-watcher/hook {:output-to "public/css/main.css"})]
The code
(ns anteo.designer.style.garden-watcher
(:require
[anteo.designer.style.style :as style]
[garden.core :as garden]
[nextjournal.beholder :as beholder]))
(defonce !watch-ref (atom nil))
(defn- rewrite-style-sheet! [css-path {:keys [path]}]
(println "Reloading reason:" (str path))
(require '[anteo.designer.style.style :as style] :reload-all)
(println "Writing new css to:" css-path)
(spit css-path (garden/css (style/styles))))
(defn ^:export hook
{:shadow.build/stage :configure}
[{:shadow.build/keys [mode] :as build-state} {:keys [output-to]}]
(let [path "src/anteo/designer/style"]
(when-not @!watch-ref
(rewrite-style-sheet! output-to {:path path})
(when (and (not (System/getenv "CI"))
(= :dev mode))
(println "Installing watcher for path:" path)
(reset! !watch-ref (beholder/watch (partial rewrite-style-sheet! output-to) path)))))
build-state)
Basically, this makes shadow-cljs call
garden-watcher/hook
once, when it starts watching (or just
compiling). We then write the initial CSS files from the Garden data. We
also start a file watcher which will call our function for rewriting the
CSS whenever a file in our style
subdirectory changes. In
the function for rewriting the .css
file we reload the
namespaces in the style.style
namespace using a simple
(require ... :reload-all)
. This is where I spent most of
the time implementing this. I tried quite a few things that didn’t work
before realizing how easy and simple (yes, both) it actually is.
This setup forces me to to have my main style
namespace
in src/something/some_ns/style/style.cljc
, while I would
rather have it in src/something/some_ns/style.cljc
, but
that is pure preference, and a small sacrifice, considering the gains.
Because now I edit my Garden data, save and see the styles applied in
the app I am working with. And if I need something more I know exactly
where to add it. Yay!
I guess I could pack this up as a library, but do I really need to? It could be better that people facing the problem find this post, and if they like it, they cook something poor of their own from it, making them richer. Tailor fit, fits the best!
Note: You should also have a look at how the author of shadow-cljs, Thomas Heller, suggests this should be solved: Supercharging the REPL Workflow