/img/cljs.png

Adding ClojureScript to a Clojure Speclj Project: Part 1

Setting up a pre-exsiting project to be able to TDD ClojureScript with Speclj

Written by: Alex Root-Roatch | Friday, September 6, 2024

This week I'm working on adding a ClojureScript/React UI to my Clojure tic-tac-toe application. The past two days have been filled with setting up my repository to have a working ClojureScript development environment, including Speclj for TDD and a development server. This two part post will detail the steps I took to get everything working. Big thank-yous to Jake Ogden, Alex Jensen, and Brandon Correa for the guidance and troubleshooting throughout the process.

Change Directory Structure

The first step was to set up my project's directory structure to accommodate ClojureScript. My file tree before adding ClojureScript was:

├── spec
│   ├── tic_tac_toe
│   │   ├── [test files and packages]
├── src
│   ├── tic_tac_toe
│   │   ├── [source files and packages]
├── deps.edn
├── README.md
└── .gitignore

With adding ClojureScript, I'll be dealing with three different Clojure extensions, .clj, .cljs for ClojureScript, and .cljc for Clojure Commons, which will be code that is shared between the ClojureScript version of the app and the terminal and desktop versions. It's important to keep all of this organized, so I changed my directory structure to:

├── spec
│   ├── clj
│   │   ├── tic_tac_toe
│   │   │   ├── [test files and packages]
│   ├── cljc
│   │   ├── tic_tac_toe
│   │   │   ├── [test files and packages]
│   ├── cljs
│   │   ├── tic_tac_toe
│   │   │   ├── [test files and packages]
├── src
│   ├── clj
│   │   ├── tic_tac_toe
│   │   │   ├── [test files and packages]
│   ├── cljc
│   │   ├── tic_tac_toe
│   │   │   ├── [test files and packages]
│   ├── cljs
│   │   ├── tic_tac_toe
│   │   │   ├── [test files and packages]
├── deps.edn
├── README.md
└── .gitignore

With this new structure, the spec and src directories are no longer marked as "Test Sources Root" and "Sources Root", but instead each folder for each Clojure file type is, resulting in three Test Sources Root folders and three Sources Root folders.

To make sure the project knows where to look for the files, the new paths need to be added to deps.edn:

{
:paths     ["src/clj" "src/cljs" "src/cljc"]
:aliases   :test {:extra-paths ["spec/clj" "spec/cljs" "spec/cljc"]}
}

At this point, I reran all of my tests to make sure everything worked correctly after changing directory structure. The cljc and cljs folders were empty at this point and all of my project was inside the clj folders.

Adding New Dependencies

The next step was bringing in the new dependencies needed for writing, compiling, and testing ClojureScript. I added the following to my deps.edn:

{
 :deps      {
             cljsjs/react                      {:mvn/version "17.0.2-0"}
             cljsjs/react-dom                  {:mvn/version "17.0.2-0"}
             com.cleancoders.c3kit/bucket      {:mvn/version "2.1.3"}
             reagent/reagent                   {:mvn/version "1.2.0"}
             com.google.jsinterop/base         {:mvn/version "1.0.1"}
             com.cleancoders.c3kit/wire        {:mvn/version "2.1.4"}
             }

 :aliases   {
             :test {
                    :extra-deps  {
                                  org.clojure/clojurescript      {:mvn/version "1.11.132"}
                                  com.google.jsinterop/base      {:mvn/version "1.0.1"}
                                  com.cleancoders.c3kit/scaffold {:mvn/version "2.0.3"}
                                  }
                    }
             :cljs {:main-opts ["-m" "c3kit.scaffold.cljs"]}
             }
 }

Scaffold is a Clean Coders library for compiling ClojureScript and running Speclj tests in JavaScript using headless Chrome. Wire is a Clean Coders library that provides useful functions for rendering components to the DOM for testing and using JavaScript interop in a more Clojure-idiomatic way.

The added :cljs alias makes it easy to run ClojureScript specs using clj -M:test:cljs.

Please note that the above example only shows what was added to the deps.edn file at this point and is not the full deps.edn file.

Adding the Resources Folder

Before being able to use Scaffold, though, I needed to create a resources directory with a configuration file for Scaffold to use. At the root of the project directory, I added:

├── resources
│   ├── config
│   │   ├── cljs.edn

In that configuration file, I put;

{:ns-prefix     "tic_tac_toe"
 :ignore-errors ["goog/i18n/bidi.js"]
 :development   {:cache-analysis true
                 :optimizations  :none
                 :output-dir     "resources/public/cljs/"
                 :output-to      "resources/public/cljs/tic_tac_toe_dev.js"
                 :pretty-print   true
                 :sources        ["spec/cljs" "src/cljs"]
                 :specs          true
                 :verbose        true
                 :watch-fn       c3kit.scaffold.cljs/on-dev-compiled
                 :parallel-build true
                 }
 }

This is telling Scaffold to watch the spec/cljs and src/cljs folders for changes, searching for all files that have "tic_tac_toe" as their namespace prefix. It also tells Scaffold to put the compiled code into the " resources/public/cljs" folder and the compiled JavaScript that will ship to the client into a file called " tic_tac_toe_dev.js". Both the folder and the file will be created automatically if they don't already exist.

CAUTION: Notice the namespace prefix "tic_tac_toe" uses underscores. One thing that's confusing about Clojure is that, while namespaces can have hyphens in the namespace declaration at the top of the file, the package names use underscores. For this configuration file, it's important to match the package name, not how the namespace appears in the Clojure file.

I started out having it as "tic-tac-toe", and this caused a problem where the tests would run if I ran them with the " once" argument, but they would not run at all when using the auto runner (which is the default).

In order for Clojure to have access to the resources folder, I needed to add it to my paths in deps.edn. Here's all the paths up to this point:

:paths     ["src/clj" "src/cljs" "src/cljc" "resources"]

Adding the First ClojureScript Files

The next step was actually creating a ClojureScript file and accompanying test file to get started.

I created src/cljs/tic_tac_toe/main.cljs and spec/cljs/tic_tac_toe/main_spec.cljs.

main.cljs

(ns tic-tac-toe.main
  (:require [reagent.dom :as rdom]
            [c3kit.wire.js :as wjs]))

(defn app []
  [:div
   {:id "bob"}
   [:a {:href "/"}]])

(defn ^:export main []
  (rdom/render [app] (wjs/element-by-id "app")))

Here I've created a basic dummy component called app that renders a div with an id of "bob" and an a tag with an href pointing to the root of the directory.

The main function takes in a component and renders it to the DOM by selecting an empty div with an id of "app" and injecting the rendered JavaScript and HTML into the page. It's the ClojureScript equivalent to this:

ReactDOM.createRoot(document.getElementById('app')).render(
  <React.StrictMode>
    <App />
 </React.StrictMode>  
)

main_spec.cljs

(ns tic-tac-toe.main-spec
  (:require-macros [speclj.core :refer [should= it describe before]]
                   [c3kit.wire.spec-helperc :refer [should-not-select should-select]])
  (:require
    [speclj.core]
    [c3kit.wire.spec-helper :as wire]
    [tic-tac-toe.main :as sut]))
    
(describe "main"
  (wire/with-root-dom)
  (before
    (wire/render [sut/app]))

  (it "does stuff"
    (should-select "#bob")
    (should= "file:///" (wire/href "#bob a"))))

This is using wire/with-root-dom to render a blank DOM for the test to use and wire/render to render the component to be tested. The test then verifies that it was able to find and select an element with an ID of "bob" as well as selecting the a tag inside "bob" and read its href value.

Run the Test!

At this point, running clj -M:test:cljs compiled the ClojureScript, placed the compiled files into resources/public/cljs, ran the Speclj tests, and watched the cljs directories for changes and automatically re-compiled and re-ran the tests! This defaults to the auto-runner. To run the tests once, I could use the command clj -M:test:cljs once.

What's It Look Like?

Nobody likes writing front-end code and not being able to see it in the browser. Now that I had the main function defined in ClojureScript, it was time to create a barebones index.html file to give that function someplace to inject the code into, as well as this being a file that I could open in my browser and see my code rendering on the page.

Inside of resources/public, I added an index.html with the following contents:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Tic Tac Toe</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/3.13.1/minified.js" type="text/javascript"></script>
  <script src="./cljs/goog/base.js" type="text/javascript"></script>
  <script src="./cljs/tic_tac_toe_dev.js" type="text/javascript"></script>
  <script type="text/javascript">goog.require("tic_tac_toe.main")</script>
  <style>
    * {
      font-family: system-ui;
    }
    button {
      padding: 32px;
    }
    h2 {
      text-align: center;
    }

  </style>
</head>
<body>
<div id="app"></div>
<!--<script src="./cljs/tic_tac_toe/main.js" type="text/javascript"></script>-->
<script type="text/javascript">
  tic_tac_toe.main.main()
</script>
</body>
</html>

Let's talk about a few specific lines:

<script src="./cljs/tic_tac_toe_dev.js" type="text/javascript"></script>

This is the filename that was defined in config/cljs.edn with the compiled JavaScript for running the tests.

<script type="text/javascript">goog.require("tic_tac_toe.main")</script>

This is pulling in the tic_tac_toe.main namespace from ClojureScript where the main function is.

<script type="text/javascript">
  tic_tac_toe.main.main()
</script>

This is calling the main function from the ClojureScript file inside the HTML body after the div with an ID of "app" has been created.

Now after compiling the ClojureScript, I could open this file in my browser and open the inspector and see that the div with an ID of "bob" was being rendered to the page!

What? No Dev Server?

I know, I know. At this point anyone with previous React experience is saying "Why can't you just type npm run dev to start a development server on localhost instead of having to load a filepath into the browser?" In part 2, I'll cover setting up a local development server and adding the ability to write CSS in Clojure!

Explore more articles

Browse All Posts