1. Introduction

This tutorial provides an introduction to ClojureScript from very basic setup, to a more complex application in little incremental steps.

It includes:

  • Set up initial Clojure app layout.

  • Set up initial ClojureScript app layout.

  • First contact with the ClojureScript language.

  • Working with DOM events.

  • Working with routing in the browser.

  • Working with AJAX requests.

  • First contact with core.async.

  • Working with events and AJAX using core.async.

  • First contact with Om/React.

  • Working with Om and time traveling.

  • Working with Om and peristent state.

  • Little bonus: browser-enabled REPL.

We also touch upon:

  • Google Closure Library along all tutorial.

  • ClojureScript features (not explained on the first contact with language).

  • Some differences between Clojure and ClojureScript.

2. Why ClojureScript

  • ClojureScript is designed as a guest language—​unlike FunScript or similar, it doesn’t intend to translate host code to JavaScript; you cannot import java.util.Date in ClojureScript…​

  • Language with own semantics (not like CoffeeScript, TypeScript, …​)

  • Good host interoperability.

  • Batteries included (ClojureScript runtime & Google Closure Library)

  • Expressive.

  • Functional.

  • Lisp.

  • Macros.

  • Google Closure Compiler (advanced code compiling with dead code elimination)

  • core.async (coroutines and CSP as a library)

  • …​ much more.

3. Set up initial project

3.1. Let’s start with

git clone https://github.com/niwibe/cljs-workshop
cd cljs-workshop
git checkout step0

3.2. This step consists of…​

  • Initial Leiningen project template.

  • Add Ring, Compojure and other related dependencies.

  • Create routes and initial application entry point.

  • First run of hello world app.

3.3. Initial project tree

resources/
resources/public/
resources/index.html
src/
src/clj/
src/clj/cljsworkshop/
src/clj/cljsworkshop/core.clj
project.clj

3.4. Initial project.clj

(defproject cljsworkshop "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "BSD (2-Clause)"
            :url "http://opensource.org/licenses/BSD-2-Clause"}
  :dependencies [[org.clojure/clojure "1.6.0"]

                 ;; Backend dependencies
                 [compojure "1.3.1"]

                 [ring/ring-core "1.3.2" :exclusions [javax.servlet/servlet-api]]
                 [ring/ring-servlet "1.3.2" :exclusions [javax.servlet/servlet-api]]
                 [ring/ring-defaults "0.1.2" :exclusions [javax.servlet/servlet-api]]

                 [cc.qbits/jet "0.5.4"]]

  :source-paths ["src/clj"]
  :main cljsworkshop.core)

3.5. Intial Ring/Compojure handlers

  • A Ring handler consists of a simple function that receives a request (hash-map) and returns a response (also hash-map).

  • Compojure adds routing handlers and some response helpers.

  • Jetty9 is an embedded HTTP/application server.

clj/cljsworkshop/core.clj
(ns cljsworkshop.core
  (:require [qbits.jet.server :refer [run-jetty]]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]
            [compojure.core :refer :all]
            [compojure.route :as route]
            [compojure.response :refer [render]]
            [clojure.java.io :as io]))

;; This is a handler that returns the
;; contents of `resources/index.html`
(defn home
  [req]
  (render (io/resource "index.html") req))

;; Defines a handler that acts as router
(defroutes app
  (GET "/" [] home)
  (route/resources "/static")
  (route/not-found "<h1>Page not found</h1>"))

;; Application entry point
(defn -main
  [& args]
  (let [app (wrap-defaults app site-defaults)]
    (run-jetty {:ring-handler app :port 5050})))

3.6. Initial resources/index.html file

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>CLJS Workshop</title>
  </head>
  <body>
    <h1>Hello World</h1>
  </body>
</html>

3.7. Run your first Clojure web application

As we declared the main entry point in our project.clj, now we only need to execute the Leiningen run command:

$ lein run
2014-12-08 14:03:49.623:INFO::main: Logging initialized @877ms
2014-12-08 14:03:52.992:INFO:oejs.Server:main: jetty-9.2.3.v20140905
2014-12-08 14:03:53.016:INFO:oejs.ServerConnector:main: Started ServerConnector@3149409c{HTTP/1.1}{0.0.0.0:5050}
2014-12-08 14:03:53.017:INFO:oejs.Server:main: Started @4283ms

4. First steps with ClojureScript

Before we dive into writing ClojureScript, let’s start with a little introduction.

4.1. Data Types

Table 1. Types

Type name

Representation

String

"Hello World"

Long

2

Float

2.4

Keyword

:foo

Map

{:foo "bar"}

Vector

[1 2 "3"]

List

(1 2 "3")

Set

#{1 2 "3"}

Regex

#"^\w+$"

4.2. Print to console

(.log js/console "hello world")
(enable-console-print!)
(println "hello world")

4.3. Modules

Declare module
(ns my.library)
Require a module
(ns my.library
  (:require [my.other :as other]))

4.4. Variables

Top level (vars)
(def myvar "foo")
Local bindings
(let [myvar "foo"]
  (println myvar))

4.5. Functions

Simple function definition
(defn foo
  [a b c]
  c)

(foo 1) ;; WARNING: function called with incorrect
        ;; number of arguments

(foo 1 2 3) ;; => 3
Dispatch on arity
(defn foo
  ([a] "one")
  ([a b] "two")
  ([a b c] "three"))

(foo 1) ;; => "one"
(foo 1 2) ;; => "two"
(foo 1 2 3) ;; => "three"

;; Under advanced compilation direct dispatch to
;; arity. No `arguments` object manipulation
Variable number of arguments
(defn foo
  [a b & rest]
  rest)

(foo 1 2 3) ;; => [3]
(foo 1 2 3 4 5) ;; => [3 4 5]
Named parameters & default values
(defn foo
  [& {:keys [bar baz]
      :or {bar "default1"
           baz "default2"}}]
  (str bar "-" baz))

(foo) ;; => "default1-default2"
(foo :bar 1) ;; => "1-default2"
(foo :bar 1 :baz 2) ;; => "1-2"

4.6. Equality

Unlike JavaScript, CLJS equality is based on value (not identity) and does not perform type coercion.

// == operator is coercive
1 == "1" // => true

// sometimes based on identity (instead of value)
{} == {} // => false

["a"] === ["a"] // => false
(= 1 "1") ;; => false
(= {} {}) ;; => true
(= ["a"] ["a"]) ;; => true

4.7. Immutable locals

In cljs, locals are immutable:

This code throws an error:
(let [x 2]
  (set! x 3))

4.8. Dynamic binding

(def ^:dynamic x 5)

(defn print-value
  []
  (println "Current value:" x))

(print-value)
(binding [x 10]
  (print-value))
(print-value)

;; Will result in:
;; Current value: 5
;; Current value: 10
;; Current value: 5

4.9. Destructuring

Positional destructuring.
(def color [255 255 100 0.5])

(let [[r g _ a] color]
  (println r)
  (println a))

;; Will result in:
;; 255
;; 0.5
Hash map keys destructuring
(def m {:first "Bob"
        :middle "J"
        :last "Smith"})

(let [{:keys [first last]} m]
  (println first)
  (println last))

;; Will result in:
;; Bob
;; Smith

4.10. Expression problem

;; For example say you'd like to use RegExps
;; as functions

(extend-type js/RegExp
  IFn
  (-invoke
   ([this s]
     (re-matches this s))))

(filter #"foo.*" ["foo" "bar" "foobar"])
;; => ("foo" "foobar")

More resources:

4.11. Multimethods

Polymorphism a la carte.

Define a multimethod
(defmulti say-hello
  (fn [person]
    (:lang person :en)))

(defmethod say-hello :en
  [person]
  (format "Hello %s" (:name person)))

(defmethod say-hello :es
  [person]
  (format "Hola %s" (:name person)))
Playing with multimethod
(def person-alex {:lang :es :name "Alex"})
(def person-yen {:lang :en :name "Yen"})
(def person-anon {:name "Anonymous"})

(say-hello person-alex)
;; => "Hola Alex"

(say-hello person-yen)
;; => "Hello Yen"

(say-hello person-anon)
;; => "Hello Anonymous"

4.12. Interoperability

4.12.1. Create JavaScript objects

ClojureScript
(def foo (js-obj "bar" "baz"))
JavaScript
var foo = {bar: "baz"};

4.12.2. Property access

ClojureScript
(set! (.-bar foo) "baz")
(.log js/console (.-bar foo))

;; aset means array set
(aset foo "abc" 17)
(.log js/console (aget foo "abc"))
JavaScript
foo.bar = "baz";
console.log(foo.bar);

foo["abc"] = 17;
console.log(foo["abc"]);

4.12.3. Conversions beween cljs and js

Convert cljs types to js using clj->js function
(let [a {:a 1 :b {:c 1}}]
  (clj->js a))
Convert js types to cljs using js->clj function
(defn get-names [people]
  (let [people (js->clj people)
        names (map "name" people)]
    (clj->js names)))
Using reader macro to convert cljs literals to js:
(let [a #js [1 2 3]]
  (println (aget a 1)))

;; Will result in:
;; 2
Note
the #js reader macro is not recursive.

5. First ClojureScript Hello World

5.1. Let’s start with

git reset --hard
git checkout step1

5.2. New dependencies in project.clj

project.clj
:dependencies [;; ...
               [org.clojure/clojurescript "0.0-2843"]
               ;; ...]

5.3. lein-cljsbuild plugin is your friend

project.clj
:plugins [[lein-cljsbuild "1.0.4"]]
:cljsbuild {:builds
            [{:id "app"
              :source-paths ["src/cljs"]
              :compiler {:output-to "resources/public/js/app.js"
                         :output-dir "resources/public/js/out"
                         :source-map true
                         :optimizations :none
                         :asset-path "/static/js/out"
                         :main "cljsworkshop.core"
                         :pretty-print true}}]}

5.4. New ClojureScript file.

New tree structure of src/cljs/ directory for ClojureScript sources.

src/cljs/
src/cljs/cljsworkshop/
src/cljs/cljsworkshop/core.cljs

5.5. Hello World in cljs

A simple main function that sets the content of a single DOM element:

core.cljs
(ns cljsworkshop.core)

(defn set-html! [el content]
  (set! (.-innerHTML el) content))

(defn main
  []
  (let [content "Hello World from ClojureScript"
        element (aget (js/document.getElementsByTagName "main") 0)]
    (set-html! element content)))

(main)

5.6. index.html changes

Adapt our template to make it compatible with our example:

<body>
  <main></main>
  <script src="/static/js/app.js"></script>
</body>

5.7. Compile our cljs file

[3/5.0.7]niwi@niwi:~/cljs-workshop> lein cljsbuild once
Compiling ClojureScript.
Compiling "resources/public/js/app.js" from ["src/cljs"]...
Successfully compiled "resources/public/js/app.js" in 3.396 seconds.

6. First DOM events

6.1. Introduction

This section shows a basic way to access DOM elements and events by using the Google Closure Library.

6.2. Let’s start with

git reset --hard
git checkout step2

6.3. index.html changes

Adapt our initial template to something that we can use for DOM event examples. It consists of two pieces:

  • One <button> to increment the counter.

  • One <span> to show the current value of the counter.

resources/index.html
<main>
  <section>
    <span>Clicks: </span>
    <span id="clicksnumber"><span>
  </section>
  <button id="button">Click me</button>
</main>

6.4. New main function

Change the main function to something like this:

(ns cljsworkshop.core
  (:require [goog.events :as events]
            [goog.dom :as dom]))

(defn main
  []
  (let [counter (atom 0)
        button  (dom/getElement "button")
        display (dom/getElement "clicksnumber")]

    ;; Set initial value
    (set! (.-innerHTML display) @counter)

    ;; Assign event listener
    (events/listen button "click"
                   (fn [event]
                     ;; Increment the value
                     (swap! counter inc)
                     ;; Set new value in display element
                     (set! (.-innerHTML display) @counter)))))

(main)

6.5. New stuff

  • ClojureScript uses Google Closure Library for modules/namespace: each ClojureScript file represents a Google Closure module

  • The :require statement on ns can load any Google Closure module or your defined module that the compiler can find on the path (see :asset-path in project.clj…​)

  • Google Closure Library comes with ClojureScript. You don’t need to add it as dependency.

  • Works in advanced mode of Google Closure Compiler (that eliminates unused code).

7. Routing in the browser

7.1. Introduction

Some JavaScript frameworks come with their own solution, but in this case we simply use a library dedicated to routing: secretary.

The ClojureScript community has built other libraries, but this one is the easiest for newcomers to understand.

7.2. Let’s start with

git reset --hard
git checkout step3

7.3. Add secretary dependency

Add the corresponding dependency entry:

project.clj
:dependencies [;; ...
               [secretary "1.2.1"]]

7.4. core.cljs changes

Adapt our cljs code to something like this:

(ns cljsworkshop.core
  (:require-macros [secretary.core :refer [defroute]])
  (:require [goog.events :as events]
            [goog.dom :as dom]
            [secretary.core :as secretary])
  (:import goog.History))

(def app (dom/getElement "app"))

(defn set-html! [el content]
  (set! (.-innerHTML el) content))

(defroute home-path "/" []
  (set-html! app "<h1>Hello World from home page.</h1>"))

(defroute some-path "/:param" [param]
  (let [message (str "<h1>Parameter in url: <small>" param "</small>!</h1>")]
    (set-html! app message)))

(defroute "*" []
  (set-html! app "<h1>Not Found</h1>"))

(defn main
  []
  ;; Set secretary config for use the hashbang prefix
  (secretary/set-config! :prefix "#")

  ;; Attach event listener to history instance.
  (let [history (History.)]
    (events/listen history "navigate"
                   (fn [event]
                     (secretary/dispatch! (.-token event))))
    (.setEnabled history true)))

(main)

7.5. New stuff

  • ClojureScript macros are written in Clojure (not ClojureScript) but emit ClojureScript code.

  • Should be imported separately, using (:require-macros ...) statement on ns.

  • Google Closure classes should be imported with (:import ...) statement.

8. First steps with AJAX

8.1. Introduction

This step introduces some experiments with AJAX. It uses a Google Closure Library facility to make JSONP requests to Wikipedia.

This is a simple demonstration of how can it be done, using plain callback style code. But in a future step, you will discover better ways to do it.

8.2. Let’s start with

git reset --hard
git checkout step4

8.3. core.cljs changes

Partial content from core.cljs
(ns cljsworkshop.core
  (:require-macros [secretary.core :refer [defroute]])
  (:require [goog.events :as events]
            [goog.dom :as dom]
            [secretary.core :as secretary])
  (:import goog.History
           goog.Uri
           goog.net.Jsonp))

(def search-url "http://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=")
(def home-html
  (str "<h1>Wikipedia Search:</h1>"
       "<section>"
       "  <input id=\"query\" placeholder=\"Type your search...\" />"
       "  <button id=\"searchbutton\">Search</button>"
       "  <ul id=\"results\"></ul>"
       "</section>"))

(defn render-results [results]
  (let [results (js->clj results)]
    (reduce (fn [acc result]
              (str acc "<li>" result "</li>"))
            ""
            (second results))))

(defn do-jsonp
  [uri callback]
  (let [req (Jsonp. (Uri. uri))]
    (.send req nil callback)))

(defroute home-path "/" []
  (set-html! app home-html)
  (let [on-response     (fn [results]
                          (let [html (render-results results)]
                            (set-html! (dom/getElement "results") html)))

        on-search-click (fn [e]
                          (let [userquery (.-value (dom/getElement "query"))
                                searchuri (str search-url userquery)]
                            (do-jsonp searchuri on-response)))]

    (events/listen (dom/getElement "searchbutton") "click" on-search-click)))

9. Introduction to core.async

9.1. Async code that looks sync

Before starting with core.async, we will try to solve one simple problem:

  1. Request one URL.

  2. Wait 1 second.

  3. Request a second URL.

  4. Return both results.

9.1.1. Solution using ES5

Let’s start by introducing a problem using ES5 (ECMAScript 5) or shortly JavaScript of today. Let’s define the following util functions:

function timeout(ms) {
  return new Promise(function(resolve) {
    setTimeout(resolve, ms);
  });
}

function httpGet(url) {
  return new Promise(function(resolve) {
    var req = new XMLHttpRequest();
    req.open("GET", url, false);
    req.send(null);
    req.onreadystatechange = function() {
      if (req.readyState == 4) {
        resolve(xhr.responseText);
      }
    }
  });
}

And implement the solution:

function doStuff() {
   return httpGet("http://page1/").then(function(response) {
    return timeout(1000).then(function() {
      return response;
    });
   })
   .then(function(response1) {
     return httpGet("http://page2/").then(function(response2) {
       return {response1: response1,
               response2: response2};
     });
   })
}
Now you can use it like so:
doStuff().then(function(result) {
  console.log(result.response1);
  console.log(result.response2);
});

Obviously, it can be done better, but nobody will save us from callbacks.

9.1.2. Solution using ES7

But, what is cooking for ES7? (ES7? but ES6 still not ready? WTF)

Same example but using the draft proposal for ES7
async function doStuff() {
  var response1, response2;

  response1 = await httpGet("http://page1/");
  await timeout(1000):
  response2 = await httpGet("http://page2/");
  return {response1: response1,
          response2: response2};
}
Now you can use it like so:
(async function() {
  var result = await doStuff()
  console.log(result.response1);
  console.log(result.response2);
})();

That looks much better.

Notes:

  • This can be "emulated" with generators, but they are not designed for this purpose.

9.1.3. Solution using cljs and core.async

Now having the background of that ES7 example, let’s see the same thing using core.async library with ClojureScript.

Define the missing util function.
(defn http-get [uri]
  (let [out (chan)
        req (XhrIo. (Uri. uri))]
    (events/listen req "success" #(put! out (.getResponseText (.-target %))))
    (.send req (Uri. uri))
    out))
Define the doStuff-like function with main logic.
(defn do-stuff
  []
  (go
    (let [response1 (<! (http-get "http://page1/"))
          _         (<! (timeout 1000))
          response2 (<! (http-get "http://page2/"))]
      {:response1 response1
       :response2 response2})))
Now see an example of how to use it.
(go
  (let [result (<! (do-stuff))]
    (.log js/console (:response1 result))
    (.log js/console (:response2 result))))

You can see that, while this code is async by nature, it is structured and reads a lot like synchronous code.

9.2. A little reference of basic core.async components

9.2.1. The go function/macro

(go
  [... do something asynchronously ...])
  • returns a channel.

  • puts in the returned channel the result of last expression in the body of the go block.

  • executes asynchronously.

9.2.2. The chan function

(chan)
  • creates a new channel

  • write to the channel with put! and >! (see below)

  • read from the channel with take! and <! (see below)

  • nil values cannot be written to or read from channels

  • supports different buffering strategies: fixed size, unbound (default), sliding window, dropping.

9.2.3. The <! and >! functions

(go
  (<! (timeout 100))
  (.log js/console "finished"))
  • <! represents a callback-less take!

  • >! represents a callback-less put!

  • in Clojure, there are blocking versions, <!! and >!!, that can be used outside of a go block; instead they block the current thread.

  • take! and <! return nil if the input channel is closed.

10. AJAX with core.async

10.1. Introduction

This step tries to reproduce the examples from step4 (AJAX-related) but using core.async to make the logic look synchronous, improving readability and eliminating the accidental complexity of explicit continuations/callbacks.

10.2. Let’s start with

git reset --hard
git checkout step5

10.3. Why

  • Callbacks suck.

  • Unclear execution flow.

  • We can do it better!

  • with core.async, async code looks sync ;)

10.4. Add core.async dependency

Add the core.async dependency to our project.clj:

project.clj
:dependencies [;; ...
               [org.clojure/core.async "0.1.346.0-17112a-alpha"]]

10.5. core.cljs changes

Convert our code from the previous step to something like this:

Partial content from core.cljs
(ns cljsworkshop.core
  (:require-macros [secretary.core :refer [defroute]]
                   [cljs.core.async.macros :refer [go]])
  (:require [goog.events :as events]
            [goog.dom :as dom]
            [secretary.core :as secretary]
            [cljs.core.async :refer [<! put! chan]])
  (:import goog.History
           goog.Uri
           goog.net.Jsonp))

(defn render-results [results]
  (let [results (js->clj results)]
    (reduce (fn [acc result]
              (str acc "<li>" result "</li>"))
            ""
            (second results))))

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

(defroute home-path "/" []
  ;; Render initial html
  (set-html! app home-html)

  (let [clicks (listen (dom/getElement "searchbutton") "click")]
    (go (while true
          (<! clicks)
          (let [uri     (str search-url (.-value (dom/getElement "query")))
                results (<! (jsonp uri))]
            (set-html! (dom/getElement "results")
                       (render-results results)))))))

Now the code looks sync:

  1. Wait for a click.

  2. Make a request to Wikipedia.

  3. Render result.

Synchronous code is easier to read and reason about.

11. Hello World from Om component

11.1. Introduction

This is the first step that introduces Om.

11.1.1. Why:

  • React (functional approach for rendering DOM).

  • Global state management facilities built in.

  • Customizable semantics. Fine-grained control over how components store state.

  • Out of the box snapshotable and undoable and these operations have no implementation complexity and little overhead.

11.2. Let’s start with

git reset --hard
git checkout step6

11.3. Add dependencies

project.clj
:dependencies [;; ...
               [org.omcljs/om "0.8.8"]
               [prismatic/om-tools "0.3.10"]]

11.4. First Om component

Before we look at a more complex app, we’ll try understand the basics of Om components.

(ns mysamplens
  (:require [om.core :as om]
            [om-tools.dom :as dom]))

(defn mycomponent
  [app owner]
  (reify
    ;; Set the initial component state.
    om/IInitState
    (init-state [_]
      {:message "Hello world from local state"})

    ;; Render the component with current local state.
    om/IRenderState
    (render-state [_ {:keys [message]}]
      (dom/section
        (dom/div message)
        (dom/div (:message app))))))

reify, what is this?

reify creates an anonymous object that implement one or more protocols.

Om components consist of any object that implements the om/IRender or om/IRenderState protocols. Implementations for other protocols is optional.

In previous examples, we have used a few protocols. Om comes with a few others, but those are out of scope for this first example.

*Now, having defined a compoment, it’s time to mount it.

(defonce state {:message "Hello world from global state."})

;; "app" is the id of a dom element in index.html
(let [el (gdom/getElement "app")]
  (om/root mycomponent state {:target el}))

12. Time traveling with Om

12.1. Introduction

  • The state of the application is serializable, which makes it easy to deterministically reproduce the concrete state of the application.

  • The union of ClojureScript and React makes some tasks, those often considered quite complex, easy and painless—​such as time traveling or undo in a few lines of code.

12.2. Let’s start with

git reset --hard
git checkout step7

12.3. How it works

Desgining the application with the global state management facilities of Om, we can easily make a snapshot of the current state.

In Clojure(Script) an atom can be listened for changes:

;; Global applicatioon state
(def tasklist-state (atom {:entries []}))

;; Undo application state. An atom that will store
;; the snapshots of tasklist-state initialized with
;; initial @tasklist-state.
(def undo-state (atom {:entries [@tasklist-state]})

;; Watch tasklist-state changes and snapshot them
;; into undo-state.
(add-watch tasklist-state :history
  (fn [_ _ _ n]
    (let [entries (:entries @undo-state)]
      (when-not (= (last entries) n)
        (swap! undo-state #(update-in % [:entries] conj n))))))

Now, each change in our application state is saved as a snapshot to another atom, and with the press of a button we can revert the last change and restore the previous state.

For it, we create another Om component…​
(defn do-undo
  [app]
  (when (> (count (:entries @app)) 1)
    ;; remove the last snapshot from the undo list.
    (om/transact! app :entries pop)

    ;; Restore the last snapshot into tasklist
    ;; application state
    (reset! tasklist-state (last (:entries @undo-state)))))

(defn undo
  [state owner]
  (reify
    om/IRender
    (render [_]
      (dom/section {:class "undo"
                    :style {:padding "5px"
                            :border "1px solid #ddd"}}
        (dom/section {:class "buttons"}
          (dom/input {:type "button"
                      :default-value "Undo"
                      :on-click (fn[_] (do-undo state))}))))))

13. Persisting Om state

13.1. Introduction

Now having experimented with time traveling, let’s try an experiment in making the state persistent. We will use the previous example and HTML5 Local Storage (via the hodgepodge library).

13.2. Let’s start with

git reset --hard
git checkout step8

13.3. Add hodgepodge dependency

project.clj
:dependencies [;; ...
               [hodgepodge "0.1.3"]]

13.4. Add the persistence logic to our tasklist app

Add the corresponding :require entry for hodgepodge
(ns cljsworkshop.core
  (:require [...]
            [hodgepodge.core :refer [local-storage]]))
Add additional watcher to tasklist-state atom to handle persistence
;; Watch tasklist-state changes and
;; persists them in local storage.
(add-watch tasklist-state :persistence
  (fn [_ _ _ n]
    (println "Event:" n)
    (assoc! local-storage :taskliststate n)))
Add code to restore stored state on app initialization.
;; Watch tasklist-state changes and
;; Get the persisted state, and if it exists
;; restore it on tasklist and undo states.
(when-let [state (:taskliststate local-storage)]
  (reset! tasklist-state state)
  (reset! undo-state {:entries [state]}))

14. Bonus: browser-enabled REPL

14.1. Introduction

One of the reasons that Clojure rocks is that it has a REPL which gives developers the most dynamic development experience possible. We would like to support this dynamic development experience in every environment where JavaScript runs.

There are many interesting environments in which JavaScript can run. And ClojureScript offers an abstraction for running a REPL in those different evaluation environments.

In this section, we will see how to use a browser as an evaluation environment.

14.2. Let’s start with

git reset --hard
git checkout step9

14.3. Add a little snippet of code to your cljs main file

(ns cljsworkshop.core
  (:require [...]
            [clojure.browser.repl :as repl]))

(repl/connect "http://localhost:9000/repl")

14.4. Crete a REPL setup file

repl_browser.clj
(require
  '[cljs.repl :as repl]
  '[cljs.repl.browser :as browser])

(repl/repl* (browser/repl-env)
  {:output-dir "out"
   :optimizations :none
   :cache-analysis false
   :source-map true})

14.5. Optionally create a helper script

This makes it easy to start the repl.

start-browserrepl.sh
#!/usr/bin/env bash
rlwrap lein trampoline run -m clojure.main repl_browser.clj

Add execution permissions:

chmod +x start-browserrepl.sh

14.6. Start the REPL

Using the previously-created shell script:

$ ./start-browserrepl.sh
Compiling client js ...
Waiting for browser to connect ...
To quit, type: :cljs/quit

And now navigate to your public page!

It will automatically connect to the REPL and you will be able evaluate ClojureScript code in the browser.

Try evaluating the current app state
cljs.user=> (in-ns 'cljsworkshop.core)
cljsworkshop.core
cljsworkshop.core=> @tasklist-state
{:entries [{:completed false, :created-at "2014-12-08T11:32:10.677Z", :subject "task 1"}]}
nil

15. Bonus: standalone REPL

Continue from step9

15.1. Introduction

Browser-enabled REPL is awesome if you are building a web app, but it is slightly tedious if you are building a library, requiring a browser for something that doesn’t need it.

Instead, we can use another evaluation environment like Node.js or Java 8 Nashorn JavaScript engine. You can start a Node.js REPL (for example) much easier than a browser-enabled REPL.

15.2. Crete a REPL setup file

repl_nodejs.clj
(require
  '[cljs.repl :as repl]
  '[cljs.repl.node :as node])

(repl/repl* (node/repl-env)
  {:output-dir "out"
   :optimizations :none
   :cache-analysis false
   :source-map true})

15.3. Optionally create a helper script

This makes it easy to start the repl.

start-noderepl.sh
#!/usr/bin/env bash
rlwrap lein trampoline run -m clojure.main repl_nodejs.clj

Add execution permissions:

chmod +x start-noderepl.sh

15.4. Start the REPL and evaluate cljs

Using the previously-created shell script:

$ ./start-noderepl.sh
To quit, type: :cljs/quit
ClojureScript Node.js REPL server listening on 58603
ClojureScript:cljs.user> (require '[clojure.string :as str])

ClojureScript:cljs.user> (str/lower-case "Foo Bar")
"foo bar"

16. Bonus: production-ready builds

16.1. Introduction

Up to this point, we have only compiled ClojureScript for development-friendly environments. And it is ok, but now it’s time to deploy our application.

As we know, ClojureScript uses the Google Closure Compiler. Let’s see how we can configure the production-ready builds.

16.2. Let’s start with

git reset --hard
git checkout step10

16.3. Build configuration

Replace your old build configuration in your project.clj with this:

{:builds
 [{:id "devel"
   :source-paths ["src/cljs"]
   :compiler {:output-to "resources/public/js/app.js"
              :output-dir "resources/public/js/out-devel"
              :source-map true
              :optimizations :none
              :cache-analysis false
              :asset-path "/static/js/out-devel"
              :main cljsworkshop.core
              :pretty-print true}}
  {:id "prod"
   :source-paths ["src/cljs"]
   :compiler {:output-to "resources/public/js/app.js"
              :output-dir "resources/public/js/out-prod"
              :source-map "resources/public/js/app.js.map"
              :optimizations :advanced
              :cache-analysis false
              :asset-path "/static/js/out-prod"
              :main cljsworkshop.core
              :pretty-print false}}]})

devel is the existing build; prod is the new one. Observe the differences:

  • :optimizations is set to :advanced

  • :pretty-print is set to false

Very easy; now run the compiler in prod mode:

[3/5.0.7]niwi@niwi:~/cljs-workshop> lein cljsbuild auto prod
Compiling ClojureScript.
Compiling "resources/public/js/app.js" from ["src/cljs"]...
Successfully compiled "resources/public/js/app.js" in 17.396 seconds.

And observe that your app is running like with previous compilation mode but loads only one little .js file.

17. Bonus: using third-party libraries

17.1. Introduction

Is well known that not all third-party libraries like jQuery, Moment.js or React work out of the box with the Google Closure compiler.

But it is not a problem. ClojureScript, thanks to some Closure compiler options, has the ability to use external libraries.

For this purpose it exposes the :foreign-libs and :externs compiler options.

The first one allows you to declare dependencies and name the Closure module for the third-party library. The second allows you to provide a file that helps the Closure compiler avoid mangling symbol names for the third-party library’s public API.

17.2. Let’s start with

git reset --hard
git checkout step11

17.3. Integrating Moment.js

Add these options to your build configurations:

:foreign-libs [{:file "js/moment.js"
                :file-min "js/moment.min.js"
                :provides ["cljsworkshop.moment"]}]
:externs ["js/moment.min.js"]
:closure-warnings {:externs-validation :off}

An "externs" file is a JavaScript source file used to specify the public API of a library. They are very tedious to write, but in this case, we can use the minified js source as an externs file.

Using the minified source as an externs file raises a lot of warnings that we can ignore in the majority of cases.

17.4. Use Moment.js in our app

To use Moment.js in our application, you should add it to our require part of the ns macro:

(ns cljsworkshop.core
  (:require [...]
            [cljsworkshop.moment :as moment]))

Independently of the alias used in requirement statement, the moment variable is set globally and it should be used through the js/ special prefix. Like this:

(def now (js/moment))

18. Other Om resources