fqcss icon indicating copy to clipboard operation
fqcss copied to clipboard

FQCSS: namespaced CSS classes

FQCSS: namespaced CSS classes

Clojars Project

tl;dr: conflict-free CSS classes, write CSS however you want (Clojure, SCSS, plain CSS...), write components however you want (Rum, Reagent, HTML...) -- the library doesn't care.

I developed this because I wanted to avoid conflicts between CSS classes from different namespaces. Say, you have a "header" component in a namespace, and you want to use that same name in another component because it has semantic sense (like the header of a data-table component, for example). You'd probably come up with some prefix like "data-table-header", but why bother? Isn't the namespace of the component enough for that? Now, of course you can do this:

.app_plugins_data-table_components_data-table--header {
    font-size: 2em;
    font-weight: bold;
}

But that results in three problems:

  • You'd have to refer to the long CSS classes every single time in your components (see below).
  • It's verbose, hence tiring, hence error-prone.
  • It's not very readable, because the dot means something else in CSS (that's why you'd need to use "_" or something)

Just look at this:

(defn header []
  [:div.app_plugins_data-table_components_data-table--header
    [:div.app_plugins_data-table_components_data-table--header-text "Hi"]])

Ew. There's got to be a better way! What if we could just use qualified keywords to refer to the classes of our namespace? Something like this (won't work, of course):

(defn header []
  [:div {:class [::header]}
    [:div {:class [::header-text]} "Hi"]])

That'd be great, hence fqcss was born. You declare the classes as a vector of qualified keywords in the :fqcss property, and you wrap your component with a call to fqcss.core/wrap-reagent:

(defn header []
  (wrap-reagent
    [:div {:fqcss [::header]}
      [:div {:fqcss [::header-text]} "Hi"]]))

And this is the result (assuming app.plugins.data-table.components.data-table namespace):

[:div {:class "header__-153777894"}
  [:div {:class "header-text__-153777894"} "Hi"]]

Here "-153777894" is (hash (.getName ns)), that is, the hash of the name of the namespace where the component lives.

wrap-reagent is just Syntax Sugar (tm), so you can use fqcss.core/resolve-kw instead:

(defn header []
  [:div {:class (resolve-kw ::header)}
    [:div {:class (resolve-kw ::header-text)} "Hi"]])

Now, this is nice, but how do I write the CSS? If you remember, our CSS looked like this:

.app_plugins_data-table_components_data-table--header {
    font-size: 2em;
    font-weight: bold;
}

We can rewrite it such that fqcss can replace the namespaced classes for us, like this:

.{app.plugins.data-table.components.data-table/header} {
    font-size: 2em;
    font-weight: bold;
}

This is the generated CSS:

.header__-153777894 {
    font-size: 2em;
    font-weight: bold;
}

"But that's still verbose, I want my money back", you say. I know. That's why you can do this:

{alias data-table app.plugins.data-table.components.data-table}

.{data-table/header} {
    font-size: 2em;
    font-weight: bold;
}
.{data-table/header-text} {
    color: black;
}

Note that the alias comes before the namespace, just like with clojure.core/alias. Also, they have to take the whole line.

Now, how do we get that CSS file / string / whatever processed by fqcss? Easy:

(fqcss.core/replace-css (slurp "style.css"))

That's it for the API.

Freedom!

Since fqcss processes a CSS string, you can write CSS however you want.

Since fqcss works by using (replace-kw), anyone can write (replace-kw) wrappers (like (wrap-reagent)) for any conceivable way of writing components -- hence you can write them however you want.

How do I integrate this with my project?

I'm going to explain how I do it, but all of it is up to you.

I have this project structure:

my-project
  src
    clj
      my-project
        components
          data-table.clj
    fqcss
      my-project
        components
          data-table.scss
    scss
      my-project
        components
          data-table.scss

I watch for file changes in the "fqcss" directory. Every time a file is updated or created, it is processed by fqcss and put into the same relative path under the "scss" directory:

fqcss/my-project/components/data-table.scss -> (replace-css) -> scss/my-project/components/data-table.scss

Then the SCSS watcher processes that file, then figwheel detects the change in the CSS file, then it gets loaded, then everything's good.

Here's my fqcss watcher:

(ns user
  (:require [mount.core :as mount :refer [defstate]]
            [async-watch.core :as watch]
            [clojure.core.async :refer [>! <! go close!]]
            [fqcss.core :as fqcss]))

(defn fqcss-start []
  (println "Starting fqcss")
  (let [changes (watch/changes-in "src/fqcss")]
    (go (while true
      (let [[op filename] (<! changes)]
        (when (and (clojure.string/ends-with? filename ".scss") (or (= (name op) "modify")
                                                                    (= (name op) "create")))
          (println "Processing fqcss (" (name op) ")")
          (let [new-path (clojure.string/replace filename "fqcss" "scss")]
            (println "\tSpitting to: " new-path)
            (spit new-path (fqcss/replace-css (slurp filename))))))))))

(defn fqcss-stop []
  (println "Stopping fqcss")
  (watch/cancel-changes))

(defstate fqcss
  :start
    (fqcss-start)
  :stop
    (fqcss-stop))

Pull requests welcome. This library is currently in alpha state. Expect breaking changes.

License

Copyright © 2017 Joel Sánchez

Distributed under the Eclipse Public License version 1.0