Clidget is a lightweight CLJS state utility that allows you to build UIs through small, composable ‘widgets’.
Add the following to your project.clj
[jarohen/clidget "0.2.0"]
and the following to your CLJS :require
:
(:require [clidget.widget :refer-macros [defwidget]])
For the Clidget changelog, including breaking changes, please see the changelog.
When writing Clidget, I had the following aims:
- Do one thing, and do it well - Clidget does not aim to be an ‘all
batteries included’ framework. In particular, it leaves you free to
choose how to render DOM elements (do you prefer Hiccup-like
libraries? Mustache templating?) and how to handle events.
Clidget is good at knowing when to re-render widgets, the rest is up to you.
This means that the ease/maintainability/expressiveness of Clidget is not dependent on waiting for new versions of Clidget - if DOM libraries get better or an amazing new library is released, Clidget users get the benefits immediately!
- No Magic - ‘magic’ is great when it’s a small example (‘wow, that’s
magic! How did they do that so concisely?!’) but not so great when
the magic stops working, you stray off the beaten track, or
encounter an incomprehensible error.
Clidget uses vanilla Clojure data structures and functions, and is mostly based around standard Clojure watches on atoms. It expects you to return standard JS DOM elements (which you can pass to other JS libraries as-they-are)
- Immutability where possible - it goes without saying that
immutable values are far easier to reason about and test.
Even though the DOM is inherently mutable, the widgets that you write are functions that take in values and return a value (and can be tested as such).
- Preserve Clojure’s composability - Clojure is great at composing small libraries together (I believe it’s one of Clojure’s core strengths) so let’s ensure that you can use other libraries easily (even other JS libraries).
If you’re looking for a comparison with Om and Reagent, it can be found here.
If you want to dive straight into examples, there are two in the repo:
a simple counter (in the clidget-sample
directory), and a TodoMVC
example in the todomvc
directory.
You can run the examples by cloning the repo, cd’ing, and running
lein dev
.
The basic premise is that clidget.widget/defwidget
watches the
application state for you, and invites you to re-render the component
when any of the state changes. The values destructured in the first
param are just that - values - that you can use to build a DOM
element:
(defwidget counter-widget [{:keys [counter]} events-ch]
(node
[:div
[:h2 "counter is now: " counter]
[:p
(doto (node [:button.btn "Inc counter"])
(d/listen! :click #(a/put! events-ch :inc-counter)))]]))
You are free to use whichever DOM rendering/events handling libraries you choose (I’m not going to impose a particular style on you). If you’re stuck with where to get started with these, I highly recommend Dommy and Clojure’s own core.async!
To include a widget in the page, call it (it’s just a function!), and provide it with the atoms that it needs to watch:
(set! (.-onload js/window)
(fn []
(let [!counter (atom 0)
events-ch (doto (a/chan)
(watch-events! !counter))]
(d/replace-contents! (.-body js/document)
(node [:h2 {:style {:margin-top "1em"}}
(counter-widget {:!counter !counter} events-ch)])))))
Note the exclamation mark in :!counter
that you pass to
counter-widget
- this informs Clidget that you’re passing an
atom. (If you don’t put an exclamation mark in, Clidget will treat it
as a value - more on this later.)
To complete the example, watch-events!
then needs to watch any
events coming out of the widget, and update the state accordingly (no
Clidget here):
(defn watch-events! [events-ch !counter]
(go-loop []
(when-let [event (a/<! events-ch)]
(when (= event :inc-counter)
(swap! !counter inc))
(recur))))
The underlying format of defwidget
is:
(defwidget widget-name [state-declaration & other-param-names]
widget-body)
;; called with:
(widget-name system-state param1 param2)
defwidget
treats its first parameter specially - this is the
parameter where the widget declares the parts of the state map that
it’s interested in. The ‘other parameters’ are passed through as-is by
Clidget; feel free to use them for other values that the widget needs.
The majority of the defwidget
macro is concerned with binding the
atoms in the system-state
map to the state-declaration
; this
section aims to explain how the binding works.
Within the state-declaration
map, Clidget particularly looks for the
:keys
and :locals
keys (the :locals
key will be covered later).
Think of the :keys
entry the same as normal de-structuring - we’re
essentially de-structuring the system-state
map - but with extra
state-watching functionality. For each symbol (let’s say ‘counter’),
Clidget will:
- look up the value in the system-state map. If it finds a ‘:counter’ entry, it will bind the ‘counter’ variable to the value provided.
- If there’s no value, it’ll look up an atom in the system-state map, prefixed with an exclamation mark (e.g. for ‘counter’, it will look up ‘:!counter’). If it finds an atom, it will assume that the appearance of the widget depends on the current value of the atom. The widget will be re-evaluated every time the atom changes value, and the variable will be bound to the new value of the atom.
- If it can’t find either a value under ‘:counter’, or an atom under ‘:!counter’, then ‘counter’ will be nil in the widget body.
Using the example above, we can now see how the ‘counter’ is bound:
(defwidget counter-widget [{:keys [counter]} events-ch]
(node
[:div
[:h2 "counter is now: " counter]
[:p
(doto (node [:button.btn "Inc counter"])
(d/listen! :click #(a/put! events-ch :inc-counter)))]]))
;; using the counter-widget:
(let [!counter (atom 0)
events-ch (a/chan)]
(d/replace-contents! (.-body js/document)
(counter-widget {:!counter !counter} events-ch)))
For a complete example, have a look at the ‘clidget-sample’ demo application.
In the above snippet, counter-widget
is just a function, and so it
can be called with test parameters, like any other CLJS function.
Because defwidget
looks for values in the system-state map before
looking for atoms, we can pass a value for the counter, and see what
the DOM element would look like:
(defwidget counter-widget [{:keys [counter]} events-ch]
(node
... as before ...))
;; In Chrome, this outputs a DOM tree in the developer console.
(js/console.log (counter-widget {:counter 4} nil))
We can also mock an events channel to test the events, if need be
Widgets occasionally need to keep local state - for example, a widget that can be edited in-place needs to store the state of whether it is currently being edited or viewed.
Clidget handles this using a ‘locals’ map, again declared in the first
parameter to defwidget
. When specifying a local atom, you also
specify an initial value, as follows:
(defwidget todo-item-widget [{:keys [editing? !editing? todo]
:locals {:!editing? (atom false)}}
...]
(node
[:li
(if editing?
(doto (node (node-when-viewing ...))
(d/listen! :dblclick #(reset! !editing? true)))
(node (node-when-editing ...)))]))
Here we declare !editing?
as a local atom, with a default value of
false. We specify both editing?
and !editing?
in the :keys
map,
so that we have access to both editing?
, the current value of the
atom (to check which version of the widget we display) and
!editing?
, the atom itself (in order to set the ‘am-I-editing’ state
in the dblclick listener).
When !editing?
is reset to true, Clidget will re-render the widget,
but this time it will render the (node-when-editing ...)
node.
Clidget uses the extra parameters (i.e. values passed in the system map) that you provide to a widget to differentiate between sub-widgets. This works out fine in most cases - for example, if you have a widget that contains a list of widgets, chances are you’ll provide a widget-unique ID as an extra parameter to the sub-widget, as follows:
(defwidget todo-item-widget [{:keys [todo]}]
(let [{:keys [caption id]} todo]
(node
[:li caption])))
(defwidget todo-list-widget [{:keys [todos]}]
(node
[:ul
(for [{:keys [id] :as todo} todos]
(todo-item-widget {:todo todo}))]))
The ‘id’ here in the todo map is enough to differentiate between different todo items, so Clidget will know when to re-render each individual item.
In the rare case that the combination of extra parameters may not be
unique amongst sub-widgets, you can provide a unique (but consistent,
for any given sub-widget) :clidget/widget-key
key in the state map,
as shown in the following (very contrived) example:
(defwidget child-widget [{:keys [elem]}]
(node
[:li ...]))
(defwidget parent-widget [{:keys [coll]}]
(node
[:ul
(for [[index elem] (map vector (range) coll)]
(child-widget {:clidget/widget-key index
:elem elem}))]))
Here, we are using the index of the ‘element’ in the ‘collection’ as a disambiguator.
As mentioned above, this really should be a rare occurrence!
If you’ve made it this far through the README (congratulations!), I’d really appreciate your feedback and suggestions.
I can be reached in the traditional GitHub ways, or on Twitter at @jarohen.
Thanks!
James
Copyright © 2014 James Henderson
Distributed under the Eclipse Public License, the same as Clojure