Exploring a todo app with core.async

We're going to build an equivalent of the AngularJS TODO example using core.async, and a templating library, papadom, that I've written to help in this.

Clojurescript recently gained a CSP implemetation via core.async, similar to Go's channels, or CML's channels (CML also has a nice select). Bruce Haumann started exploring this with ClojureScript Core.Async Todos, and David Nolen has been looking at how to use core.async for responsive design. In this post, we'll take the TODO example, and take it a little further.

Basic Display

We'll start with just displaying a list of todo items. For this we'll need a template, so we'll just write this in HTML, and add a t-template attribute, which enables us to use mustache style templating of values to display. This doesn't use mustache sections for looping, in order to preserve valid HTML markup.

<h1>TODOS</h1>
<ul class="unstyled">
  <li t-template="todos">{{text}}</li>
</ul>

To get this to show something we'll need some code:

(ns app
  (:require
   [papadom.template :refer [compile-templates render]]))

(defn start []
  (compile-templates)
  (render {:todos [{:text "learn papadom" :done false}
                   {:text "write a papadom app" :done false}]}))

When you call app.start() from the page containing the above template, you'll see a list of two todo entries.

Adding an event

Now we have something displayed, lets add a checkbox to mark todo items as done:

<ul class="unstyled">
  <li t-template="todos">
    <input type="checkbox" t-prop="done" t-event="done"
           t-id="index" index="{{@index}}">
    <span>{{text}}</span>
  </li>
</ul>

The t-prop attribute tells the template to which data value to display as the checkbox.

The t-event attribute specifies that we want an event. When the checkbox is clicked, we will get a core.async message with a :done event type. We need to know which todo was clicked, so we use the t-id attribute to list the attributes whose values should be sent as the event data – in this case the index, which has a value based on handlebars style @index property.

Now we need some code to process the events. To do this we'll define an app function that will be passed a state atom containing a map with our todos state, and a core.async channel from which to read events. The function will loop over events, and dispatch them as required.

(defn app
  [state event-chan]
  (go
   (loop [[event event-data] (<! event-chan)]
     (case event
       :done
       (let [nv (boolean (:checked event-data))]
         (swap! state assoc-in [:todos (:index event-data) :done] nv)))
     (recur (<! event-chan)))))

When the app function receives a :done event, it will update the state atom appropriately. Now we have our state updating, we'll need to display it, which we can again do with the render function.

(defn show-state [state]
  (render "state" state))

We still need to get show-state called, and we'll arrange this in a modified start function. This will create an atom for the state, and add a watch on the atom that will call show-state.

(defn start
  []
  (let [event-chan (chan)
        state (atom nil)]
    (compile-templates)
    (template-events event-chan)
    (add-watch state :state (fn [key ref old new] (show-state new)))
    (reset! state {:todos [{:text "Learn papadom" :done false}
                           {:text "Build a papadom app" :done false}]})
    (app state event-chan))))

We've also added a core.async channel, event-chan, which we've passed to template-events to arrange delivery of the events defined in our template. We pass this channel to the app function to start processing the events.

This shows the basic structure of the application.

Adding New Todo Elements

To allow you to add new todo items, we'll add a form to our template, specifying a t-event attribute, which will cause an event to be sent when the form is submitted, with the form's input values as the event data.

<form t-event="add-todo">
  <input type="text" t-prop="text" size="30" placeholder="add new todo here">
  <input class="btn btn-primary" type="submit" value="add">
</form>

To process this new event, we'll add a case to the app function loop's case form.

:add-todo
(swap! state update-in [:todos]
       conj {:text (:text (input-seq->map event-data))
             :done false})

This uses the input-seq->map helper to convert the data from the form into a map, and we extract the :text value (defined by t-prop in the input element).

And we're done. To see a full working example have a look at the template and code in the todo example of papadom. To run the example:

git clone [https://github.com/hugoduncan/papadom.git](https://github.com/hugoduncan/papadom.git)
cd papadom/examples/todo
lein ring server

Discuss this post here.

Published: 2013-08-15

Configuration in Templates is not Configuration as Code

If you have configuration that uses template configuration files, you are practising neither configuration as code nor configuration as data. Having configuration locked away in template files reduces its visibility, and makes it hard for you to query it. It might be easier to write configuration code to use templates, but it will come back to bite you.

One of the first things I implemented in pallet was a templating mechanism, because configuration management tools use templates, right? I even built a template selection mechanism, just like Chef's.

I have come to realise however, that having configuration in template files is not particularly useful. There are three major problems you are likely to encounter. Firstly template files are not visible, secondly you can not query the data in the template files, and lastly you will need to touch multiple files to add or modify parameters.

Visibility at the point of usage is important, especially in a team environment. If you have to find the template file and look at its content when reading your configuration code, then the chances are you assume it hasn't changed, and skip the contents. Making an analogy to the world of software development, templates are like global variables in one sense. You can change the operation of a program with a global variable modified in some obscure place, and in the same way, you can change your system configuration by changing a template file, tucked away in some folder, and not visible from where you are actually calling your configuration crate/recipe.

The ability to query configuration settings allows not just finding out, for example, which directory a log file is in, but also enables you to put tools on top of your configuration data. Template configuration files suffer on two counts here - they are separate text files that require parsing to be read, and the format of each configuration file is different.

The last point concerns the flexibility of your configuration. If you have used template files, with hard coded parameter values, and you then want to modify your configuration to dynamically set one of those hard coded values, you have to modify all the specialised versions of the existing templates, and specify the value in code. You have to touch multiple files - lots of room for making typos.

My goal for pallet then, is to have all configuration supplied as arguments to crates. For most packages a hash map is sufficient an abstraction for providing the data, but when this gets too cumbersome, we'll use a DSL that mirrors the original configuration file language.

Goodbye hidden configuration!

Discuss this post here.

Published: 2010-10-04

Archive