A good while ago I was looking for good ways to do client-side routing and templating in ClojureScript. I investigated using a bunch of JavaScript frameworks from ClojureScript, of which Angular probably gave the most promising results but still felt a bit dirty and heavy. I even implemented my own routing/templating mechanism based on Pedestal and goog.History
, but something felt wrong still.
Things have changed and today there’s a lot buzz about React-based libraries like Reagent and Om. I suspect that React on the front with a bunch of “native” ClojureScript libraries may be a better way to go.
Before I get there though, I want to revisit routing and templating. Let’s see how we can marry together two nice libraries: Secretary for routing and Enfocus for templating.
Let’s say our app has two screens which fill the entire page. There are no various “fragments” to compose the page from yet. We want to see one page when we navigate to /#/add
and another at /#/browse
. The “browse” page will be a little bit more advanced and support path parameters. For example, for /#/browse/Stuff
we want to parse the “Stuff” and display a header with this word.
The main HTML could look like:
<!DOCTYPE html> <html> <body> <div class="container-fluid"> <div id="view">Loading...</div> </div> <script src="js/main.js"></script> </body> </html>
Then we have two templates.
add.html:
<h1>Add things</h1> <form> <!-- boring, omitted --> </form>
browse.html:
<h1></h1> <div> <!-- boring, omitted --> </div>
Now, all we want to do is to fill the #view
element on the main page with one of the templates when location changes. The complete code for this is below.
(ns my.main (:require [secretary.core :as secretary :include-macros true :refer [defroute]] [goog.events :as events] [enfocus.core :as ef]) (:require-macros [enfocus.macros :as em]) (:import goog.History goog.History.EventType)) (em/deftemplate view-add "templates/add.html" []) (em/deftemplate view-browse "templates/browse.html" [category] ["h1"] (ef/content category)) (defroute "/" [] (.setToken (History.) "/add")) (defroute "/add" [] (ef/at ["#view"] (ef/content (view-add)))) (defroute "/browse/:category" [category] (ef/at ["#view"] (ef/content (view-browse category)))) (doto (History.) (goog.events/listen EventType/NAVIGATE #(em/wait-for-load (secretary/dispatch! (.-token %)))) (.setEnabled true))
What’s going on?
- We define two Enfocus templates.
view-add
is trivial and simply returns the entire template.view-browse
is a bit more interesting: Given category name, alter the template by replacing content ofh1
tag with the category name. - Then we define Secretary routes to actually use those templates. All they do now is replace content of the
#view
element with the template. In case of the “browse” route, it passes the category name parsed from path to the template. - There is a default route that redirects from
/
to/add
. It doesn’t lead toexample.com/add
, but only sets the fragment:example.com/#/add
. - Finally, we plug in Secretary to
goog.History
. I’m not sure why it’s not in the box, but it’s straightforward enough. - Note that in the history handler there is the
em/wait-for-load
call. It’s necessary for Enfocus if you load templates with AJAX calls.
That’s it, very simple and straightforward.
Update: Fixed placement of em/wait-for-load
, many thanks to Adrian!
Thanks for writing this article, Konrad! It’s interesting to see how people are using Secretary in their projects.
It probably could be and we have added some code to the README to help folks get up and running faster. I think one reason we haven’t added a default listener to the project is to avoid making choices upfront about how to use the project.
Joel – you could go one step further and provide this wrapper around goog.History.
Note the necessary improvement here though: https://www.refheap.com/59205 – I’ll test it later and update the post.
Konrad. Sure! I think what might be best is to open up the wiki so people can share these examples.