Rendering groov with ClojureScript
This is mostly meant as a demo/tutorial for my coworkers, but I thought it’d be fun to share in general too.
For any who haven’t heard of it, groov is a tool to make it easy to build an interface to industrial automation systems and make it available on your phone. It lets you set up an interface with both desktop and mobile specific layouts, and will automatically scale those layouts as best it can to fit whatever browser or device you’re using. If you want to take a look there’s a demo available; you can log in using username trial, password opto22.
I’ve been toying around with a re-implementation of groov’s operator interface using ClojureScript and a couple of the React wrappers out there, and this is one way I’d approach rendering a page out of groov.
Note: the real groov works nothing like this. It’s not written in ClojureScript, doesn’t use React, and the pages aren’t made available as JSON. :)
In real groov, the page we’re going to be rendering looks like this:
For now, all I’m aiming for is positioning those gadgets correctly.
Before we get into the details, here’s a quick overview of what we’ll be putting together. This is going to be all ClojureScript, using Om as our rendering interface. Om’s big thing is that it treats the application as a function of an input state, and that’s it. You create your state, give Om your render function and a target (a DOM element), and it renders your UI. If your state changes, it re-renders.
To follow along, you’ll want to install Leiningen. Then clone the
repo for this tutorial: https://github.com/mohiji/groov-cljs/ and check out the tutorial-begin
tag. Open a terminal, switch to the directory your repository is in, and run lein figwheel
.
Give it a minute (Clojure and Leiningen are pretty slow to boot) and you should get a
prompt that looks like this:
Figwheel: Starting server at http://localhost:3449
Focusing on build ids: dev
Compiling "resources/public/js/app-dev.js" from ["src/cljs" "dev"]...
Successfully compiled "resources/public/js/app-dev.js" in 13.661 seconds.
Started Figwheel autobuilder
Launching ClojureScript REPL for build: dev
Figwheel Controls:
(stop-autobuild) ;; stops Figwheel autobuilder
(start-autobuild [id ...]) ;; starts autobuilder focused on optional ids
(switch-to-build id ...) ;; switches autobuilder to different build
(reset-autobuild) ;; stops, cleans, and starts autobuilder
(reload-config) ;; reloads build config and resets autobuild
(build-once [id ...]) ;; builds source one time
(clean-builds [id ..]) ;; deletes compiled cljs target files
(fig-status) ;; displays current state of system
(add-dep [org.om/om "0.8.1"]) ;; add a dependency. very experimental
Switch REPL build focus:
:cljs/quit ;; allows you to switch REPL to another build
Docs: (doc function-name-here)
Exit: Control+C or :cljs/quit
Results: Stored in vars *1, *2, *3, *e holds last exception object
Prompt will show when figwheel connects to your application
Open a web browser (Chrome has the best support for working w/ ClojureScript at the moment) and visit http://localhost:3449/dev.html: you should get a page that looks like this:
Now we’re ready to go!
The goal here is to just render placeholders for the gadgets in a page in the proper positions. Before we can do that, we have to determine what those positions will be.
Gadgets in groov are positioned on a grid: for desktop views that grid is 96 units wide, and on handheld it’s 32 units wide. A gadget’s description includes that positioning info; it looks something like this:
The x
, y
, width
, and height
values are in grid units, z
is the z-position we assign
to the element, and locked isn’t important right now: it’s used by the editor.
To figure out the proper position and size to draw a gadget at, we need to know which layout we’re using and how many points a grid unit takes up for the given page size.
For now we’ll use 480 points as the cutoff for switching between mobile and desktop views (groov does it based on browser sniffing), so given a page width we can generate our layout info like this:
If the page width is > 480 points, we use the desktop layout, otherwise the mobile one. We then divide the page width by the number of grid units, rounding down, then figure out what the container width for the page will be based on that unit size.
Go ahead and add that to src/cljs/groov/core.cljs
after the (ns...)
expression. Save the
file, and you should see this little icon appear at the bottom left of that browser window
you opened earlier:
Figwheel noticed that you saved the file, so it
recompiled it and pushed the updated version into the web browser automatically. If there were
any errors or warnings when recompiling the file, it will instead give you a warning instead
and not send any new code to the browser. Also, whenever Figwheel pushes new code to the
browser it’ll call the render-root
function in our core.cljs
; that’s set up in
dev/cljs/groov/dev.cljs
.
Now one more quick aside before we get into playing with Om: in the terminal window where you launched Figwheel you should now have a functioning REPL that’s connected live to the browser. That means you can do this:
Which is silly, but fun. But you can also do this:
You can directly test your code in the REPL. You could (and probably still should) write a unit test for this, but once you get used to having your code immediately available like this, you’ll never want to go back.
Now let’s start rendering using that. Right now our application state (which is what we’ll be handing to Om) looks like this:
It’s a global atom
: an reference that holds something (an empty map at the moment) that
can replace its reference atomically (hence atom) and allows things to listen to when that
reference is changed (which is how Om knows when to re-render). Let’s add a function to grab
the current size of the viewport and replace that atom’s map with one that contains one of
those layout maps.
ClojureScript is built on top of Google’s Closure Library, which means there’s a bunch of super handy stuff ready to go. Here, we’ll use a ViewportSizeMonitor to both query the current viewport size and let us subscribe to updates.
Update the (ns ...)
expression at the top of src/cljs/groov/core.cljs
to let it know we’ll
be using ViewportSizeMonitor
:
Make a global instance of it:
And a function to update *app-state*
for the current viewport size:
The swap!
function in there will atomically update *app-state*
with the results of
applying the 2nd argument (assoc
) with the remaining arguments. It could also be written
more explicitly like this:
In ClojureScript it doesn’t really matter which you use since we’re always going to be
running in a single-threaded context, but in general the swap!
way of doing things is
preferred in Clojure. swap!
will handle retries if another thread updates the atom behind
your back, etc.
Generally speaking, a function that ends with an exclamation point is one that changes
state somewhere, which is why we add that to update-layout!
as well.
Finally, add a call to update-layout!
in main
:
Refresh the page, and it won’t look like anything has changed, but you can inspect the current
value of *app-state*
in the REPL to confirm that it does now hold the right value:
cljs-user=> (in-ns 'groov.core)
nil
groov.core=> *app-state*
#<Atom: {:layout {:mode :desktop, :container-width 672, :points-per-grid 7, :viewport-width 757}}>
It’s not updating when we resize the page yet, but we’ll get to that. First, let’s start rendering something using it. We’ll just render out what’s in that layout map so that we can confirm that things are what we expect them to be. We’re aiming for this HTML:
We can translate that to what Om expects pretty easily:
This defines a function that returns a thing that implements the om/IRender
protocol,
which is all Om needs to render something.
The render
method needs to return a data structure that describes what the rendered
component’s DOM will look like. Om’s default DOM description stuff is a little ugly: it’s
really verbose and having to prefix things with #js
gets annoying. Thankfully, Om allows
for other syntaxes (I usually use Sablono), but this
article is way too long already, so we’ll stick with Om’s syntax here.
Let’s update our render-root
function to use that instead of the existing hello-world
component:
And voila, we’re rendering something:
Now let’s make that update as we resize the browser. The Closure Library has an event system
for publishing things like that, and there’s an easy wrapper for us to use. Add an extra line
to the (ns...)
expression at the top of the file:
A function to start listening somewhere between main
and update-layout!
:
And we’ll update main
one more time to call that when the application starts:
We’ll need to refresh the page again, and after that you should see the rendered viewport information change while you resize the browser.
As an aside: why do we need to refresh the page? Isn’t the live code reloading supposed to
deal with that? We’ve defined a couple of things with defonce
: that means that those things
won’t change when Figwheel pushes new code to the browser. It lets us code freely without
screwing up the current state of the application. If we didn’t do that, than any changes
to core.cljs
would reset *app-state*
back to the default, plus it’d recreate the
ViewportSizeMonitor object, etc. We don’t want that. So we ensure those things are
created once, and we set up their initial values and hook up event listeners in main
,
which is only called once when the page is loaded. main
is only called once because
there’s a call to it in dev.html
to run in window.onload
.
Figwheel’s author goes into a lot more detail in reloadable code.
Aside done, let’s render the page! We’re aiming for something like this for the page as a whole:
That page-wrapper
class uses position: relative
so that we can lay the gadgets out using absolute positioning.
A gadget will look like this:
To do that, we need both a gadget description and the current layout data. We’ll be passing both of those to the gadget component. Add this below the make-layout
function:
That {:keys ...}
bit is a
destructuring binding form:
it’s a shorthand for pulling keys out of a map.
Calling back to the beginning of this overly long tutorial, a gadget description looks like this:
The gadget-bounds
function takes one of those and a layout map (from make-layout
) and
returns a map with the gadget’s proper position and size. gadget-style
takes the output
of gadget-bounds
and formats it in the way that Om expects for a style property, then
GadgetContainer
renders it appropriately.
Now for the page itself. Add this somewhere between the gadget code you just added and render-root
.
Then let’s replace the viewport rendering component in render-root
with one that’ll show
us both the viewport information and the rendered page. Replace the current render-root
with this:
We don’t have any pages in our app state yet, so the rendered page should look something like this now:
I’ve included a JSON file with the pages in it in the repository as resources/public/pages.json
.
We’ll load that up using cljs-ajax and use
transit to parse it out into a Clojure map.
There are a bunch of ways to do this, this just happens to be what I use.
Update the (ns...)
expression at the top of the file again:
And add a defonce
for a JSON reader:
Now we just need a way to kick off that request. I promised to avoid making you reload the
page, so let’s add a button to do it. Add this above RootComponent
:
And add that button to RootComponent
so it shows up:
You should now have a Load Pages button on your page. Click it, and voila:
This article got quite a bit longer than I intended it to; sorry about that. All told though, the code for this ended up at 149 lines with comments. Not bad.