Clojure Re-Frame Exercise

Clojure Re-Frame Exercise in IntelliJ IDEA / Cursive IDE.

Introduction

What is ClojureScript?

What are Reagent and Re-Frame?

Tooling

  • Shadow-cljs. Shadow-cljs is a build tool for ClojureScript. Shadow-cljs is really nice to use — you get to see your changes both in the ClojureScript code and Sass code in real-time in the browser. You can also get a REPL which runs your ClojureScript code in the browser (see the example in the picture above — I have a scratch file in which I have written some ClojureScript code and I send the forms for evaluation to the REPL running in the browser — you can e.g. examine the re-frame app db using the REPL). Shadow-cljs configuration, see: shadow-cljs.edn.
  • Justfile. I use Just to provide commandline interface to setup the project fixture before development, e.g. just postgres: start the PostgreSQL development database, just backend-kari: start backend repl with my own Clojure config, just frontend-kari: start frontend etc. Justfile configuration, see: Justfile.
  • Npm. Shadow-cljs integrates nicely with Npm. See package.json for a list of npm packages I’m using in this exercise.
  • Sass. I’m using Sass which is an extension language for CSS. The Sass in this exercise is pretty minimalistic — the purpose of this exercise was not to create a beautiful frontend but to learn to use the tooling and re-frame. See example in main.scss.
  • Deps.edn. In the deps.edn file you can find the frontend alias which gives the dependencies for the frontend in this exercise.
  • Metosin Reagent-dev-tools. I used quite a lot the excellent Metosin Reagent-dev-tools (as you can see in the picture below). I tried the re-frame-10x tool as well, but I liked more the Metosin Reagent-dev-tool’s visual layout (and I’m a company man — you eat your own dog food). The Metosin Reagent-dev-tool was one of the most important development and debugging tools during this exercise. Once you configure the tool to show the Re-frame application db state you get a nice tree view to the app-db:
(defn ^:export init []
(js/console.log "ENTER init")
(re-frame/dispatch-sync [::initialize-db])
(dev-tools/start! {:state-atom re-frame.db/app-db})
(dev-setup)
(start))

Routing

(def routes-dev
["/"
[""
{:name ::sf-state/home
:view home-page
:link-text "Home"
:controllers
[{:start (fn [& params] (js/console.log "Entering home page"))
:stop (fn [& params] (js/console.log "Leaving home page"))}]}]
["signin"
{:name ::sf-state/signin
:view sf-signin/signin-page
:link-text "Sign-In"
:controllers
...

Using Re-Frame

(defn products-page
"Products view."
[match] ; NOTE: This is the current-route given as paramter to the view. You can get the pgid also from :path-params.
(let [_ (sf-util/clog "ENTER products-page, match" match)
{:keys [path]} (:parameters match)
{:keys [pgid]} path
pgid (str pgid)
_ (sf-util/clog "path" path)
_ (sf-util/clog "pgid" pgid)]
(fn []
(let [products-data @(re-frame/subscribe [::products-data pgid])
product-group-name @(re-frame/subscribe [::product-group-name pgid])
_ (if-not products-data (re-frame/dispatch [::get-products pgid]))]
[:div
[:h3 "Products - " product-group-name ]
[:div.sf-pg-container
(products-table products-data)]
[:div
[:button.sf-basic-button
{:on-click (fn [e]
(.preventDefault e)
(re-frame/dispatch [::sf-state/navigate ::sf-state/home]))}
"Go to home"]
]
(sf-util/debug-panel {:products-data products-data})]))))
(re-frame/reg-event-fx
::get-products
(fn [{:keys [db]} [_ pg-id]]
(sf-util/clog "get-product, pg-id" pg-id)
(sf-http/http-get db (str "/api/products/" pg-id) nil ::ret-ok ::ret-failed)))
(re-frame/reg-event-db
::ret-ok
(fn [db [_ res-body]]
(sf-util/clog "reg-event-db ok: " res-body)
(let [pgid (:pg-id res-body)]
(-> db
(assoc-in [:products :response] {:ret :ok :res-body res-body})
(assoc-in [:products :data pgid] (:products res-body))))))
(re-frame/reg-sub
::products-data
(fn [db params]
(sf-util/clog "::products-data, params" params)
(let [pgid (second params)
data (get-in db [:products :data])
_ (sf-util/clog "products-data" data)]
(get-in data [pgid]))))

Development Flow

Browser view and development tools.
(defn debug-panel
"Debug panel - you can use this panel in any view to show some page specific debug data."
[data]
(let [debug @(re-frame/subscribe [::sf-state/debug])]
#_(js/console.log (str "ENTER debug-panel, debug: " debug))
(if debug
[:div.sf-debug-panel
[:hr.sf-debug-panel.hr]
[:h3.sf-debug-panel.header "DEBUG-PANEL"]
[:pre.sf-debug-panel.body (with-out-str (clojure.pprint/pprint data))]])))
(defn login-page
"Login view."
[]
...
(sf-util/debug-panel {:login-data login-data
:ret ret
:msg msg
:r-body r-body})]))))
Custom Debug Panel.

Styles

.sf-debug-panel {
.hr {
margin-top: 100px;
}
.header {
font-size: 28px;
}
.body {
font-size: 12px;
}
}

Issues

Live Reloading

Conclusions

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Kari Marttila

I’m a Software architect and developer. Currently implementing systems on AWS / GCP / Azure / Docker / Kubernetes using Java, Python, Go and Clojure.