Rendering groov with ClojureScript

28 Jul 2015

This is mostly meant as a demo/tutorial for my coworkers, but I thought it’d be fun to share in general too.

groov logo

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:

Hello world from Om

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:

{
  "desktopLayout": {
    "x": 2,
    "y": 6,
    "z": 0,
    "width": 21,
    "height": 21,
    "locked": false
  },
  "handheldLayout": {
    "x": 0,
    "y": 5,
    "z": 0,
    "width": 15,
    "height": 15,
    "locked": false
  },
  "type": "Round Gauge"
}

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:

(def grid-unit-size
  {:desktop 96
   :mobile 32})

(def mobile-cutoff 480.0)

(defn make-layout [viewport-width]
  (let [mode (if (> viewport-width mobile-cutoff) :desktop :mobile)
        grid-size (get grid-unit-size mode)
        points-per-grid (.floor js/Math (/ viewport-width grid-size))
        container-width (* grid-size points-per-grid)]
    {:mode mode
     :container-width container-width
     :points-per-grid points-per-grid
     :viewport-width viewport-width}))

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 reload icon

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:

Popping up an alert in the browser

Which is silly, but fun. But you can also do this:

Testing make-layout

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:

(defonce *app-state* (atom {}))

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:

(ns groov.core
  (:require [om.core :as om]
            [om.dom :as dom])
  (:import [goog.dom ViewportSizeMonitor]))

Make a global instance of it:

(defonce viewport-monitor (ViewportSizeMonitor.))

And a function to update *app-state* for the current viewport size:

(defn update-layout! []
  (let [layout (make-layout (.-width (.getSize viewport-monitor)))]
    (swap! *app-state* assoc :layout layout)))

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:

(reset! *app-state* (assoc *app-state* :layout layout))

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:

(defn ^:export main
  []
  (update-layout!)
  (render-root))

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:

<div class="container">
  <p><strong>Viewport width:</strong> 757</p>
  <p><strong>Layout mode:</strong> :desktop</p>
  <p><strong>Points per grid unit:</strong> 7</p>
  <p><strong>Container width:</strong> 672</p>
</div>

We can translate that to what Om expects pretty easily:

(defn ViewportComponent
  [data owner]
  (reify
    om/IRender
    (render [this]
      (let [layout (:layout data)]
        (dom/div #js {:className "container"}
          (dom/p nil (dom/strong nil "Viewport width: " ) (:viewport-width layout))
          (dom/p nil (dom/strong nil "Layout mode: ") (str (:mode layout)))
          (dom/p nil (dom/strong nil "Points per grid unit: ") (:points-per-grid layout))
          (dom/p nil (dom/strong nil "Container width: ") (:container-width layout)))))))

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:

(defn render-root []
  (om/root ViewportComponent *app-state*
           {:target (.getElementById js/document "app")}))

And voila, we’re rendering something:

Rendering the current layout

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:

(ns groov.core
  (:require [om.core :as om]
            [om.dom :as dom]
            [goog.events :as events])
  (:import [goog.dom ViewportSizeMonitor]))

A function to start listening somewhere between main and update-layout!:

(defn listen-for-viewport-events!
  "Start listening for viewport update events, and update *app-state* when they happen."
  []
  (events/listen viewport-monitor
                 goog.events.EventType.RESIZE
                 #(update-layout!)))

And we’ll update main one more time to call that when the application starts:

(defn ^:export main
  []
  (update-layout!)
  (listen-for-viewport-events!)
  (render-root))

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:

<div class="container-fluid">
  <div class="page-wrapper" style="width: 1000px; height: 1000px">
    <!-- A whole bunch of gadgets -->
  </div>
</div>

That page-wrapper class uses position: relative so that we can lay the gadgets out using absolute positioning.

A gadget will look like this:

<div class="gadget-container" style="left: 10px; top: 30px; width: 100px; height: 100px">
  <p>I am a gadget.</p>
</div>

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:

(defn gadget-bounds
  "Given a gadget description and a layout map, figure out what the gadget's bounds are."
  [gadget layout]
  (let [gadget-layout (get gadget (if (= (:mode layout) :desktop) "desktopLayout" "handheldLayout"))
        points-per-grid (:points-per-grid layout)]
    {:left (* (get gadget-layout "x") points-per-grid)
     :top (* (get gadget-layout "y") points-per-grid)
     :width (* (get gadget-layout "width") points-per-grid)
     :height (* (get gadget-layout "height") points-per-grid)
     :z-index (get gadget-layout "z")}))

(defn gadget-style
  "Takes a gadget's bounds (from gadget-bounds) and returns an appropriately formatted style map"
  [bounds]
  #js {:left (str (:left bounds) "px")
       :top  (str (:top bounds) "px")
       :width  (str (:width bounds) "px")
       :height (str (:height bounds) "px")
       :z-index (:z-index bounds)})

(defn GadgetContainer
  [{:keys [gadget layout]} owner]
  (reify
    om/IRender
    (render [this]
      (let [bounds (gadget-bounds gadget layout)]
        (dom/div #js {:className "gadget-container"
                      :style (gadget-style bounds)}
                 (dom/p nil (str "I am a " (get gadget "type") " gadget.")))))))

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:

{
  "desktopLayout": {
    "x": 2,
    "y": 6,
    "z": 0,
    "width": 21,
    "height": 21,
    "locked": false
  },
  "handheldLayout": {
    "x": 0,
    "y": 5,
    "z": 0,
    "width": 15,
    "height": 15,
    "locked": false
  },
  "type": "Round Gauge"
}

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.

(defn PagesLoadingComponent
  [data owner]
  (reify
    om/IRender
    (render [this]
      (dom/div #js {:className "container"}
        (dom/p nil "The pages haven't loaded yet.")))))

(defn PageComponent
  [{:keys [page layout]} owner]
  (reify
    om/IRender
    (render [this]
      (if (nil? page)
        ;; If the pages haven't loaded yet, just use an empty thing that tells the user that.
        (om/build PagesLoadingComponent nil)

        ;; Otherwise, build up the wrapper DOM and render all the gadgets.
        (dom/div #js {:className "container-fluid"}
          (dom/div #js {:className "page-wrapper"}
            (for [gadget (get page "gadgets")]
              (om/build GadgetContainer {:gadget gadget :layout layout}))))))))

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:

(defn RootComponent
  [data owner]
  (reify
    om/IRender
    (render [this]
      (dom/div nil
        (om/build ViewportComponent data)
        (om/build PageComponent {:page (first (:pages data)) :layout (:layout data)})))))

(defn render-root []
  (om/root RootComponent *app-state*
           {:target (.getElementById js/document "app")}))

We don’t have any pages in our app state yet, so the rendered page should look something like this now:

Pages not loaded yet

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:

(ns groov.core
  (:require [om.core :as om]
            [om.dom :as dom]
            [goog.events :as events]
            [ajax.core :refer [ajax-request transit-response-format]]
            [cognitect.transit :refer [reader]])
  (:import [goog.dom ViewportSizeMonitor]))

And add a defonce for a JSON reader:

(defonce viewport-monitor (ViewportSizeMonitor.))
(defonce json-reader (reader :json))
(defonce *app-state* (atom {}))

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:

(defn load-pages!
  []
  (ajax-request {:uri "/pages.json"
                 :method :get
                 :response-format (transit-response-format {:reader json-reader})
                 :handler (fn [[ok pages]]
                            (if ok
                              (swap! *app-state* assoc :pages pages)
                              (.warn js/console "Couldn't load pages. :(")))
                 :error-handler (fn [{:keys [status-text]}]
                                  (.warn js/console (str "Load pages request returned an error: " status-text)))}))

(defn LoadPagesButton
  [data owner]
  (reify
    om/IRender
    (render [this]
      (dom/div #js {:className "container"}
        (dom/button #js {:className "btn btn-primary"
                         :onClick (fn [e]
                                    (load-pages!))} "Load Pages")))))

And add that button to RootComponent so it shows up:

(defn RootComponent
  [data owner]
  (reify
    om/IRender
    (render [this]
      (dom/div nil
        (om/build ViewportComponent data)
        (om/build LoadPagesButton nil)
        (om/build PageComponent {:page (first (:pages data)) :layout (:layout data)})))))

You should now have a Load Pages button on your page. Click it, and voila:

Final product

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.