Skip to content

Instantly share code, notes, and snippets.

@iantruslove
Created February 16, 2014 19:34
Show Gist options
  • Save iantruslove/9039452 to your computer and use it in GitHub Desktop.
Save iantruslove/9039452 to your computer and use it in GitHub Desktop.
Building a simple ClojureScript single page web application [Den of Clojure Feb 2014]

Beyond hello world: building a simple ClojureScript single page web application

Shown at Feb '14 Den of Clojure

Intro

So you've read the blogs and thought how fun it would be to write web front ends with your favorite lisp. Perhaps you've even fired up the repl and done a little playing. But how do you get from there to a real-life app?

This talk will give a brief introduction to ClojureScript, cover using Austin to drive a browser REPL from within Emacs, configuring the Google Closure compiler to make any JavaScript library usable from ClojureScript, and finally put it all together in a map-based REST-backed single page web application.

ClojureScript

ClojureScript is a Clojure implementation compiling to JavaScript.

It's heavily dependent upon Google Closure compiler for optimization.

Not 100% of Clojure, but all that makes sense:

  • No concurrency support - JavaScript is single-threaded.

REPLs

Out of the box: Rhino and browser-connected.

Austin

Austin > ClojureScript's repl:

  • nREPL via Piggieback
  • Lein middleware
  • Internal HTTP server

Toolchain

Server:

Client:

  • Austin - REPL
  • ClojureScript libs: jayq, dommy, domina, ...
  • Closure for interop with LeafletJS, Backbone, etc.

Interop

Like with Clojure, it's both important and powerful. The underlying platform is exposed.

Luke Vanderhart has a great article on Using JavaScript libraries in ClojureScript. The summary is "use externs definitions to use JavaScript libraries".

L = {};

L.map;
L.Map;

L.map.addLayer;
L.map.setView;

App setup

                                  +---------------------+
                                  |        nREPL        |
                                  |---------------------|
                                  |                     |               +------------+
 +-----------+     +-----------+  |   +-----------+     |               |            |
 |           |     |           |  |   |           |     | HTML CSS JS   |            |
 |  .clj     +---->|  Cider    +------+   Ring    +-------------------->|            |
 |           |     |           |  |   |           |     |     AJAX      |            |
 +-----------+     +-----------+  |   +-----------+     |               |            |
                                  |       ^             |               |            |
                                  |       |             |               |  Browser   |
                                  |       |             |               |            |
       +----------------------------------+             |               |            |
       |                          |                     |               |            |
 +-----+-----+     +-----------+  |   +-----------+     |               |            |
 |           |     |           |  |   |           |     |               |            |
 |  .cljs    +---->|  Cider    +------+  Austin   +---------------------+            |
 |           |     |           |  |   |           |     |    REPL       |            |
 +-----------+     +-----------+  |   +-----------+     |  JS updates   |            |
                                  |                     |               +------------+
                                  +---------------------+

edit

App description

Simple Leaflet map showing all the bike racks in Denver, using data from open data services (OpenStreetMap, Denver Open Data)

Technically, also want to prove:

  • ClojureScript/Clojure client/server workflow
  • Refreshless development
  • Non-trivial interop with extant JS libs

Caveats:

  • This isn't the best pure ClojureScript way to build a front end. Consider Om, core.async, and simple idiomatic template libs.

Walkthrough

Less points

app.clj:

;; in requires:
[ring.middleware.params :refer [wrap-params]]

;;
(defn keep-n-features [feature-collection n]
  (assoc feature-collection
    :features (vec (take (Integer. n) (:features feature-collection)))))

(defroutes api-routes
  (GET "/bike_racks/" [n]
    (-> all-bike-racks
        (keep-n-features (or n 1))
        json/write-str
        resp/response)))

Test by running (retrieve-geojson) in den-of-cljs.js-app.

Backbone events

backbone-externs.js:

Backbone.on;
Backbone.trigger;

js-app.cljs:

(defn retrieve-geojson
  ([count]
     (let-ajax [geojson {:url (str "/api/bike_racks/?n=" count)}]
               (swap! model (fn [m] (assoc m :feature-geojson geojson)))))
  ([] (retrieve-geojson 1000)))

(defn ^:export init []
  (.on js/Backbone "change-count" (fn [& args]
                           (retrieve-geojson (first args))))
  (leaflet-map/init! model)
  (leaflet-map/start!)
  (retrieve-geojson))

Demo with (.trigger js/Backbone "change-count" 20)

Ideas for other walkthrough steps

  1. Testing ClojureScript
  2. ClojureScript MVC/MVP + idiomatic patterns for a SPWA
  3. Basic DOM manipulation
  4. Events
  5. AJAX
  6. Deployment to Heroku
  7. Remote debugging (and hot fixes?)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment