UP | HOME

Rapid Recipe - Add Clojurescript to your Luminus project

Table of Contents

This article is for when you already have a Luminus web-app using Selmer templating, and you want to add ClojureScript to it.

This doesn't cover front-end frameworks, it just puts you in a position where you can use ClojureScript to manipluate the DOM (as one might do with JQuery).

1. Update project.clj

First, add ClojureScript to your project.clj dependencies. The snippet to do this is documented here: https://github.com/clojure/clojurescript

Now add lein-cljsbuild to the project.clj plugins (not dependencies). Snippet here: https://github.com/emezeske/lein-cljsbuild

Then, from that same page, copy+paste the :cljsbuild {} part (from the "Basic Configuration" section). You will want to change:

  • :source-paths to src/cljs. This is just so it's in src where it belongs.
  • :output-to to target/cljsbuild/public/js/app.js.

Finally:

  • edit :resource-paths, adding "target/cljsbuild"
  • add the cljsbuild build hooks: :hooks [leiningen.cljsbuild]

2. Add your code

Edit one of your HTML templates. Put this snippet where you want the script tag to be templated in:

  {% script "/js/app.js" %}

ⓘ the Selmer script tag is documented here: https://github.com/yogthos/Selmer#script

For your Hello World, add an empty div that you can add content to:

  <div id="dynamiccontent"></div>

Now make the directory for your Clojurescript code (edit "appname"):

mkdir -p src/cljs/appname/

Now create a new file core.cljs and write your Hello World code (edit "appname"):

(ns appname.core)

(let [container (.getElementById js/document "dynamiccontent")
      paragraph (.createElement js/document "p")]

  (set! (.-textContent paragraph) "Ultimate World!")
  (.appendChild container paragraph))

3. Run it

Restart your development server if it's running.

And run in an extra terminal:

lein cljsbuild auto

Now when you load your site in browser you should see your Hello World!

4. How to call out to a function?

In the above example, the javascript just directly runs when the script loads.

However if you want to define functions that are called on certain events you can. There are two ways to do that:

4.1. specify namespace

You can directly call to a function from the HTML if you prefix the function name with it's namespace.

Here is an example using onclick (edit "appname"):

      <div id="functiontest">
        <input id="mynumber" type="number">
        <button onclick="appname.core.timesfive()">times 5!</button>
        <p id="equals"></p>
      </div>  
(defn timesfive
  "multiplies the supplied number by 5 and renders it onto the page"
  []
  (let [number (.-value (.getElementById js/document "mynumber"))
        paragraph (.getElementById js/document "equals")]
    (set! (.-innerHTML paragraph) (* number 5))))

4.2. attach listener

This way we attach a listener to the button from code using .addEventListener:

      <div id="functiontest">
        <input id="mynumber" type="number">
        <button id="mybutton">times 5!</button>
        <p id="equals"></p>
      </div>  
(defn timesfive
  "multiplies the supplied number by 5 and renders it onto the page"
  []
  (let [number (.-value (.getElementById js/document "mynumber"))
        paragraph (.getElementById js/document "equals")]
    (set! (.-innerHTML paragraph) (* number 5))))

(let [button (.getElementById js/document "mybutton")]
  (.addEventListener button "click" timesfive))

5. How to share code between server and client?

So you want to write some code which will be available to both server and client.

This is fairly simple, we just make a new source directory and add it to both the Clojure and ClojureScript source-paths.

The convention for naming is:

extension type
clj Clojure
cljs Clojurescript
cljc Platform-independant code

Generally when writing cljc, you avoid using platform-specific code. But if you have to, you may use Reader Conditionals.

For the example, it's quite a lot so I just show you the git change: https://codeberg.org/xylon/cccl2/commit/a0d533578ff7c5da51331140c3c181c3772c8fc4

6. How to AJAX?

For AJAX you may use cljs-alax. I will let the project documentation speak for itself https://github.com/JulianBirch/cljs-ajax

Note that, if using Luminus, you don't need to manually convert to/from JSON, as the middleware will take care of that automatically.

7. Export more than one script

Look back at the :cljsbuild map you added to project.clj.

:builds is a list, so you may simply add more builds.

You will have to add a :main key to control the entry-point of each script. Docs for compiler options here.

8. General notes on ClojureScript

Miscelaneous notes.

8.1. REPL

If you want a REPL for experimenting with, you may either

8.2. Interop

This example demonstrates calling a function, reading a property, and setting a property:

   (let [myinput (.getElementById js/document "myinput")
         myvalue (.-value myinput)]
     (set! myvalue "foobar"))

n.b. there are multiple ways to do everything. See this article for more examples: ClojureScript <-> JavaScript Interop

8.3. js/ namespace

In the examples we used js/document. But the js/ namespace passes through to JavaScript so you may also access other javascript objects, e.g:

  • js/document
  • js/window
  • js/console
  • js/formdata

And you can also using it to access libraries. For example if using htmx:

  • js/htmx

8.4. this

If you need to access javascript's "this" object, just wrap it with:

   (this-as this
     )

8.5. log to console

(:require [cljs.pprint :refer [pprint]])

8.6. cheat-sheet

8.7. adding reagent

Should you want to add reagent, it builds on-top of what we've already done. Simply:

Add the dependencies:

[reagent "1.2.0"]              
[cljsjs/react "18.2.0-1"]      
[cljsjs/react-dom "18.2.0-1"]] 

Add a div:

<div id="reagentdiv"> 
</div>                

Import reagent to your script :require:

[reagent.core :as r] 
[reagent.dom :as dom]

And render a Hello World:

(dom/render                                                  
 [:div {:id "hello" :class "content"} [:h1 "Hello, Reagent"]]
 (.getElementById js/document "reagentdiv"))                 

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