CSS Selector Transforms in ClojureScript June 9, 2015
Coming full circle
Lift's CSS Selector Transforms are the best thing in web development, ever.
Granted, Lift's CSS Selector Transforms are built off concepts in Enlive, Lift treats the transforms as composable components... and that means more concise, reusable code.
If you're doing client-side work, there's Enfocus and kioo, but they are very brittle and don't play well with Reagent.
So, I decided to write a bunch of web utilities for ClojureScript, single page apps.
The code is stand-alone and outputs Hiccup format HTML or it can return a DocumentFragment that can be used any old way.
The code is the first part of Dragonmark Web.
So what, and why?
So, why don't I like Enlive/Enfocus? Mostly because they are very heavy weight when it comes to writing code. And kioo has dependencies on old versions of Om and Reagent.
I want to write very concise DOM transformations and over the 5 years that Lift has had CSS Selector Transforms, we've learned a lot about how people use them.
Show me the monkey...
So, if you have some HTML:
<div><span id="dog">cat</span></div>
And you want to replace the span
with a string:
(xform html ["span" "woof"])
;; [:div "woof"]
The xform
function takes a DOM (string is parsed as HTML, a Hiccup template,
an HTML Node
) and applies a series of transformations. Each
transformation is expressed in a vector (inside the [ ]
).
A 2 element vector is interpreted as "selector" and "operation".
The selector is a CSS selector. It's passed to the browser's querySelectorAll
function, so you get all the CSS selector goodness.
A operation can be a constant. In the above example the operation is a String that replaces the selector nodes.
An operation can also be a Hiccup template or a Node:
(xform html ["span" [:div "moo"]])
;; [:div [:div "moo"]]
If the operation is nil
, the selected element is removed
(xform html ["#dog" nil])
;; [:div]
If the operation is a hashmap, the attributes of the selected node are modified:
(xform html ["#dog" {:name "cat"}])
;; [:div [:span {:id "dog", :name "cat"} "cat"]]
We can append, prepend, or remove class attributes:
(xform [:div [:span {:class "red"}]] ["span" {:class> "cat"}])
;; [:div [:span {:class "red cat "}]]
(xform [:div [:span {:class "red"}]] ["span" {:class< "cat"}])
;; [:div [:span {:class " cat red"}]]
(xform [:div [:span {:class "red blue"}]] ["span" {:class-- "red"}])
;; [:div [:span {:class " blue"}]]
In addition to modifying the attributes and replacing an Element, we can also replace the contents of an Element:
(xform html ["span" :* "Hello"])
;; [:div [:span {:id "dog"} "Hello"]]
The above example uses an alternative, 3 element for of the vector.
It in the format [selector modifier operation]
.
Modifiers can be:
:!
-- replace (the default):*
-- replace the child element:!<
-- prepend (places the node before the found node):!>
-- append (places the node after the found node):*<
-- inserts as first child of found element:*>
-- inserts as the last element of the found element
Let's see how it works:
(xform html ["span" :!< "Hello"])
;; [:div "Hello" [:span {:id "dog"} "cat"]]
(xform html ["span" :*> "Hello"])
;; [:div [:span {:id "dog"} "cat" "Hello"]]
(xform html ["span" :*< "Hello"])
;; [:div [:span {:id "dog"} "Hello" "cat"]]
If the operation is a collection, then that collection is applied:
(xform [:ul [:li {:class "x"}]]
["li" :* ["cat" [:b "meow"] "dog"]])
;; [:ul [:li {:class "x"} "cat"]
[:li {:class "x"} [:b "meow"]]
[:li {:class "x"} "dog"]]
In the above example, we create a collection of li
elements
and populate the child nodes of the li
elements with the
values.
An operation can also be a function.
We can also build a function that's applied to an incoming DOM.:
((xf "." :*> "moo") [:div "the cow sez"]) ;; append a node
;; [:div "the cow sez" "moo"]
And the transform functions can be composed:
((comp (xf "." :*> "moo")
(xf "span" "Span be gone")) [:div [:span] "the cow sez"])
;; [:div "Span be gone" "the cow sez" "moo"]
And change up classes:
((comp (xf "." :*> "moo") (xf "span" {:class "foo"})) [:div [:span] "the cow sez"])
;; [:div [:span {:class " foo "}] "the cow sez" "moo"]
Combining xf
with operations and collections, we get:
(def data ["foo" "bar" "baz"])
(xform html ["span" (map #(xf "." {:id %}) data)])
;; [:div [:span {:id "foo"} "cat"]
[:span {:id "bar"} "cat"]
[:span {:id "baz"} "cat"]]
We create a collection of functions that set the id
on the
current element ("."
). This results in 3 spans being created.
And the functions compose. So:
(xform html ["span" (map #(comp (xf "." {:id %})
(xf "." :*> %)) data)])
;; [:div [:span {:id "foo"} "cat" "foo"]
[:span {:id "bar"} "cat" "bar"]
[:span {:id "baz"} "cat" "baz"]]
For the found span
set the id
attribute and append the value
as a child node.
That's it for now
Most of Lift's CSS Selector Transforms are in the above code. It makes it easy to separate templates from logic. The code is also not dependent on Reagent or Om or any other single page technologies. However Dragonmark web works very well with Reagent.