ClojureScript

Contents

  1. Quick Start
  2. Adding ClojureScript Support
  3. Managing JavaScript and ClojureScript dependencies
  4. Running the Compiler
  5. shadow-cljs with nREPL
  6. Interacting with JavaScript
  7. Reagent
  8. Client Side Routing
  9. Ajax

ClojureScript is an excellent alternative to JavaScript for client-side application logic. Some of the advantages of using ClojureScript include:

  • use the same language on both the client and the server
  • share common code between the front-end and back-end
  • cleaner and more consistent language
  • immutable data structures
  • powerful standard library

Quick Start

This section provides a short list of steps that you need to complete to start ClojureScript development with Kit. See the sections below for details.

  1. Add the :kit/cljs module by running (kit/install-module :kit/cljs).
  2. Restart your application.
  3. Install JavaScript dependencies by running npm install in your project's root directory.
  4. Run the shadow-cljs compiler in watch mode by executing npx shadow-cljs watch app.
  5. Connect to you shadow-cjls nREPL on port 7002 using your preferred editor.
  6. Open your project's root page (http://localhost:3000 by default) in your preferred browser.
  7. In the shadow-cjls REPL, run (shadow.cljs.devtools.api/repl :app).
  8. Verify that everything is wired correctly by running (js/alert "Hi") in your shadow-cljs REPL. This should display an alert in your browser window.
  9. You can now write your ClojureScript code by editing the core.cljs file inside the src/cljs directory.

Adding ClojureScript Support

ClojureScript support can be added via the official :kit/cljs module. Run (kit/install-module :kit/cljs) to add the assets. This will add support for compiling ClojureScript using shadow-cljs. Be sure to restart the application afterwards.

Managing JavaScript and ClojureScript dependencies

NPM modules

NPM is used to manage JavaScript modules in the project. Make sure that you have NPM installed. Adding the :kit/cljs module will create a package.json file with the following content:

{
  "devDependencies": {
    "shadow-cljs": "^2.14.3"
  },
  "dependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }
}

Make sure to run npm install to install the modules above before starting the shadow-cljs compiler.

ClojureScript libraries

ClojureScript libraries are managed using the :dependencies key in the shadow-cljs.edn. The module will have generated the following content for this file:

{:nrepl {:port 7002}
 :source-paths ["src/cljs"]
 :dependencies [[binaryage/devtools "1.0.3"]
                [nrepl "0.8.3"]
                [reagent "1.1.0"]
                [cljs-ajax "0.8.4"]]
 :builds       {:app {:target     :browser
                      :output-dir "target/classes/cljsbuild/public/js"
                      :asset-path "/js"
                      :modules    {:app {:entries [kit.guestbook.app]}}
                      :devtools   {:after-load kit.guestbook.core/mount-root}}}}

Running the Compiler

The easiest way to develop ClojureScript applications is to run the compiler in watch mode. This way any changes you make in your namespaces will be recompiled automatically and become immediately available on the page.

To start the compiler in this mode, run:

npx shadow-cljs watch app

This will start shadow-cljs and connect a browser REPL. Any changes you make in ClojureScript source will now be automatically recompiled.

When you run the uberjar task, ClojureScript will be compiled with production settings according to the following function from your build.clj:

 (defn build-cljs []
   (println "npx shadow-cljs release app...")
   (let [{:keys [exit], :as s} (sh "npx" "shadow-cljs" "release" "app")]
     (when-not (zero? exit) (throw (ex-info "could not compile cljs" s)))))
 

shadow-cljs with nREPL

By default, running the npx shadow-cljs watch app command will also enable nREPL on port 7002. This is governed by the :nrepl {:port 7002} key present in your shadow-cljs.edn.

After running shadow-cljs, connect to nREPL using your preferred editor and run (shadow.cljs.devtools.api/repl :app). You can now test that everything is running correctly by executing (js/alert "Hi") in the REPL. This should display an alert in the browser. To exit the ClojureScript nREPL run :cljs/quit.

Note that for the JavaScript alert to work, you must have the homepage of your project open in a browser window. Otherwise your REPL will display the following error: No available JS runtime. This is because of the code needed in the browser to wire JavaScript runtime with shadow-cljs as explained below.

Installing the :kit/cljs module adds that code to your home.html:

  <div id="app"></div>
  <script src="/js/app.js"></script> 

The first line indicates the mount point of your Reagent application, defined in core.cljs by default.

The second line ensures the app.js file, which contains your ClojureScript code compiled to JavaScript, is loaded when you open this page in your browser. This code is required for your REPL to have the direct connection to your browser window, allowing for interactive coding like with regular Clojure code.

Interacting with JavaScript

All the global JavaScript functions and variables are available via the js namespace.

Method Calls

(.method object params)

(.log js/console "hello world!")

Accessing Properties

(.-property object)

(.-style div)

Setting Properties

(set! (.-property object))

(set! (.-color (.-style div) "#234567"))

For more examples of ClojureScript synonyms of common JavaScript operations see the ClojureScript Synonyms.

Reagent

Reagent is the recommended approach for building ClojureScript applications with Kit.

Reagent is backed by React and provides an extremely efficient way to manipulate the DOM using Hiccup style syntax. In Reagent, each UI component is a data structure that represents a particular DOM element. By taking a DOM-centric view of the UI, Reagent makes writing composable components simple and intuitive.

A simple Reagent component looks as follows:

[:label "Hello World"]

Components can also be functions:

(defn label [text]
  [:label text])

The values of the components are stored in Reagent atoms. These atoms behave just like regular Clojure atoms, except for one important property. When an atom is updated, it causes any components that dereference it to be rerendered. Let's take a look at an example.

Important: Make sure that you require Reagent atom in the namespace, otherwise regular Clojure atoms will be used and components will not be rerendered on change.

(ns myapp
  (:require [reagent.core :as reagent]))

(def state (reagent/atom nil))

(defn input-field [label-text]
  [:div
    [label label-text]
    [:input {:type "text"
             :value @state
             :on-change #(reset! state (-> % .-target .-value))}]])

Above, the input-field component consists of a label component we defined earlier and an :input component. The input will update the state atom and render it as its value.

Notice that even though label is a function we're not calling it, but instead we're putting it in a vector. The reason for this is that we're specifying the component hierarchy. The components will be run by Reagent when they need to be rendered.

This is behavior makes it trivial to implement the React Flux pattern.

Views--->(actions) --> Dispatcher-->(callback)--> Stores---+
Ʌ                                                          |
|                                                          V
+--(event handlers update)--(Stores emit "change" events)--+

Our view components dispatch updates to the atoms, which represent the stores. The atoms in turn notify any components that dereference them when their state changes.

In the previous example, we used a global atom to hold the state. While it's convenient for small applications this approach doesn't scale well. Fortunately, Reagent allows us to have localized states in our components. Let's take a look at how this works.

(defn input-field [label-text id]
  (reagent/with-let [value (reagent/atom nil)]
    [:div
     [label "The value is: " @value]
      [:input {:type "text"
               :value @value
               :on-change #(reset! value (-> % .-target .-value))}]]))

All we have to do is create a local binding for the atom inside a closure. The returned function is what's going to be called by Reagent when the value of the atom changes.

Finally, rendering components is accomplished by calling the render-component function:

(defn render-simple []
  (reagent/render-component [input-field] (.-body js/document))

Client Side Routing

Reitit is used to handle both client and server routes. We'd need to require Reitit in the routing namespace along with Google Closure history and events helpers.

(ns <app>.core
 (:require
  [reagent.core :as r]
  [reitit.core :as reitit]
  [goog.events :as events]
  [goog.history.EventType :as HistoryEventType])
 (:import goog.History))

We'll now add a session atom to hold the selected page along with a couple of pages:

(def session (r/atom {:page :home}))

(defn home-page []
  [:div "Home"])
  
(defn about-page []
  [:div "About"])

(def pages
  {:home #'home-page
   :about #'about-page})

We can now create a page function that will check the state of the session and render the appropriate page:

(defn page []
  [(pages (:page @session))])

We can now add a route that will dispatch the key associated with each page when the route is selected:

(def router
  (reitit/router
    [["/" :home]
     ["/about" :about]]))     

Finally, we'll add functions to match routes and hook into browser navigation:

(defn match-route [uri]
  (->> (or (not-empty (string/replace uri #"^.*#" "")) "/")
       (reitit/match-by-path router)
       :data
       :name))

(defn hook-browser-navigation! []
  (doto (History.)
    (events/listen
      HistoryEventType/NAVIGATE
      (fn [event]
        (swap! session assoc :page (match-route (.-token event)))))
    (.setEnabled true)))

When the hook-browser-navigation! is called, it will hook into page events and call the match-route function when the page navigation event is dispatched.

See Reitit documentation for further details.

Ajax

ClojureScript module uses cljs-ajax to handle Ajax operations.

ajax-request

The ajax-request is the base request function that accepts the following parameters:

  • uri - the URI for the request
  • method - a string representing the HTTP request type, eg: "PUT", "DELETE", etc.
  • format - a keyword indicating the response format. Can be either :raw, :json, :edn, or :transit and defaults to :transit
  • handler - success handler, a function that accepts the response as a single argument
  • error-handler - error handler, a function that accepts a map representing the error with keys :status and :status-text
  • params - a map of params to be sent to the server

GET/POST helpers

The GET and POST helpers accept a URI followed by a map of options:

  • :handler - the handler function for successful operation should accept a single parameter which is the deserialized response
  • :error-handler - the handler function for errors, should accept a map with keys :status and :status-text
  • :format - the format for the request can be either :raw, :json, :edn, or :transit, and defaults to :transit
  • :response-format - the response format. If you leave this blank, it will detect the format from the Content-Type header
  • :params - a map of parameters that will be sent with the request
  • :timeout - the ajax call's timeout. 30 seconds if left blank
  • :headers - a map of the HTTP headers to set with the request
  • :finally - a function that takes no parameters and will be triggered during the callback in addition to any other handlers
(ns foo
  (:require [ajax.core :refer [GET POST]]))

(defn handler [response]
  (.log js/console (str response)))

(defn error-handler [{:keys [status status-text]}]
  (.log js/console
    (str "something bad happened: " status " " status-text)))

(GET "/hello")

(GET "/hello" {:handler handler})

(POST "/hello")

(POST "/send-message"
        {:headers {"Accept" "application/transit+json"}
         :params {:message "Hello World"
                  :user    "Bob"}
         :handler handler
         :error-handler error-handler})

In the example above, the handler will be invoked when the server responds with a success status. The response handler function should accept a single parameter. The parameter will contain the deserialized response from the server.

The library attempts to automatically discover the encoding based on the response headers, however the response format can be specified explicitly using the :response-format key.

The error-handler function is expected to accept a single parameter that contains the error response. The function will receive the entire response map that contains the status and the description of the error along with any data returned by the server.

  • :status - contains the HTTP status code
  • :status-text - contains the textual description of the status
  • :original-text - contains the server response text
  • :response - contains the deserialized response when if deserialization was successful

When no handler function is supplied then no further action is taken after the request is sent to the server.

The request body will be interpreted using the ring-middleware-format library. The library will deserialize the request based on the Content-Type header and serialize the response using the Accept header that we set above.

The route should simply return a response map with the body set to the content of the response:

(ns <app>.routes.services
  (:require
   [ring.util.response :refer [response status]]))

(defn save-message! [{:keys [params]}]
  (println params)
  (response {:status :success}))

(defn service-routes []
  [""
   ["/send-message" {:post save-message!}]])

Note that CSRF middleware is enabled by default. The middleware wraps the home-routes of your application. It will intercept any request to the server that isn't a HEAD or GET.

(defn home-routes [base-path]
  [base-path
   {:middleware [middleware/wrap-csrf
                 middleware/wrap-formats]}
   ["/" {:get home-page}]])

We would now need to pass the CSRF token along with the request. One way to do this is to pass the token in the x-csrf-token header in the request with the value of the token.

To do that we'll first need to set the token as a hidden field on the page:

<input id="csrf-token" type="hidden" value="{{csrf-token}}"></input>

Then we'll have to set the header in the request:

(POST "/send-message"
        {:headers {"Accept" "application/transit+json"
                   "x-csrf-token" (.-value (.getElementById js/document "csrf-token"))}
         :params {:message "Hello World"
                  :user    "Bob"}
         :handler handler
         :error-handler error-handler})