Or How I Learned To Stop Worrying And Ditch Custom Directives
Since writing my post Reactive Canvas with TypeScript and Vue I've discovered glue
, a library for defining Vue components in ClojureScript. Ever the hipster, I had to give it a spin. This post details the same functionality as that post but using ClojureScript instead of TypeScript.
Setup
To start, you'll need to have a JDK installed. You'll also need to obtain leiningen
which provides package managment and build tooling for Clojure/ClojureScript.
Once you've installed the above navigate to your project directory and issue: lein new figwheel rxcanvas-cljs
. Navigate to your new folder rxcanvas-cljs
and open up project.clj
. We just need to make one change. Find your :dependencies
key and make it look like this:
:dependencies [[org.clojure/clojure "1.9.0"]
[org.clojure/clojurescript "1.10.238"]
[org.clojure/core.async "0.4.474"]
[glue "0.1.3-SNAPSHOT"]]
We've just added glue
to the list. Don't worry too much if your version numbers don't match exactly - this is just what the template came with on the date of this writing.
Now we execute lein figwheel
. The first run will be the longest as it gathers dependencies. When it loads, open your browser to localhost:3449
. When the page loads you should see the REPL prompt appear in your terminal - try issuing (js/alert "Hello from ClojureScript")
:
// ...
[Rebel readline] Type :repl/help for online help info
ClojureScript 1.10.238
dev:cljs.user=> (js/alert "Hello from ClojureScript")
You should see the requested alert in your browser. Leave this running as you develop and when you're ready to close type :cljs/quit
at the REPL prompt.
If you're new to figwheel
take a moment to familiarize yourself with the blank project layout. There's not too much here. The dev
directory just sets up some convenience functions, and our HTML and CSS will live in resources/public
. It has pre-populated a .gitignore
and a README.md
for you. All of our logic will live in src/rxcanvas_cljs/core.cljs
.
Add a template
We're not using Single-File Components. This would currently involve some non-trivial DIY plumbing. There's no vue-loader
equivalent to do the parsing for us yet - you could write the first! If I'm wrong about this, somebody pipe up below.
We're just going to keep our template separate. Open up resources/public/index.html
. The figwheel template comes with a div
with the id app
. We'll keep the div but replace the contents:
<div id="app">
<rxcanvas></rxcanvas>
</div>
Now we can use the <template>
tag to define our resizable dot component. Place this above the app
div, directly following the opening <body>
tag:
<template id="rxcanvas">
<div>
<span>{{ size }}</span>
<input type="range" min="1" max="100" step="5" id="size" @change="drawDot">
<label for="size">- Size</label>
<p><canvas id="rx"></canvas></p>
</div>
</template>
There are two changes from the TypeScript. For one, I've replaced v-model="size"
in the range
tag with @change="drawDot"
. This method will handle updating our state. I've also ditched the custom directive in the <canvas>
tag, instead just assigning an id.
Add some Lisp
Now we get to the good stuff. Open up src/rxcanvas_cljs/core.cljs
. First, we need to override the built-in atom
with the one glue
provides and bring the rest of the library into scope. Add the following to your ns
form at the top of the file:
(ns rxcanvas-cljs.core
(:refer-clojure :exclude [atom])
(:require [glue.core :as g :refer [atom]]))
Leave in the (enable-console-print!)
line at the top of the file - this allows us to use the browser console for output with println
should we so choose - but delete everything else.
We'll start with the mount point:
(defonce app (g/vue {:el "#app"})
This locates the <div id="app">
from index.html
and mounts our Vue stuff to it. We also need to make sure it keeps itself refreshed - add the following below:
(defn on-js-reload []
(g/reset-state!))
ClojureScript is not object-oriented like TypeScript, so we'll just define a plain old function to handle the canvas drawing logic instead of a Dot
class. Put this above your app
definition:
(defn draw
[radius canvas]
(let [canvas-dim (* 2 radius)]
;; resize canvas
(set! (.-width canvas) canvas-dim)
(set! (.-height canvas) canvas-dim)
;; draw the shape
(let [ctx (.getContext canvas "2d")
center-x (/ (.-width canvas) 2)
center-y (/ (.-height canvas) 2)]
(set! (.-fillStyle ctx) "rgb(0,0,0)")
(.clearRect ctx 0 0 (.-width canvas) (.-height canvas))
(.beginPath ctx)
(.arc ctx center-x center-y radius 0 (* 2 (.-PI js/Math)) false)
(.fill ctx)
(.stroke ctx))))
Interop is dirt simple - you just put the method in the first position of the s-expression. You can get and set properties via syntax like (.-PI js/Math)
. It's rather easy to get addicted to the hyper-regular syntax.
Now we're ready to define the component itself. With glue
we use defcomponent
, right below draw
:
(g/defcomponent
:rxcanvas
{:template "#rxcanvas"
:state (fn [] {:size (atom 10)})
:methods {:draw-dot (fn [this state _]
;; update the state
(reset! (:size state) (.-value (.querySelector js/document "#size")))
;; grab the new value and the canvas for drawing
(draw @(:size state) (.querySelector js/document "#rx"))
)}})
Instead of data()
we're using the key :state
but it still returns a function. We've explicitly stored the size
in an atom
, ClojureScript's mechanism for allowing mutability in an otherwise immutable language. This particular atom
, as discussed, is from glue
and has some extra goodness built in to ease use in Vue components. Using it we can access size
using simple forms like (:size state)
.
Also note - in our template we style the method name drawDot
, and in our ClojureScript it's called draw-dot
. This is another part of what glue
is handling!
We need the @
operator as in @(:size state)
to get at the current value of the atom
in our call to draw
.
That's it! Now our canvas will resize and redraw on each change to our slider.
The completed code can be found here.