In recent weeks I've implemented a few different UIs for an application.
- Terminal (text-based)
- Desktop GUI (drawing via quil/processing)
I'm about to embark on a web browser UI. I've only ever dabbled with front-end development in the past. I only know enough to know that I don't know very much, and that the JS ecosystem is in a constant state of flux.
Since I'm learning Clojure that eventually means working with ClojureScript + Reagent (React wrapper), but in an effort to NOT skip important steps and understand the platform I'll be building on, I wanted to experience ClojureScript without Reagent (or even React). How hard could it be?
Hmm. Well, it's surprisingly difficult to find a tutorial that does anything meaningful in plain clojurescript before diving head-first into Reagent. The official "Quick Start" got me to a place where I found myself wondering:
- "Ok, so a page was rendered, now...how to I write ClojureScript that updates the page?"
- This tutorial uses the build-in
clj
deps tool--how would I go about the same thing usinglein
?
I seriously wrestled with these questions for a few days, all the while debating whether to just jump into reagent. Finally, I found a tutorial that, while it introduced reagent pretty early, it actually abandoned it later on to allow the reader to delve into plain ClojureScript. I learned a ton along the way, and plan to continue. The book is "Learn ClojureScript", by Andrew Meredith.
https://www.learn-clojurescript.com/
So, that book got me to a confident enough place that I could then integrate what he taught using whatever build tool and template I wanted to use. "Learn ClojureScript" used Sean Corfield's clj-new
for bootstrapping the project. This approach was fairly easy to setup, but didn't use lein
, which is a standard tool around here.
So, here's how to get up and running with the lein
template called mies
.
$ lein new mies <project-name>
For this tutorial, the <project-name>
I used was spike-mies-2
.
$ lein new mies spike-mies-2
...
$ cd spike-mies-2
At that point your directory structure looks like this:
$ tree
.
|____.gitignore
|____index.html
|____index_release.html
|____project.clj
|____README.md
|____scripts
| |____repl.clj
| |____release.clj
| |____repl
| |____release.bat
| |____release
| |____repl.bat
| |____build.bat
| |____watch.clj
| |____watch
| |____watch.bat
| |____build.clj
| |____brepl.bat
| |____build
| |____brepl.clj
| |____brepl
|____src
| |____spike_mies_2
| | |____core.cljs
The README.md
gives direction about running the scripts, which do the dirty work of compiling the ClojureScript and setting up REPL connections. These scripts, while straight-forward, seem a bit 'magical', since they are generated and invoke ClojureScript api functions to do the build, rather than just using the command line like the "Quick Start", but they do the job, and are about as simple as I could find among the lein
bootstrapping options.
When you run ./scripts/build
it actually compiles the ClojureScript code and makes it available to the index.html
page, which you should then load in any web browser. Then, access the developer/inspector tools and find the console. You should see "Hello world!" there, which was emitted by the code in src/spike_mies_2/core.cljs
. Try changing that message, re-run ./scripts/build
, and re-load the browser and you should see the change. This is proof that the compiler is working and that the browser is loading the ClojureScript code which has been compiled to javascript.
But, how do you actually make changes to the page from ClojureScript code? This was the big question, and it didn't get answered for me until chapter/lesson 7 of "Learn ClojureScript" (which shows you how to integrate with the browser javascript APIs) and chapter/lesson 14 (which demonstrates lots of browser I/O operations).
It turns out that ClojureScript ships with a library that wraps over a Google javascript library called "Closure" (which is a pretty unfortunate naming collision).
cljs.user=> (require '[goog.dom :as gdom])
nil
cljs.user=> (def body (.querySelector js/document "body"))
#'cljs.user/body
cljs.user=> (def heading (gdom/createElement "h1"))
#'cljs.user/heading
cljs.user=> (gdom/setTextContent heading "I am new")
nil
cljs.user=> (gdom/appendChild body heading)
nil
Why nobody gives any examples of how to interact with the JavaScript API or the Google Closure API wrapper is beyond me.
If you uncomment the commented stuff in src/spike_mies_2/core.cljs
you can then run ./scripts/brepl
, which does the following:
- compiles the clojurescript
- launches a browser and loads the index.html page
- establishes a REPL in your terminal that is connected to the browser environment just loaded.
The REPL can be used to experiment with the javascript API:
...as well as the DOM of the running page:
I managed to implement the password comparer/validator from the book without too much trouble.
(ns spike-mies-2.core
(:require
[goog.dom :as gdom]
[goog.events :as gevents]))
(defn values-same? [field-1 field-2]
(= (aget field-1 "value")
(aget field-2 "value")))
(defn handle-change [password confirmation status]
(gdom/setTextContent
status
(if (values-same? password confirmation)
"Matches"
"Do not match")))
(let [password (gdom/createElement "input")
confirm (gdom/createElement "input")
status (gdom/createElement "p")
app (gdom/getElement "app")]
(gdom/setProperties password #js {"type" "password"})
(gdom/setProperties confirm #js {"type" "password"})
(gevents/listen password "keyup" #(handle-change password confirm status))
(gevents/listen confirm "keyup" #(handle-change password confirm status))
(gdom/setTextContent app "")
(gdom/appendChild app password)
(gdom/appendChild app confirm)
(gdom/appendChild app status))
Phew! I hope you don't struggle as much as I did on this one...