INDEX | HOME

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:

We make the following assumptions:

ⓘ 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.

Copyright 2024 Joseph Graham (joseph@xylon.me.uk)