Rapid Recipe - ClojureScript with Shadow CLJS
Table of Contents
This article just covers how to quickly make a basic app with Shadow CLJS.
We will:
- build a Hello World
- setup our development environment and editor
- then we will build a workout dice app
- finally we add tests
We make the following assumptions:
- you have a project directory with git setup
- you have npm installed on your machine
ⓘ in the examples our project is called "workout-dice"
Before we proceed, open and bookmark the shadow-cljs userguide.
1. ClojureScript Hello World
1.1. Install Shadow-CLJS
There is a node utility for making a shadow-cljs project, but we do it manually because we want to be able to set shadow up alongside a deps.edn project.
Add these to your .gitignore
:
/node_modules/ .shadow-cljs /resources/public/cljs/
Make these directories:
mkdir src/cljs/workout_dice/ test/cljs/workout_dice/
We will be using the standalone version of shadow-cljs.
Run these commands in your project directory:
npm init -y npm install --save-dev shadow-cljs
Commit package.json
and package-lock.json
to git.
Now check you can run shadow-cljs using your shell:
npx shadow-cljs help
✎ changes
1.2. Add our ClojureScript
Now we make the config. Make a shadow-cljs.edn
{:source-paths ["src/cljs" "test/cljs"] :dependencies [] :dev-http {3010 "resources/public/"} :builds {:app {:output-dir "resources/public/cljs/" :asset-path "cljs" :target :browser :modules {:main {:init-fn workout-dice.hello/main!}}}}}
Make a src/cljs/workout_dice/hello.cljs
:
(ns workout-dice.hello) (defn main! [] (let [container (.getElementById js/document "dynamiccontent") paragraph (.createElement js/document "p")] (set! (.-textContent paragraph) "Hello World!") (.appendChild container paragraph)))
Now build with:
npx shadow-cljs compile app
ⓘ There is also a list of commonly used commands here.
✎ changes
1.3. Add a page to demo your code
Now just make an html page for your demo.
This must have a div with id dynamiccontent
, and a <script>
tag
which loads the compiled script.
1.3.1. If you want to serve it yourself
If you already have the capability of serving HTML pages and static
files (for example you have a deps.edn project with
ring.middleware.resource), you just need to ensure resources/public
is being served.
Now load your page and check it works!
1.3.2. If you need node to serve it
You can use the shadow-cljs dev server. In that case you just need to
create your resources/public/index.html
like this example:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Dummy HTML Document</title> </head> <body> <div id="dynamiccontent"> <!-- Content will be dynamically loaded here --> </div> <script src="/cljs/main.js"></script> </body> </html>
Now start the shadow-cljs development server.
npx shadow-cljs watch app
Now in your browser access http://0.0.0.0:3010/
✎ changes
2. Setup your editor
I don't cover this as it depends on your editor. So read the docs here.
3. Setup hot-reload
You will want hot reload while you are doing development.
You don't need any extra tools for this. Just leave the watch running in a terminal:
npx shadow-cljs watch app
Then add lifecycle hooks to your code so your JS knows how to reload on change. Read the docs here.
Here is an example of doing this:
(ns workout-dice.hello) (defn add-contents [] (let [container (.getElementById js/document "dynamiccontent") paragraph (.createElement js/document "p")] (set! (.-textContent paragraph) "Hello World!") (.appendChild container paragraph))) (defn reset-contents [] (let [container (.getElementById js/document "dynamiccontent")] (set! (.-innerHTML container) ""))) (defn main! [] (add-contents) ) (defn ^:dev/before-load stop [] (js/console.log "stopping") (reset-contents)) (defn ^:dev/after-load start [] (js/console.log "starting") (main!))
Now edit "Hello World!" to something else and it should update on the page.
✎ changes
4. Setup our workout dice app
This is the easiest part. We will add a button which will print a random excercise to the screen.
First add a button before the dynamiccontent div:
<button id="myButton" >Click Me</button>
Re-write your main!
function to add an event listener to that button:
(defn main! [] (let [button (.getElementById js/document "myButton")] (.addEventListener button "click" (fn [] (reset-contents) (add-contents)))))
Define your list of excercises:
(def exercises ["Pushups" "Squats" "Lunges" "Plank" "Dips" "Burpees"])
And edit your add-contents function, replacing "Hello World!" with:
(rand-nth exercises)
Now when you click the button it will randomly select an excercise!
✎ changes
5. Testing
You will want to test your clojurescript app.
5.1. Write your test file
Create a file test/cljs/workout_dice/hello_test.cljs
.
Add this to your requires to get the ability to run tests:
[clojure.test :refer [is deftest run-tests]]
And also require the functions you want to test from your main namespace. We will just test that excercises contains the ones we want:
[workout-dice.hello :refer [exercises]]
Great now write our test:
(deftest test-pushups ;; are there at-least 6 excercises? (is (>= (count exercises) 6) ;; is pushups included? (is (some #{"Pushups"} exercises))))
And at the end of the file add:
(run-tests)
5.2. Run the tests
Add a test build to your shadow-cljs.edn
as documented:
:test {:target :node-test :output-to "target/test.js" :ns-regexp "-test$" :autorun true}
This is a node-test which means it doesn't require a browser to work.
Now just compile your test target:
npx shadow-cljs compile test
✎ changes
6. Release
To build your release code, simply run:
npx shadow-cljs release app
This strips out the development code and optimizes using the Google Closure Compiler.
7. Notes
If you are using CIDER REPL and when you switch namespace you end up
on a ghost realm where nothing works, this may be because you used
C-c M-j
. You have to use C-c C-x j s
.