Building a web app in Clojure(Script)
Using the Chestnut leiningen template
part 1
*
*
New project…
lein new chestnut clojure-ireland -- --om-tools --site-middleware
New project…
lein new chestnut clojure-ireland -- --om-tools --site-middleware
New leiningen project
New project…
lein new chestnut clojure-ireland -- --om-tools --site-middleware
New leiningen project using the chestnut template
New project…
lein new chestnut clojure-ireland -- --om-tools --site-middleware
New leiningen project using the chestnut template, called clojure-ireland
New project…
lein new chestnut clojure-ireland -- --om-tools --site-middleware
New leiningen project using the chestnut template, called clojure-ireland, with the following chestnut options:
New project…
lein new chestnut clojure-ireland -- --om-tools --site-middleware
New leiningen project using the chestnut template, called clojure-ireland, with the following chestnut options:
• om-tools Makes using Om a bit easier
New project…
lein new chestnut clojure-ireland -- --om-tools --site-middleware
New leiningen project using the chestnut template, called clojure-ireland, with the following chestnut options:
• om-tools Makes using Om a bit easier
• site-middleware Adds session support and more
Directory Structure
$ ls
env LICENSE Procfile project.clj README.md resources src sytem.properties
Directory Structure
$ ls
env LICENSE Procfile project.clj README.md resources src sytem.properties
Build-specific stuff goes in env
Eg, code you only want in developmentor code you only want in production
Directory Structure
$ ls
env LICENSE Procfile project.clj README.md resources src sytem.properties
Project settings go in project.clj
Eg, dependencies, build profiles,leiningen plugins, etc
Directory Structure
$ ls
env LICENSE Procfile project.clj README.md resources src sytem.properties
Static resources go here
HTML, CSS, javascript, images, fonts, …
Directory Structure
$ ls
env LICENSE Procfile project.clj README.md resources src sytem.properties
All of your code goes into src
Getting Started
$ lein repl
… lots of output …
clojure-ireland.server=> (run)
… more output …
Now open localhost:10555
clojure-ireland.server=>
Making Changes
$ vim src/cljs/clojure_ireland/core.cljs
Making Changes
$ vim src/cljs/clojure_ireland/core.cljs
ClojureScript source files
Making Changes
$ vim src/cljs/clojure_ireland/core.cljs
Project namespace
Making Changes
$ vim src/cljs/clojure_ireland/core.cljs
Main source file
(application entry point)
Making Changes
(ns clojure-ireland.core
(:require [om.core :as om :include-macros true]
[om-tools.dom :as dom :include-macros true]
[om-tools.core :refer-macros [defcomponent]]))
Making Changes
(ns clojure-ireland.core
(:require [om.core :as om :include-macros true]
[om-tools.dom :as dom :include-macros true]
[om-tools.core :refer-macros [defcomponent]]))
Project namespace declaration
Making Changes
(ns clojure-ireland.core
(:require [om.core :as om :include-macros true]
[om-tools.dom :as dom :include-macros true]
[om-tools.core :refer-macros [defcomponent]]))
Om
Making Changes
(ns clojure-ireland.core
(:require [om.core :as om :include-macros true]
[om-tools.dom :as dom :include-macros true]
[om-tools.core :refer-macros [defcomponent]]))
dom functions, used to generate HTMLEg (dom/div {:class “foo”} “My new div!”)
=
<div class=“foo”>My new div!</div>
Making Changes
(ns clojure-ireland.core
(:require [om.core :as om :include-macros true]
[om-tools.dom :as dom :include-macros true]
[om-tools.core :refer-macros [defcomponent]]))
defcomponent macro = easier Om components
Making Changes
(defonce app-state (atom {:text “Hello Chestnut!”}))
Making Changes
(defonce app-state (atom {:text “Hello Chestnut!”}))
This is our applications state
Making Changes
(defonce app-state (atom {:text “Hello Chestnut!”}))
defonce means it will NOT get reloaded as you edit code
Making Changes
(defn main []
(om/root
(fn [data owner]
(reify
om/IRender
(render [_]
(dom/h1 (:text data)))))
app-state
{:target (. js/document (getElementById “app”))}))
Making Changes
(defn main []
(om/root
(fn [data owner]
(reify
om/IRender
(render [_]
(dom/h1 (:text data)))))
app-state
{:target (. js/document (getElementById “app”))}))
Making Changes
(defn main []
(om/root
(fn [data owner]
(reify
om/IRender
(render [_]
(dom/h1 (:text data)))))
app-state
{:target (. js/document (getElementById “app”))}))
Making Changes
(defn main []
(om/root
(fn [data owner]
(reify
om/IRender
(render [_]
(dom/h1 (:text data)))))
app-state
{:target (. js/document (getElementById “app”))}))
A New Component
(defcomponent container
[data]
(render [_]
(dom/div
“Test”)))
A New Component
(defn main []
(om/root
(fn [data owner]
(reify
om/IRender
(render [_]
(om/build container data))))
app-state
{:target (. js/document (getElementById “app”))}))
A New Component
(defcomponent container
[data]
(render [_]
(dom/div
“Test”)))
Hit save!
And like magic…
React Dev Tools
React Dev Tools
Component Tree
React Dev Tools
Our component
(defcomponent container
[data]
(render [_]
(dom/div
“Test”)))
React Dev Tools
The div
(defcomponent container
[data]
(render [_]
(dom/div
“Test”)))
React Dev Tools
Componentproperties andstate
React Dev Tools
Components viewof app-state
An om-tools shortcut
(defn main []
(om/root
(fn [data owner]
(reify
om/IRender
(render [_]
(->container data))))
app-state
{:target (. js/document (getElementById “app”))}))
The CSS
.box {
border: 1px solid;
margin: 10px;
padding: 5px;
}
.box.inline div {
display: inline;
padding-right: 10px;
}
(defcomponent container
[data]
(render [_]
(dom/div
(dom/div
{:class “box”}
“A”)
(dom/div
{:class “box”}
“B”))))
(defcomponent container
[data]
(render [_]
(dom/div
(dom/div
{:class “box”}
“A”)
(dom/div
{:class “box”}
“B”))))
New app-state
(defonce app-state (atom {:a {:label “A”
:value “a”}
:b {:label “B”
:value “b”}}))
Press refresh!
A new component
(defcomponent labelled-data
[data]
(render [_]
(dom/div
{:class “box”}
(dom/div
(:label data))
(dom/input
{:value (:value data)}))))
Updated container
(defcomponent container
[data]
(render [_]
(dom/div
(->labelled-input
(:a data))
(->labelled-input
(:b data)))))
labelled-data component
(defcomponent labelled-data
[data owner]
(render [_]
(dom/div
{:class “box”}
(dom/div
(:label data))
(dom/input
{:value (:value data)}))))
labelled-data render
(defcomponent labelled-data
[data owner]
(render [_]
(dom/div
{:class “box”}
(dom/div
(:label data))
(dom/input
{:value (:value data)}))))
labelled-data render(dom/div
{:class “box”}
(dom/div
(:label data))
(dom/input
{:value (:value data)
:ref “val”
:onChange #(om/update!
data :value
(.-value (om/get-node owner “val”))}))
labelled-data render(dom/div
{:class “box”}
(dom/div
(:label data))
(dom/input
{:value (:value data)
:ref “val”
:onChange #(om/update!
data :value
(.-value (om/get-node owner “val”))}))
labelled-data render(dom/div
{:class “box”}
(dom/div
(str “*” (:label data) “*”))
(dom/input
{:value (:value data)
:ref “val”
:onChange #(om/update!
data :value
(.-value (om/get-node owner “val”))}))
labelled-data render(dom/div
{:class “box”}
(dom/div
(str “*” (:label data) “*”))
(dom/input
{:value (:value data)
:ref “val”
:onChange #(om/update!
data :value
(.-value (om/get-node owner “val”))}))
One last change to app-state
(defonce app-state (atom {:a {:label “Planning”
:value 0}
:b {:label “Design”
:value 0}}))
Press refresh!
labelled-data render(dom/div
{:class “box”}
(dom/div
(:label data))
(dom/input
{:value (:value data)
:ref “val”
:onChange #(om/update!
data :value
(js/parseInt
(.-value (om/get-node owner “val”)))}))
labelled-data render(dom/div
{:class “box”}
(dom/div
(:label data))
(dom/input
{:value (:value data)
:ref “val”
:onChange #(om/update!
data :value
(js/parseInt
(.-value (om/get-node owner “val”)))}))
Helper function(defn percent
[{v1 :value} {v2 :value}]
(str (* (/ v1 (+ v1 v2)) 100) “%”))
labelled-value component(defcomponent labelled-value
[data]
(render-state [_ state]
(dom/div
{:class “box inline”}
(dom/div
(:label state))
(dom/div
data))))
container render(dom/div
(->labelled-input
(:a data))
(->labelled-input
(:b data))
(->labelled-value
“Test”
{:state {:label “Planning”}}))
container render(dom/div
(->labelled-input
(:a data))
(->labelled-input
(:b data))
(->labelled-value
“Test”
{:state {:label “Planning”}}))
container render(dom/div
(->labelled-input
(:a data))
(->labelled-input
(:b data))
(->labelled-value
(percent (:a data) (:b data))
{:state {:label “Planning”}}))
container render(dom/div
(->labelled-input
(:a data))
(->labelled-input
(:b data))
(->labelled-value
(percent (:a data) (:b data))
{:state {:label “Planning”}}))
container render(dom/div
(->labelled-input
(:a data))
(->labelled-input
(:b data))
(->labelled-value
(percent (:a data) (:b data))
{:state {:label “Planning”}})
(->labelled-value
(percent (:b data) (:a data))
{:state {:label “Design”}}))
container render(dom/div
(->labelled-input
(:a data))
(->labelled-input
(:b data))
(->labelled-value
(percent (:a data) (:b data))
{:state {:label “Planning”}})
(->labelled-value
(percent (:b data) (:a data))
{:state {:label “Design”}}))
container component(defcomponent container
[data]
(did-update [_ pp ps]
(if (> (get-in data [:a :value])
(get-in data [:b :value]))
(om/update! data [:a :warn] true)
(om/update! data [:a :warn] false)))
(render [_]
…
labelled-data render(dom/div
{:class “box”}
(dom/div
(:label data))
(dom/input
{:value (:value data)
:style {:color (if (:warn data) “red” “black”)}
:ref “val”
:onChange #(om/update!
data :value
(js/parseInt
(.-value (om/get-node owner “val”)))}))
labelled-data render(dom/div
{:class “box”}
(dom/div
(:label data))
(dom/input
{:value (:value data)
:style {:color (if (:warn data) “red” “black”)}
:ref “val”
:onChange #(om/update!
data :value
(js/parseInt
(.-value (om/get-node owner “val”)))}))
labelled-value component(defcomponent labelled-value
[data]
(render-state [_ state]
(dom/div
{:class “box inline”
:style {:color (if (:warn state) “red” “black”)}}
(dom/div
(:label state))
(dom/div
data))))
container render(dom/div
(->labelled-input
(:a data))
(->labelled-input
(:b data))
(->labelled-value
(percent (:a data) (:b data))
{:state {:label “Planning”
:warn (get-in data [:a :warn])}})
(->labelled-value
(percent (:b data) (:a data))
{:state {:label “Design”}}))
container render(dom/div
(->labelled-input
(:a data))
(->labelled-input
(:b data))
(->labelled-value
(percent (:a data) (:b data))
{:state {:label “Planning”
:warn (get-in data [:a :warn])}})
(->labelled-value
(percent (:b data) (:a data))
{:state {:label “Design”}}))
Building a web app in Clojure(Script)
Using the Chestnut leiningen template
End of Part One