dom
dom copied to clipboard
town.lilac/dom
An extremely simple library for declaratively creating and updating web pages in ClojureScript.
It is a thin wrapper around google's extremely stable incremental-dom library.
Install
The library is available via git deps
town.lilac/dom {:git/url "https://github.com/lilactown/dom"
:git/sha "77de341c5e6ecb6687e03b4139a70c6f8edc49fa"}
Usage
The library exposes a $
macro that allows one to create elements that are then
diffed against the existing nodes on the page and patched accordingly.
No virtual DOM is kept in memory.
There are helpful macros that wrap $
for specific elements like div
,
button
, input
, etc. With these, the library provides a simple DSL for
constructing elements.
To create an app, you need a function that constructs these "DOM expressions,"
which will be called via patch
function with the root node of the app when
necessary. State management is left as an exercise to the reader.
API
See town.lilac.dom
docstrings.
Example
(ns my-app.main
(:require
[town.lilac.dom :as d]))
(def *state (atom {:text "bonjour"}))
(defn on-change
[text]
(swap! *state assoc :text text))
(defn app
[{:keys [text]} on-change]
(d/div
{:style {:fontFamily "sans-serif"}}
(d/input
{:style {:border "1px solid red"}
:oninput (fn [e]
(on-change (.. e -target -value)))
:value text
& (when (= "hello" text)
{:style {:border "1px solid blue"}})})
(d/div (d/text text))))
(defn render!
[state]
(d/patch (js/document.getElementById "root")
#(app state on-change)))
(add-watch *state :render (fn [_ _ _ state] (render! state)))
(render! @*state)
(comment
(swap! *state assoc :text "hi"))
How it works
Side effects may include...
incremental-dom operates via side-
effects; when you call $
or a specific DOM macro like div
via this library,
the library does some internal book keeping to track what elements contain others
via the order of open
and close
calls.
($ "div" {:style {:color "red"}} (text "hello"))
;; emits something akin to
(open "div" {:color "red"})
(text "hello")
(close "div")
The close
call will return the HTMLElement
node for the div with the HTML
Text
node as a child of it.
After the function you passed to patch
returns, it will take the tree of
elements constructed, diff the resulting tree with what is present within the
root element, and update the root with nodes that have changed.
What this means
You do not need to return an element for it to be added to the result
(defn app
[]
(d/div
(d/text "hello ")
(let [text (d/text "side effects")]
;; no return value
nil)
(d/text "!")))
(d/patch root app)
;; results in <div>"hello " "side effects" "!"</div> added to the page
Dynamic attributes
The $
macro needs to determine whether an argument passed to it is a child in
order to place the open
and close
calls correctly. The heuristic it uses is:
anything that isn't a map literal in the first position is a child. So:
($ "div" (foo) [bar] baz)
;; emits
(open "div")
(foo)
[bar]
baz
(close "div")
This complicates things when you want to pass in a dynamic map of attributes
(let [attrs {:style {:color "red"}}]
($ "div" (merge {:id "asdf"} attrs) foo bar baz))
;; OOPS! emits
(let [attrs {:style {:color "red"}}]
(open "div")
(merge {:id "asdf"} attrs)
foo
bar
baz
(close "div"))
Ideally, we would detect that attrs
is in fact a map and pass it to the open
call. However, this isn't possible to do at compile time in all cases.
Instead, the $
and other DOM element macros accept a special attribute, &
or
:&
which will merge any static attributes you pass in with ones that are
passed in dynamically.
Here is the correct code:
(let [attrs {:style {:color "red"}}]
($ "div" {:id "asdf" & attrs} foo bar baz))
;; emits
(let [attrs {:style {:color "red"}}]
(open "div" (merge {:id "asdf"} attrs))
foo
bar
baz
(close "div"))
Async
The async
macro and use
function allows you to declaratively have your view
depend on a value that needs to be fetched and cached across the network,
showing a fallback while it's loading.
Example:
(def cache nil)
(defn fetch-data
"If the data is present in the cache, fetches it and returns a promise.
Otherwise, returns the cached result."
[]
(if (nil? cache)
(-> (js/Promise. (fn [res]
(js/setTimeout #(res {:foo "bar"})
2000)))
(.then (fn [v] (set! cache v))))
cache))
(defn app
[]
(let [data (d/use (fetch-data))]
(d/div {:style {:border "1px solid blue"}}
(d/textarea
(d/text (pr-str data))))))
(patch
(js/document.getElementById "root")
(fn []
(d/div (text "hi"))
(d/async
(app)
(fallback
(d/div (d/text "loading..."))))))