Navigation and Routing with Om and Secretary

After some quick experiments with Secretary and Enfocus, I decided to dive headfirst to Om.

Since I’m kind of restarting my pet project all the time, the first thing I lay down is routing and navigation. This time I’ll implement it by combining Secretary with Om and a little Bootstrap.

One of the key features of Om is strong separation of state from behavior from rendering. In a nutshell, state is defined in one place in an atom and is just, you know, state. You can manipulate it as you like without worrying about rendering. Finally, you install renders on top of it without worrying about the behavior.

Let’s start with a bunch of imports. We’ll need Secretary and goog.History from Closure as well as some Om for rendering. I’ll also keep a reference to History so I don’t instantiate it over and over.

(ns demo.navigation
  (:require [secretary.core :as secretary :include-macros true :refer [defroute]]
            [goog.events :as events]
            [om.core :as om :include-macros true]
            [om.dom :as dom :include-macros true])
  (:import goog.History
           goog.History.EventType))

(def history (History.))

Now, the state. Each route has a name that will appear on the navigation bar and path for routing.

(def navigation-state 
  (atom [{:name "Add" :path "/add"}
         {:name "Browse" :path "/browse"}]))

Time for some state manipulation. Enter Secretary and Closure history:

(defroute "/add" [] (js/console.log "Adding"))

(defroute "/browse" [] (js/console.log "Browsing"))

(defn refresh-navigation []
  (let [token (.getToken history)
        set-active (fn [nav]
                     (assoc nav :active (= (:path nav) token)))]
    (swap! navigation-state #(map set-active %))))

(defn on-navigate [event]
  (refresh-navigation)
  (secretary/dispatch! (.-token event)))

(doto history
  (goog.events/listen EventType/NAVIGATE on-navigate)
  (.setEnabled true))

It’s very similar to what I did before – two basic routes, gluing Secretary to Closure history with pretty much the same code that is in Secretary docs.

There’s one thing worth noting here. Every time the route changes, refresh-navigation will update the navigation-state atom. For each of the routes it will set the :active flag, making it true for the path we navigated to and false for all others. This will be used to render the right tab as active.

Now, somewhere in my HTML template I’ll put the div to hold my navigation bar:

<div id="navigation"></div>

Finally, let’s do the rendering in Om:

(defn navigation-item-view [{:keys [active path name]} owner]
  (reify
    om/IRender
    (render [this]
            (dom/li #js {:className (if active "active" "")}
                    (dom/a #js {:href (str "#" path)} name)))))

(defn navigation-view [app owner]
  (reify
    om/IRender
    (render [this]
            (apply dom/ul #js {:className "nav nav-tabs"}
                   (om/build-all navigation-item-view app)))))

(om/root navigation-view navigation-state
         {:target (. js/document (getElementById "navigation"))})

Let’s investigate it from the bottom.

om/root binds a component (navigation-view) to state (navigation-state) and installs it on the navigation element in DOM.

navigation-view itself is a composite (container) component. It creates a <ul class="nav nav-tabs"> containing a navigation-item-view for each route.

Finally, navigation-item-view renders <li class="active"><a href="#{path}">{name}</a></li> using the right pieces of information from the map representing a route.

That’s it. Like I said, state is as pure as it can be, routing doesn’t know anything about rendering, and rendering only cares about state. There is no explicit call to rerender anything anywhere. What’s more, reportedly Om is smart enough to figure out exactly what changed and keep the DOM changes to minimum.

Side note – Om looks like a big thing to learn, especially since I don’t know React. But it’s quite approachable thanks to its incredibly good tutorial. It also made me switch from Eclipse with CounterClockWise to LightTable, giving me more productive fun than I can remember.

6 thoughts on “Navigation and Routing with Om and Secretary

  1. Very nice intro! Thank very much for that…

    I also giving a try to Om, a very promising framework. But I noticed you mentioned Reagent in your past post. Could I have your opinion about the differences between these two, what one would you prefer? Or in which situations would you use one over the other? Again, thanks.

  2. Roberto – I have never actually used Reagent. I spent a while googling for the differences and found a few good writeups, including some comments by David Nolen. I decided to go with Om first because even though it’s bigger and seemingly more complex, it seems to have better state management and isolation and in general is more powerful while avoiding some pitfalls. Plus, you know, David is David. :-)

    I know this probably sounds vague and ungrounded. One of the discussions I meant is here: https://groups.google.com/forum/#!topic/clojurescript/NlaYPfQBW2I

  3. Just a question – are you actually using secretary for something here? Without being too familiar with Om it seems secretary is just doing these console.logs. In a real world case, what would go there?

  4. Niklas – normally I would put whatever code is necessary to change the page in there instead of console.log. Hide and show components (if using something like jQuery for scaffolding?). Or change something in Om’s main atom to get Om to rebuild the whole thing for me.

  5. I don’t understand why you need to use `secretary` to do that? These lines seems unnecessary?

    (defroute "/add" [] (js/console.log "Adding"))
    (defroute "/browse" [] (js/console.log "Browsing"))

  6. Raphael – these are just hooks. After all, the purpose of routing is to reshape/change the page. And these two are where the actual change would happen.

Leave a Reply

Your email address will not be published. Required fields are marked *

Spam protection by WP Captcha-Free