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.