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.

$ ( '#my-button ) .click ( function ( ) { .. . } ) ( dommy/append! ( sel1 :#todos ) todo-element ) ( dommy/listen! ( sel1 :#my-button ) :click click-handler )

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!

Runs its own server to load assets

Watches and compiles your files

Pushes changes to the browser

Sets up a repl into your browser

Can co-exist with your regular server in development

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

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

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

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

Reagent Cookbook

re-com library of UI Components

Future

Routing (beyond Secretary)

Syncing data with server

Enhancements to RE-Frame / Flux

OM Next - Relay / JSONGraph

Thank You

Questions?