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).

This time we’ll continue building on previous post and will integrate ClojureScript with Knockout.js.

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.

There is an excellent 20-min Knockout.js demo video on KO web site. If you completely new to KO I’d recommend to watch it first. It will make material below easier to understand, plus it’s a truly masterfully-done demo.

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->js headers)

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.

Adding Knockout bindings to HTML

Note: Code samples include only interesting parts, full source code is available below.

We need to change our default.html as follows (only relevant parts are shown):

 1 <head>
 2   <script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.2.1.js"></script>
 3   <script defer src="app.js"></script>
 4 </head>
 5 <body>
 6   <div id="content">
 7     <table>
 8       <thead><tr><th>Header</th><th>Value</th></tr></thead>
 9       <tbody data-bind="foreach: $root">
10         <tr>
11           <td data-bind="text: $data[0]"></td>
12           <td data-bind="text: $data[1]"></td>
13         </tr>
14       </tbody>
15 ...

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:

1 (go
2   (let [headers (->> "/api/echo" get-edn <! :headers (sort-by first))]
3     (ko/applyBindings (clj->js headers))))

We obtain the data and massage it in line 2. Then convert Clojure data to JavaScript using clj->js and pass it as a root binding to KO using ko/applyBindings in line 3.

And viola! Our headers are shown in a nice-looking table (with little CSS help):

table1

Now readers familiar with KO are probably shaking their head in disgust: no observables, and applyBindings should be called only once (not from some potentially reusable go-routine).

And let’s not forget about explicit clj->js call.

Fear not! we’ll take care of it right away!

Using computed observables

My first (but not final) improvement attempt is to use KO’s computed observable to do automatic clj->js translation.

KO documentation says that extenders could be a better fit, but I’m more familiar with computed observables.

First we create a helper function to build our computed observable:

1 (defn observable [val]
2   (ko/computed
3     (let [state (ko/observable (clj->js val))]
4       (js-obj
5         "read"  (fn [] (state))
6         "write" (fn [new] (state (clj->js new)))))))

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).

Now our main code looks like this:

1 (def view-model (observable []))
2 (ko/applyBindings view-model)
3  
4 (go
5   (let [headers (->> "/api/echo" get-edn <! :headers (sort-by first))]
6     (view-model headers)))

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

Let’s create a helper function to create a ko/observable tracking Clojure ref:

1 (defn observable-ref [r]
2   (let [state (ko/observable (clj->js @r))]
3     (add-watch r state (fn [obs _ _ new] (obs (clj->js new))))
4     state))

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 ref r. 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.

Our main code now changes to this:

1 (def view-model (atom []))
2 (ko/applyBindings (observable-ref view-model))
3 
4 (go
5   (let [headers (->> "/api/echo" get-edn <! :headers (sort-by first))]
6     (reset! view-model headers)))

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:

(go
  (let [headers (->> "/api/echo" get-edn <! :headers (sort-by first))]
    (doseq [h headers]
      (<! (timeout 500))
      (swap! view-model conj h)))

Then we’ll delay for a second and pretend that terrible computer virus eats our headers one at a time:

    (<! (timeout 1000))
    (while (seq @view-model)
      (swap! view-model rest)
      (<! (timeout 500)))

Now delay for another second to keep the suspense, and finally restore our headers table to calm down the user:

    (<! (timeout 1000))
    (reset! view-model headers)

Pure Clojure data manipulation code, yet KO faithfully reflects all our changes in the DOM!

Mission accomplished!

Or is it? I have tentative plans for another post in the series, but it requires a little more research. I’ll keep you posted.

UPDATE: Keeping you posted, here is the next post.

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:

var ko = {};
ko.applyBindings = function() {};
ko.observable = function() {};
ko.computed = function() {};

Just declarations, no code. And after adding it to project.clj:

:externs ["cljs/ko.externs.js"]

… we can build our optimized code, and it will work correctly:

lein cljsbuild once opt

If you are wondering how big is produced optimized app.js file (I sure was), it is 118'871 bytes.

Source code

Full source code can be found on GitHub.

If you want to build and run it locally, execute:

git clone https://github.com/Dimagog/dimagog.github.io.git -b ClojureAllTheWay3 --single-branch ClojureAllTheWay3
cd ClojureAllTheWay3
lein ring server


blog comments powered by Disqus

Published

22 July 2013
by Dmitry Kakurin

Tags