Clojure All The Way - Knockout Part 3 of mini-series

This is a third article in mini-series. The goal of mini-series is to create a Client-Server environment where Clojure data structures are pervasive and we don’t have to deal with JSON and JavaScript objects in Clojure/ClojureScript land (as much as possible).

Where we are

We start where we left off in the previous post: we have a Server that implements echo HTTP API and calling it returns edn-encoded data.

And we have a Client capable of calling this API, receiving data, and edn-decoding it. But it displays the data (headers of our HTTP request) in a very primitive way by simply setting text of DOM element.

Our Goal = ClojureScript + Knockout

We’d like to nicely format received headers as HTML table. We can of cause write ClojureScript code to build HTML for it using Hiccup-like library. But where is fun in that!

It would be more entertaining to use some JavaScript data-binding library. First, because I like MVC-based approach. But more importantly because it will allow us to explore ClojureScript integration with existing JavaScript framework.

We’ll use Knockout.js(abbreviated KO for the rest of this post) as it happens to be my favorite MVVM framework for JavaScript. And also KO claims that it is very smart and highly optimized in minimizing DOM modifications, so I expect it to be efficient as well.

Naive Knockout integration

Let’s see what is the minimal effort required to integrate our code with KO, and then we’ll improve on it.

Preparing input data

First we need to massage our data: headers are represented as a map but KO requires it to be array’ish collection if we want to use its foreach binding. Easy enough: map converted to seq is actually a sequence of vectors, each vector being a key-value tuple. While we at it let’s sort it by header name as well:

(let [headers(->>"/api/echo"get-edn<!:headers(sort-by first))]

Calling sort-by is the only addition to our previous code. It converts map to seq and also sorts it by first element of each tuple, i.e. by header name.

The last step is converting Clojure data to plain JavaScript data that KO understands. We do it by using clj->js function that will recursively convert our Clojure vector of vectors into JavaScript Array of Arrays:

(clj->jsheaders)

I know the whole premise of the series is to avoid such explicit conversion. Please bear with me, we’ll deal with it in following sections.

First we add reference to KO itself in line 2. Then we create a table (line 7) with static headers (line 8) and KO-bound body (line 9). The foreach in line 9 iterates over $root binding (not explained yet) expecting it to be an array and replicates its HTML body for each element while binding $data to it.

In our case each element is a key-value tuple for each header represented as a 2-element array. So tds in lines 11-12 simply extract first element for the header name and second for the value.

Applying root Knockout binding

Now to connect our ClojureScript data to KO bindings in HTML we just need to call ko/applyBindings. And our final code for this step looks like this:

ko/computed observable (line 2) takes a JavaScript object created in line 4 that specifies read and write functions. The actual state is kept in regular ko/observable created in line 3.

KO observables are functions. Calling it with no arguments returns current value and calling it with one arguments sets its value. So (state) in line 5 gets the current state and (state (clj->js new)) in line 6 sets it (after converting new to JavaScript representation).

We create our view-model in line 1 using our observable helper, and set it to empty vector first. Then we bind it in line 2, this needs to be done only once. And when we obtain our headers in line 5 we set view-model observalble in line 6 (remember - observables are functions). This in turn triggers DOM update.

Note: using global vars like view-model is not recommended in good Clojure code, but will do for our sample code.

Great!clj->js conversion is nowhere to be seen in our main code and we adhere to good KO practices, but …

What’s wrong with it?

First, getting/setting view-model by calling it as a function is not very Clojury.

Second, and more importantly, if we read (view-model) we’ll get JavaScript-converted objects, not the original. The original is “lost in translation”. Which means that if we want to modify our view model in ClojureScript we need to keep it separately somewhere.

And where do we usually keep changing data in Clojure? In refs of cause! At this point a light bulb should begin hovering above your head: the refs in Clojure have watchers, so they can notify whoever is interested when they change.

refs sound suspiciously familiar to ko/observables: both track changing state and both send notifications when change happens. Can we somehow bridge the two? We certainly can!

Using Observable refs

observable-ref takes a ref as parameter r (the ref itself, not its value!). We create a ko\observable named state in line 2 with initial value of r converted to JavaScript. Then in line 3 we add a watcher for the r.

The watcher fn takes 4 parameters, but we only care about 2: obs is our observable state and new is the new value of the refr. Then we simply convert new value to JavaScript representation and put it into obs.

And finally we return state observable from the function so that it can be passed to KO.

Notice that view-model created in line 1 is now a Clojure ref (in this case an atom). And we don’t even save the observable returned by our observable-ref helper, we just pass it directly to ko/applyBindings in line 2.

And modifying our view-model(line 6) is as simple as modifying any other Clojure atom, yet DOM immediately reflects the changes!

Isn’t it nice! Let’s have some fun with it …

Animating our table

Just for fun, let’s pretend that our Internet connection is very slow and headers are received one by one, slooowly. We can emulate this by conjing headers to view-model one at a time with delay:

One last thing: optimized builds

Unlike Steve Job’s famous “one last things” this one is boring, but important. If we try to build our ClojureScript code with :advanced optimization it will not work. This is because Google Closure compiler minifies all names in produced .js file including names like ko and observable. Certainly not what we want.

The solution is to add so-called externs file and feed it to Google Closure compiler, so it would know which names it is not supposed to touch.

Conceptually the externs file is similar to C/C++ .h headers files, or .asmmeta files for C#. Ours is called ko.externs.js and looks like this: