Skip to content
Marcelo Nomoto edited this page May 31, 2023 · 1 revision

Hoplon provides two means of exposing navigation in your application:

  • Route cells
  • Any routing library

Routes

hoplon/brew provides hoplon.history, and the history-cell function that returns a Javelin cell that responds to the URL hash, using the HTML5 History API.

You may be used to frameworks where a "route" always defines everything output to the browser window. As Hoplon has the concept of both "pages" and "routes", it is possible to combine both to achieve navigation and dynamic page structures.

Routes can simulate navigation through the use of template macros.

The simplest possible example is a "two page" application with routes "#home" and "#contact":

(ns my.app
  (:require
    [hoplon.core :as h]
    [hoplon.history :as history]))

(def route (history/history-cell))

(defelem page []
  (h/div
    (h/nav (h/a :href "/#home" "Home") (h/a :href "/#contact" "Contact us"))
    (h/case-tpl route
      "home" (h/div "Welcome home")
      "contact" (h/div "Email us"))))

Navigating to any URL fragment will update the route cell, and cause the DOM to update appropriately.

Note that in the above example, as the nav element is the same on every "page" we rendered it outside the case-tpl. This is the main advantage of the route being implemented as a cell - we are free to only update the parts of the rendered DOM that make sense to, while still leveraging existing abstractions.

Advanced routing with Secretary

Secretary is a routing library for ClojureScript that works by interpreting strings passed to its dispatch! function.

https://github.com/gf3/secretary

It is definitely worth reading Secretary's documentation on GitHub, but essentially we can update a group of related cells based on the current route. Secretary supports both path and query parameters style strings.

For example, (using the cuerdas and url libraries as well), we can build some cells to help us create a paginated home page based on a URL like http://example.com/#/home?page=1.

We can add some utility functions in an app.route namespace:

(ns app.route
  (:require [clojure.string]
            [javelin.core :as j]
            [hoplon.core :as h]
            [cuerdas.core]
            [cemerick.url]
            [secretary.core]

(def r (h/route-cell))

(j/cell= (->
            r
            (cuerdas.core/strip-prefix "#")
            (cuerdas.core/strip-prefix "/")
            (#(str "/" %))
            secretary.core/dispatch!))

(defn generate-route
  ([path] (generate-route path nil))
  ([path query]
   {:pre [(sequential? path) (or (nil? query) (map? query))]}
   (str "#/"  (clojure.string/join "/" path)
              (if query (str "?" (cemerick.url/map->query query))))))

(defn set-route!
  "Set the URL hash to work for the given route. Works for a raw string or the same arguments as generate-route."
  ([route]
   (if (string? route)
       (set!  (.-hash (.-location js/window))
              route)
       (set-route! (generate-route route))))
  ([path query]
   (set-route! (generate-route path query))))

Now, on our home page:

(ns my.app
  (:require [app.route]
            [secretary.core :refer-macros [defroute]]))

(def route (cell :home))
(def page (cell 1))
(defroute "/home" [query-params] (dosync (reset! route :home)
                                         (reset! page (:page query-params)))

(defroute "/contact" [] (reset! route :contact))

(defelem page []
  (case-tpl route
    :home (div :id pager
            (map (fn [i] (div :click #(app.route/set-route! ["home"] {:page i})
                   (str "page " i)) [1 2 3])
             (case-tpl page
               1 (div "page 1!")
               2 (div "page 2!")
               3 (div "page 3!")))))
    :contact (div "Email us!")

Benefits of routes

Routes are just Javelin cells and so can be used everywhere that a cell can be, allowing for much more granular control over the application behavior.

Reflowing the DOM in response to a route change will generally be much faster than a round trip to the server for a new page, even for static HTML.

Certain application behavior is only possible with route based navigation where the page never reloads, such as loading animations or other transitions.

As routes are arbitrary strings they can support much more dynamic information than a static file name. For example, we could implement pagination on our #home route with a route like #home?page=1.

Drawbacks of routes

Navigation has to be controlled through application logic, which can be more complicated to engineer than pages.

Because routing generally relies on templates (or :toggle) it can result in higher memory usage over time as the DOM elements rendered by templates are not garbage collected.