dosync Archive Pages Categories Tags

ClojureScript 101

07 November 2013

While none of the ideas in core.async are new, understanding how to solve problems with CSP is simply not as well documented as using plain callbacks or Promises. My previous posts have mostly explored fairly sophisticated uses of core.async, this post instead takes the form of a very basic tutorial on using core.async with ClojureScript.

We're going to demonstrate all the steps required to build a simple search interface and we'll see how core.async provides some unique solutions to problems common to client side user interface programming.

I recommend using Google Chrome so that you can get good source map support. You don't need Emacs to have fun with Lisp. SublimeText 2 is pretty nice these days, I recommend installing the paredit and lispindent packages via Sublime Package Control.

If you have Leiningen installed you can run the following at the command line in whatever directory you like:

lein new mies async-tut1

This will create a template project so you don't have to worry about configuring lein-cljsbuild yourself.

Unless otherwise noted files are relative to the project directory.

Change the :dependencies in the project.clj file to look like the following:

:dependencies [[org.clojure/clojure "1.5.1"]
               [org.clojure/clojurescript "0.0-2030"]
               [org.clojure/core.async "0.1.256.0-1bf8cf-alpha"]] ;; ADD

In the project directory run the following to start the auto compile process:

lein cljsbuild auto async-tut1

First off we want to add the following markup to index.html before the first script tag which loads goog/base.js:

<input id="query" type="text"></input>
<button id="search">Search</button>
<p id="results"></p>

Open index.html in Chrome and make sure you see an input field and a text button.

Now we want to write some code so that we can interact with the DOM. We want our code to be resilient to browser differences so we'll use Google Closure to abstract this stuff away as we might with jQuery.

We require goog.dom and give it a less annoying alias. Change the ns form in src/async_tut1/core.cljs to the following:

(ns async-tut1.core
  (:require [goog.dom :as dom]))

We want to confirm that this will work so let's change the console.log expression so it looks this instead:

(.log js/console (dom/getElement "query"))

Save the file and it should be recompiled instantly. We should be able refresh the browser and see that a DOM element got printed in the JavaScript Console (View > Developer > JavaScript Console). Remove this little test snippet after you've confirmed it works.

So far so good.

Now we want a way to deal with the user clicking the mouse. Instead of just setting up a callback on the button directly we're going to make the button put the click event onto a core.async channel.

Let's write a little helper called listen that will return a channel of the events for a particular element and particular event type. We need to require core.async macros and functions. Our ns should now look like the following:

(ns async-tut1.core
  (:require-macros [cljs.core.async.macros :refer [go]])
  (:require [goog.dom :as dom]
            [goog.events :as events]
            [cljs.core.async :refer [put! chan <!]]))

Again we want to abstract away browser quirks so we use goog.events for dealing with that. We include only the core.async macros and functions that we intend to use.

Now we can write our listen fn, it looks like this:

(defn listen [el type]
  (let [out (chan)]
    (events/listen el type
      (fn [e] (put! out e)))
    out))

We want to verify our function works as advertised so we check it with following snippet of code at the end of the file:

(let [clicks (listen (dom/getElement "search") "click")]
  (go (while true
        (.log js/console (<! clicks)))))

Note that we've created what appears to be an infinite loop here, but actually it's a little state machine. If there are no events to read from the click channel, the go block will be suspended.

Let's search Wikipedia. Define the basic URL we are going to hit via JSONP, put this right after the ns form.

(def wiki-search-url
  "http://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=")

Now we want to make a function that returns a channel for JSONP results.

We again reach for Google Closure to avoid browser quirks. Make your ns form looking like the following:

(ns async-tut1.core
  (:require-macros [cljs.core.async.macros :refer [go]])
  (:require [goog.dom :as dom]
            [goog.events :as events]
            [cljs.core.async :refer [<! put! chan]])
  (:import [goog.net Jsonp]
           [goog Uri]))

Here we use :import so that we can use short names for the Google Closure constructors.

Note: :import is only for this use case, you never use it with ClojureScript libraries

Our JSONP helper looks like the following (put it after listen in the file):

(defn jsonp [uri]
  (let [out (chan)
        req (Jsonp. (Uri. uri))]
    (.send req nil (fn [res] (put! out res)))
    out))

This looks pretty straight forward, very similar to listen. Let's write a simple function for constructing a query url:

(defn query-url [q]
  (str wiki-search-url q))

Again lets test this by writing a snippet of code at the bottom of the file.

(go (.log js/console (<! (jsonp (query-url "cats")))))

In the JavaScript Console we should see we got an array of JSON data back from Wikipedia. Success!

It's time to hook everything together. Remove the test snippet and replace it with the following:

(defn user-query []
  (.-value (dom/getElement "query")))

(defn init []
  (let [clicks (listen (dom/getElement "search") "click")]
    (go (while true
          (<! clicks)
          (.log js/console (<! (jsonp (query-url (user-query)))))))))

(init)

Try it now, you should be able to write a query in the input field, click "Search", and see results in the JavaScript Console.

If you've done any JavaScript programming this way of writing the code should be somewhat surprising - we don't need a callback to work with button clicks!

Think a bit how this work. When the page loads, init will run, the go block will try to read from clicks, but there will be nothing to read so the go block becomes suspended. Only when you click on the button can it proceed at which point we'll run the query and loop around. The code reads exactly how it would if you didn't have to consider asynchrony!

Instead of printing to the console we would like to render the results to the page. Let's do that now, add the following before init:

(defn render-query [results]
  (str
    "<ul>"
    (apply str
      (for [result results]
        (str "<li>" result "</li>")))
    "</ul>"))

The usual string concatenation stuff - we use a list comprehension here just for fun.

Now change init to look like the following:

(defn init []
  (let [clicks (listen (dom/getElement "search") "click")
        results-view (dom/getElement "results")]
    (go (while true
          (<! clicks)
          (let [[_ results] (<! (jsonp (query-url (user-query))))]
            (set! (.-innerHTML results-view) (render-query results)))))))

Hopefully this code at this point just makes sense. Notice how we can use destructuring on the JSON array of Wikipedia results.

A beautiful succinct program! The complete listing follows:

(ns async-tut1.core
  (:require-macros [cljs.core.async.macros :refer [go]])
  (:require [goog.dom :as dom]
            [goog.events :as events]
            [cljs.core.async :refer [<! put! chan]])
  (:import [goog.net Jsonp]
           [goog Uri]))

(def wiki-search-url
  "http://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=")

(defn listen [el type]
  (let [out (chan)]
    (events/listen el type
      (fn [e] (put! out e)))
    out))

(defn jsonp [uri]
  (let [out (chan)
        req (Jsonp. (Uri. uri))]
    (.send req nil (fn [res] (put! out res)))
    out))

(defn query-url [q]
  (str wiki-search-url q))

(defn user-query []
  (.-value (dom/getElement "query")))

(defn render-query [results]
  (str
    "<ul>"
    (apply str
      (for [result results]
        (str "<li>" result "</li>")))
    "</ul>"))

(defn init []
  (let [clicks (listen (dom/getElement "search") "click")
        results-view (dom/getElement "results")]
    (go (while true
          (<! clicks)
          (let [[_ results] (<! (jsonp (query-url (user-query))))]
            (set! (.-innerHTML results-view) (render-query results)))))))

(init)