Compassus
A routing library for Om Next.
Read the announcement blog post.
Contents
Installation
Stable
Leiningen dependency information:
[compassus "1.0.0-alpha1"]
Maven dependency information:
<dependency>
<groupId>compassus</groupId>
<artifactId>compassus</artifactId>
<version>1.0.0-alpha1</version>
</dependency>
Guide
To get started, require Compassus somewhere in your project.
(ns my-app.core
(:require [om.next :as om :refer-macros [defui]]
[compassus.core :as compassus]))
Declaring routes
Your application's routes are represented by a map of keywords (the route handlers of your application) to the respective Om Next component classes. The following example shows the routes for a simple application that has 2 routes, :index
and :about
:
(defui Index
...)
(defui About
...)
(def routes
{:index Index
:about About})
To specify the initial route of the application, use the :index-route
configuration
key in the Compassus application configuration map:
(def app
(compasssus/application
;; :index is the initial route of the application
{:routes {:index Index
:about About}}
:index-route :index))
Routes can also be idents. Below is an example route definition that uses an ident as the route key.
(defui Item
...)
(defui ItemList
...)
{:items ItemList
[:item/by-id 0] Item}
Assembling a Compassus application
Creating a Compassus application is done by calling the application
function.
This function accepts a configuration map that should contain your routes and
an Om Next reconciler. Note that the parser must be constructed with compassus.core/parser
.
Here's an example:
(def app
(compassus/application
{:routes {:index Index
:about About}
:index-route :index
:reconciler (om/reconciler
{:state {}
:parser (compassus/parser {:read read))})}))
Implementing the parser
The parser is a required parameter to an Om Next reconciler. As such, a Compassus application also needs to have one, with the added advantage that most of the plumbing has been done for you. The parser in a Compassus application will dispatch on the current route. Therefore, all that is required of a parser implementation is that it knows how to handle the routes that your application will transition to. An example is shown below with routes we have previously declared.
For convenience, the parser's env
argument contains a :route
key with the current
route of the Compassus application.
;; we declared routes for `:index` and `:about`.
;; our parser should dispatch on those keys:
(defmulti read om/dispatch)
(defmethod read :index
[env k params]
{:value ...
:remote ...})
(defmethod read :about
[env k params]
{:value ...
:remote ...})
In some cases you may not want the parser to dispatch on your application's current
route, but instead on the query that its component declares. This is optionally
possible by passing an optional :route-dispatch
key in the configuration map
passed to compassus.core/parser
. If set to false
, the parser will not dispatch
on the current route, but instead on the query of the component pertaining to that
route. Here's an example:
(defui Home
static om/IQuery
(query [this]
[{:menu (om/get-query Menu)}
{:footer (om/get-query Footer)}])
Object
(render [this]
...))
(def app
(compassus/application
{:routes {:index Home}
:index-route :index
:reconciler (om/reconciler
{:state {}
:parser (compassus/parser {:read read
:route-dispatch false))})}))
;; our parser won't dispatch on the `:index` key (the current route), but instead
;; on the `:menu` and `:footer` keys that are part of `Home`'s query:
(defmulti read om/dispatch)
(defmethod read :menu
[env k params]
{:value ...
:remote ...})
(defmethod read :footer
[env k params]
{:value ...
:remote ...})
Mixins
The configuration map you pass to compassus.core/application
can also contain an
optional :mixins
key. Its value should be a vector of mixins. Mixins hook into
the generated Compassus root component's functionality in order to extend its capabilities
or change its behavior. The currently built-in mixin constructors are:
compassus.core/wrap-render
:
Constructs a mixin that will wrap all the routes in the application. It becomes
useful to specify this mixin whenever you want to define common presentation logic
for all the routes in your Compassus application. wrap-render
takes a function
or an Om Next component class. The component class may or may not implement
om.next/IQuery
— a query that refers to data that you want to have available to
the wrapper, e.g. the current logged in user and such.
The wrapper will be passed a map with the keys below. If the wrapper is a simple
function, the map will be passed as argument. If it is a component class, the map
will be in the component's props (accessible through om.next/props
). If the wrapper
implements om.next/IQuery
, its props will be the data that the query asks for.
You'll find the map with the keys below in the component's computed props, accessible
through om.next/get-computed
.
- :owner - the parent component instance
- :factory - the component factory for the current route
- :props - the props for the current route.
Note: There is only supposed to be one wrap-render
mixin under the :mixins
key. If there are more than one, Compassus will only use the first it finds.
Example:
;; Wrapper that doesn't implement `om.next/IQuery`
(defui Wrapper
Object
(render [this]
;; the wrapper doesn't implement `om.next/IQuery`, so the map is accessible
;; through `om.next/props`:
(let [{:keys [owner factory props]} (om/props this)]
;; implement common presentation logic for all routes
;; call the given factory with props in the end
(factory props))))
(def wrapper (om/factory Wrapper))
(def app
(compassus/application
{:routes ...
:reconciler (om/reconciler ...)
:mixins [(compassus/wrap-render wrapper)]}))
;; Example of a wrapper that implements `om.next/IQuery`
(defui Wrapper
static om/IQuery
(query [this]
[:current-user])
Object
(render [this]
;; `:current-user` will be in props, the other keys will be in computed props:
(let [{:keys [current-user]} (om/props this)
{:keys [owner factory props]} (om/get-computed this)]
(dom/div nil
;; implement common presentation logic for all routes
(dom/p nil (str "Current logged in user: " current-user))
;; call the given factory with props in the end
(factory props)))))
compassus.core/will-mount
:
Constructs a mixin that will hook into the componentWillMount
lifecycle method
of the generated root component. Takes a function which will receive the component
as argument. Useful to perform any setup before the Compassus application mounts.
Example:
(compassus.core/will-mount
(fn [self]
;; sets a property in the state of the root component
(om/set-state! self {:foo 42})))
compassus.core/did-mount
:
Constructs a mixin that will hook into the componentDidMount
lifecycle method
of the generated root component. Takes a function which will receive the component
as argument. Useful to perform any setup after the Compassus application mounts.
Example:
(compassus.core/did-mount
(fn [self]
(start-analytics!)))
compassus.core/will-unmount
:
Constructs a mixin that will hook into the componentWillUnmount
lifecycle method
of the generated root component. Takes a function which will receive the component
as argument. Useful to perform any cleanup after the Compassus application unmounts.
Example:
(compassus.core/will-unmount
(fn [self]
(stop-analytics!)))
A note on mixins
Mixins are just data. Compassus built-in mixin constructors are just helpers around assembling this data. For example, building a mixin to hook into the Compassus root component's query could also be done as shown below:
(def app
(compassus/application
{:routes ...
:reconciler (om/reconciler ...)
:mixins [{:render MyWrapper}]}))
Utility functions
There are a few utility functions in compassus.core
. Below is a description of
these functions along with simple examples of their usage.
root-class
Return the Compassus application's root class.
(compasssus/root-class app)
mount!
Mount a compassus application in the DOM.
(compassus/mount! app (js/document.getElementById "app"))
get-reconciler
Get the reconciler for the Compassus application.
(compassus/get-reconciler app)
application?
Returns true if the argument is a Compassus application.
(compassus/application? app)
;; true
current-route
Returns the current application route.
(compassus/current-route app)
compassus-merge
By default, Compassus uses Om's default-merge
function to merge remote responses into the application state. If your server responses are keyed by the current route, use compassus-merge
as the :merge
in the reconciler. It will look for the current route in the remote response and merge that into the application state instead.
(compassus/application
{:routes ...
:reconciler (om/reconciler
{:state ...
:parser ...
:merge compassus/compassus-merge})})
Changing routes
To change the current route of a Compassus application, call the function set-route!
. An example follows:
;; the argument to `set-route!` can be one of: a Compassus application, an
;; Om Next component or an Om Next reconciler
(compassus/set-route! app :about)
(compassus/set-route! reconciler :about)
(compassus/set-route! this :about)
The set-route!
function can take an additional argument, a map with the following
supported options:
queue?
: boolean indicating if the application root should be queued for re-render. Defaults to true.params
: map of parameters that will be merged into the application state.tx
: transaction(s) (e.g.:'(do/it!)
or'[(do/this!) (do/that!)]
) that will be run after the mutation that changes the route. Can be used to perform additional setup for a given route (such as setting the route's parameters).
Examples:
;; don't re-render from root
(compassus/set-route! app :about {:queue? false})
;; don't re-render from root, but re-read `:friends/list`
(compassus/set-route! app :about {:queue? false
:tx [:friends/list]})
;; merge `{:route-params {:id 42}}` into the app-state
(compassus/set-route! app :about {:params {:route-params {:id 42}}})
;; run the `set-route-params!` tx after changing route.
(compassus/set-route! app :about {:tx '[(set-route-params! {:id 42})]})
Integrating with browser history
URL (or path) navigation is an orthogonal concern to routing in Om Next components,
which is mainly about swapping components in and out according to the selected route.
However, it might be desirable for applications to setup history navigation only
when the application mounts. In addition, applications might also want to teardown
history if the application unmounts from the DOM. This can easily be achieved in
Compassus through the use of the did-mount
and the will-unmount
mixins.
Below are two examples, one using Bidi and
Pushy, and another using
Secretary and goog.History
.
Bidi + Pushy example
(ns my-ns
(:require [om.next :as om :refer-macros [defui]]
[compassus.core :as compassus]
[bidi.bidi :as bidi]
[pushy.core :as pushy]))
(def bidi-routes
["/" {"" :index
"about" :about}])
(declare app)
(def history
(pushy/pushy #(compassus/set-route! app (:handler %))
(partial bidi/match-route bidi-routes)))
(def app
(compassus/application
{:routes {:index Index
:about About}
:index-route :index
:mixins [(compassus/did-mount (fn [_]
(pushy/start! history)))
(compassus/will-unmount (fn [_]
(pushy/stop! history)))]}))
goog.History
example
Secretary + (ns my-ns
(:require [om.next :as om :refer-macros [defui]]
[compassus.core :as compassus]
[secretary.core :as secretary]
[goog.history.EventType :as EventType]
[goog.events :as evt]])
(:import goog.History))
(declare app)
(defroute index "/" []
(compassus/set-route! app :index))
(defroute about "/about" []
(compassus/set-route! app :about))
(def event-key (atom nil))
(def history
(History.))
(def app
(compassus/application
{:routes {:index Index
:about About}
:index-route :index
:mixins [(compassus/did-mount (fn [_]
(reset! event-key
(evt/listen history EventType/NAVIGATE
#(secretary/dispatch! (.-token %))))
(.setEnabled history true)))
(compassus/will-unmount (fn [_]
(evt/unlistenByKey @event-key)))]}))
Documentation
There's API documentation here.
There are also some devcards examples here. Refer to their source for more information.
Companies using Compassus
Copyright & License
Copyright © 2016 António Nuno Monteiro
Distributed under the Eclipse Public License (see LICENSE).