Article summary
The Elm architecture is a simple and straight-forward alternative to the common model-view-controller architecture, and it’s well suited to functional programming.
In brief, the Elm architecture uses a data structure to render a UI, the UI fires actions, and actions are used to update the data structure. This is the same sort of uni-directional flow that React.js uses and the one that Ember.js has been gravitating toward in place of two-way data bindings.
Thanks to the core.async library, using the Elm architecture in a ClojureScript app is very natural. Channels are excellent stand-ins for Elm’s signals, and we can use the virtual-dom library for rendering HTML just as the elm-html
library does.
Virtual DOM
We’ll need to call a few functions from the virtual-dom library, in particular createElement
, diff
, and patch
. We’ll also need its virtual DOM node constructors. Virtual-dom is a Node module, so for access to these functions from ClojureScript, I’ve done the simple thing and created a JavaScript module that gets everything we need:
return VDOM = {
diff: require("virtual-dom/diff"),
patch: require("virtual-dom/patch"),
create: require("virtual-dom/create-element"),
VHtml: require("virtual-dom/vnode/vnode"),
VText: require("virtual-dom/vnode/vtext")
}
Using Browserify, we can compile all these dependencies together into a single JavaScript file and include it in our HTML via a script
tag, making sure to load it before our ClojureScript application.
Rendering
Now that we have access to virtual-dom, we can create a function to render an actual DOM. Just like in virtual-dom’s example, we’ll keep a reference to the last virtual DOM tree to be rendered (so we can diff against it) and to the last root element (so we can patch it with updates). Since we have to keep this state between calls to our render function, it’s useful to wrap it in a closure that encapsulates these state variables. We can also use it to do the initial setup.
(defn renderer [elem]
(let [tree (atom (js/VDOM.VText. ""))
root (atom (js/VDOM.create @tree))
update (fn [f] (.requestAnimationFrame js/window f))]
(.appendChild elem @root)
(fn [new-tree]
(let [patches (js/VDOM.diff @tree new-tree)]
(reset! tree new-tree)
(update #(swap! root js/VDOM.patch patches))))))
We can then create a render function by calling renderer
with the DOM element we want to render to.
Since we’re using the Elm architecture, we need to think in signals—or, in our case, channels. Renders should happen automatically when there’s something new to render, so let’s create a function that takes a channel of virtual DOM trees and renders it any time there’s a new value.
(defn render! [trees elem]
(let [render (renderer elem)]
(go-loop []
(render (
UI
To get a channel of virtual DOM trees to render, we first need a function that takes our application model and returns a virtual DOM tree. Here's an example that simply turns the model into a text node:
(defn ui [model]
(js/VDOM.VText. (str model)))
With this function, we can use core.async's map
to turn a channel of models into a channel of virtual DOM trees.
(core.async/map ui [models])
Before we can create a channel of models, though, we need to talk about actions.
Actions
In the Elm architecture, actions are the only way changes are made to the application model, so we need a step
function that takes an action and returns a new, updated model. In this example, we'll assume the model is an integer and create actions to increment it and decrement it. Clojure's core.match library is useful here.
(defn step [model action]
(core.match/match action
:increment (inc model)
:decrement (dec model)))
Models
To create a channel of application models, we'll start with an initial model and send an updated model any time we get an action. Elm wraps this logic up in the function foldp
. Here it is in Clojure:
(defn foldp [f init in]
(let [out (chan)]
(put! out init)
(go-loop [m init
v (
We can now derive a channel of models from our step
function and a channel of actions:
(foldp step 0 actions)
Putting it All Together
Here's the setup for this example given the above pieces and parts:
(let [actions (chan)
models (foldp step 0 actions)]
(render! (core.async/map ui [models]) js/document.body))
As it is, this application renders the model, but doesn't give any way of firing actions. To see changes, put :increment
or :decrement
onto the actions channel. To follow virtual-dom's example of a second counter, add the following to the above let
block:
(js/setInterval #(put! actions :increment) 1000)
I've written a small ClojureScript library to provide render!
and foldp
, as well as offer a more natural way to create virtual DOM trees in ClojureScript. If you'd like to see a more full-fledged example of the Elm architecture in ClojureScript, check out the source code of my small calendar app.
I assume you have seen both https://github.com/jamesmacaulay/zelkova (faithful reimplementation of Elm Arch) and https://github.com/Day8/re-frame (orthoginal to Elem Arch)
Not “orthogonal”, meant parallel.
I have seen zelkova, which seems to be a Clojure implementation of Elm’s signals and signal functions, not the Elm’s model-view-action architecture as demonstrated here.
I had not seen re-frame, but it appears to be a much heavier hammer than I’m looking for.
Zelkova appears fairly complete to me – have a look at the examples.
And I’ve never had re-frame called heavy before.
I have no doubt that zelkova is sufficiently complete to implement the Elm architecture. For instructive purposes, though, I chose to stick to Clojure core and core.async. I didn’t want to distract from the elemental pattern by introducing a third-party library.
Calling re-frame heavy is, of course, unfair. I’m not familiar with it. I was merely intimidated by the 8,000-word readme and its dependence on Reagent (and therefore React).
@Mike, as cool as re-frame is, some of us don’t want to use React. What vdom is really close to is a project called Cycle.js (with vdom choosing CSP instead of Rx/FRP). I would recommend reading its site (as well as this presentation) to see why someone might choose to go down this path instead of re-frame.
Thanks for the tutorial, nice work
Eric, Thanks for vdom, I like it.
Question: do you have an example of component-reuse (one of the selling points of the Elm arch), (e.g. see counter-pair example)?
Idea: A nice next step would be the creation of a protocol with the view and step functions in vdom.elm to create ‘components’
Thanks in advance!
Thanks. A reusable component is just a function that returns a virtual DOM tree. It can be called in multiple places to include as the child of different elements.
I’m not sure I understand how you’re suggesting vdom use protocols for views and step functions. You’re welcome to file an issue on Github (https://github.com/exupero/vdom/issues) explaining in more detail.
I spent 2 hours on this (:
(defn ui [model]
(js/VDOM.VText (str model)))
Please, add dot after VText for feature readers.
And thank you for this nice post!
Thanks. Fixed.
Nice article. I generally like your approach, except that it’s not reloadable / live-codeable, because of various functions beings passed around.
You’re exactly right. Since writing this post, I’ve modified how I write the above code in order to support live coding. You can see a more up-to-date example here: https://github.com/exupero/vdom-app/blob/master/src/leiningen/new/vdom_app/src/vdom_app/core.cljs