Notes from a ClojureScript workshop for the Portland ClojureScript meetup group. We used CLJS, Reagent, Figwheel, re-frame to build a simple working single page app to discuss related concepts.

Code is pdxbike-final and pdxbike-reframe.


ClojureScript (CLJS)

Clojure that targets JavaScript. All (-most all of) the tasty goodness of Clojure with the reach of JS. Runs in the browser and on NodeJS.


Direct DOM Manipulation

First Option is to directly manipulate the DOM and add event listeners ala JQuery.


ReactJS

A Javascript library for building user interfaces.

Current popular Clojure options to wrap ReactJS:

Great overview of the three frameworks: * Luke VanderHart - The ReactJS Landscape (video) Other options: Brutha


Reagent

Minimalistic and Clojure-esque. Uses Hiccup syntax.

(defn simple-component []
  [:div
   [:p "I am a component!"]
   [:button {:on-click my-click-handler} "Push me"]
   [:p.someclass "I have some class"]
   [some-function]])

Tooling

Older tutorials use lein-cljsbuild (directly) but I usually use lein-figwheel.

Bruce’s ClojureWest Talk * Bruce Hauman - Developing ClojureScript With Figwheel (video)


FigWheel

Figwheel builds your ClojureScript code and hot loads it into the browser as you are coding!


What we will build

[![pdxbike screenshot](http://e-string.com/wp-content/uploads/2015/06/pdxbike-300x277.jpg)](http://e-string.com/wp-content/uploads/2015/06/pdxbike.jpg)
screenshot

Lets Get Started

lein new figwheel pdxbike  -- --reagent 
cd pdxbike
open . 
lein figwheel
open http://localhost:3449

Open your browser’s console. * Look for the printed message * Change the message and the text in app-state * Reload the browser * swap! into the app-state atom in the repl

<link rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">

<div id="app" class="container">
  <h2>Figwheel template</h2>
  <p>Checkout your developer console.</p>
</div>

Add A button

  [:div
   [:h1 (:text @app-state)]
   [:button "Push Me"]]

Make it do something

   [:button {:on-click (fn [e] (println "I've been pushed"))} "Push Me"]

Make the action visible

   [:button {:on-click #(swap! app-state assoc :text (str "It is now " (js/Date.)))}
            "Push Me"]

A Counting Example

(ns ^:figwheel-always pdxbike.core
    (:require
              [reagent.core :as reagent :refer [atom]]))

(enable-console-print!)

(defonce app-state (atom {:text "Hello Portland"
                          :count 0}))

(defn increment-count [e]
  (swap! app-state update-in [:count] inc))

(defn my-page []
  [:div
   [:h1 (:text @app-state)]
   [:h2 (str "We've seen " (:count @app-state) " bikes.")]
   [:button {:on-click increment-count} "There goes a bike"]])

(reagent/render-component [my-page]
                          (. js/document (getElementById "app")))

Multiple Counters

(defonce app-state (atom {:text "Hello Portland"
                          :counters {"abc" {:id "abc"
                                            :name "counter 1"
                                            :count 0}
                                     "def" {:id "def"
                                            :name "counter 2"
                                            :count 0}}}))

(defn increment-count [c]
  (swap! app-state update-in [:counters (:id c) :count] inc))

(defn counter-component [c]
  [:div
   [:h2 (str "We've seen " (:count c) " bikes at " (:name c))]
   [:button.btn.btn-success {:on-click #(increment-count c)} "Push Me"]])

(defn my-page []
  [:div
   [:h1 (:text @app-state)]
    [:h4 (str "We've seen " (apply + (map :count (vals (:counters @app-state))))
          " bikes total.")]
   (for [counter (vals (:counters @app-state))]
     ^{:key (:name counter)} [counter-component counter])])

(reagent/render-component [my-page]
                          (. js/document (getElementById "app")))

Cursors

(defn increment-count [c]
  (swap! c update-in [:count] inc))

(defn counter-component [c]
  [:div
   [:h2 (str "We've seen " (:count @c) " bikes at " (:name @c))]
   [:button.btn.btn-success {:on-click #(increment-count c)} "Push Me"]])

(defn my-page []
  [:div
    [:h1 (:text @app-state)]
    [:h4 (str "We've seen " (apply + (map :count (vals (:counters @app-state))))
          " bikes total.")]
   (for [k (keys (:counters @app-state))]
     ^{:key k} [counter-component (reagent/cursor app-state [:counters k])])])

Text Input

(defn handle-add-counter [tf]
  (let [val (:value @tf)]
    (if val
      (swap! app-state assoc-in [:counters val] {:id val :name val :count 0}))
    (swap! tf assoc :value nil)))

(defn add-counter-component []
  (let [private-state (atom {:value nil})]
    (fn []
      [:div
       [:input {:type :text
                :value (:value @private-state)
                :on-change
                  #(swap! private-state assoc :value (-> % .-target .-value))}]
       [:button.btn.btn-default
               {:on-click #(handle-add-counter private-state)} "Add Counter"]])))

(defn my-page []
  [:div
   [:h1 (:text @app-state)]
   [add-counter-component]
   (for [counter (vals (:counters @app-state))]
     ^{:key (:name counter)} [counter-component counter])])

Reagent Forms

Remove the tedium of managing an atom and a DOM field with click/change handlers.

(defn row [label input]
  [:div.row
    [:div.col-md-2 [:label label]]
    [:div.col-md-5 input]])

(def form-template
  [:div
   (row "first name" [:input {:field :text :id :first-name}])
   (row "last name" [:input {:field :text :id :last-name}])
   (row "age" [:input {:field :numeric :id :age}])
   (row "email" [:input {:field :email :id :email}])
   (row "comments" [:textarea {:field :textarea :id :comments}])])

(defn form []
  (let [doc (atom {})]
    (fn []
      [:div
       [:div.page-header [:h1 "Reagent Form"]]
       [bind-fields form-template doc]
       [:label (str @doc)]])))

Kioo

Have designers that don’t want to deal with Hiccup? Kioo brings Enlive/Enfocus style templates to React. This allows for much better separation between the view and logic layers of the application.


A closer look

Is app-state a global mutable variable? Is that a problem?

(defonce app-state (atom {:text "Hello Portland"
                          :counters {}}))

(defn increment-count [c]
  (swap! app-state update-in [:counters (:id c) :count] inc))

(defn counter-component [c]
  [:div
   [:h2 (str "We've seen " (:count c) " bikes at " (:name c))]
   [:button.btn.btn-success {:on-click #(increment-count c)} "Push Me"]])

(defn my-page []
  [:div
   [:h1 (:text @app-state)]
    [:h4 (str "We've seen " (apply + (map :count (vals (:counters @app-state))))
          " bikes total.")]
   (for [counter (vals (:counters @app-state))]
     ^{:key (:name counter)} [counter-component counter])])

(reagent/render-component [my-page]
                          (. js/document (getElementById "app")))

A Sketch of an approach

Basic idea from OM tutorials.

(defonce app-state (atom {:text "Hello Portland"
                          :counters {}}))

;; somewhere create a channel and pass it around
(def my-chan (chan))

;; somewhere set up a go loop to process messages off of the channel
(go
  (loop [[cmd args] (<! my-chan)]
    ;; handle command
    (case cmd
      :increment-counter (...)
      ;; ... )
    (recur (<! my-chan))))))

;; event handlers simply put messages on the channel
(defn increment-count [c]
  (put! my-chan [:increment-counter c]))

Re-Frame

Much information. Such Overload. Wow. Seriously. Read the README.md


Components

;; ==== Components
(defn counter-component [c]
  [:div
   [:h2 (str "We've seen " (:count c) " bikes at " (:name c))]
   [:button.btn.btn-success {:on-click #(rf/dispatch [:increment-count c])} "Push Me"]]
)

(defn my-page []
  (let [counters (rf/subscribe [:all-counters])]
    (fn []
      [:div
       [:h1 "RE-Frame version"]
       [:h4 (str "We've seen " (apply + (map :count @counters)) " bikes total.")]
       (for [counter @counters]
         ^{:key (:id counter)} [counter-component counter])]))
)

Handlers

;; ==== Handlers
(rf/register-handler
 :increment-count
 (fn [db [cmd c]]
   (update-in db [:counters (:id c) :count] inc)))

(rf/register-handler
 :add-counter
 (fn [db [cmd n]]
   (assoc-in db [:counters n] {:id n :name n :count 0})))

(rf/register-handler
 :init
 (fn [db c]
   {:counters {"abc" {:id "abc" :name "counter 1" :count 0}
               "def" {:id "def" :name "counter 2" :count 0}}}))

Subscriptions

;; ==== Subscriptions
(rf/register-sub
 :all-counters
 (fn [db _]
   (reaction (vals  (:counters @db)))))

(rf/register-sub
 :most-popular-counter
 (fn [db _]
   (reaction (apply max-key :count  (vals (:counters @db))))))

(rf/register-sub
 :total-count
 (fn [db _]
   (reaction (apply + (map :count (vals (:counters @db)))))))

Other Reagent Resources

Future


Thank You

Questions?

@JulioBarros


Want to be automatically notified of more articles like this?