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.
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.
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
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?
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.
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"))
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.