24 September 2021

Getting Started with ClojureScript

Is this a really tricky thing, or was it just me?

In recent weeks I've implemented a few different UIs for an application.

  1. Terminal (text-based)
  2. 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:

  1. "Ok, so a page was rendered, now...how to I write ClojureScript that updates the page?"
  2. This tutorial uses the build-in clj deps tool--how would I go about the same thing using lein?

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:

  1. compiles the clojurescript
  2. launches a browser and loads the index.html page
  3. 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:

Alert

...as well as the DOM of the running page:

H1

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

Code Repository

Phew! I hope you don't struggle as much as I did on this one...