Angular ui-router with Yesod
yesod-angular module for managing the interaction between the client and server. Eventually, we found that Angular’s default routing provider wasn’t flexible enough for our needs in the browser (we have quite a few “wizard”-type stateful interactions, plus a fairly complex dashboard with lots of more independent states).
So we decided to switch over to using the Angular
ui-router state-based routing system. This is pretty good and is definitely flexible enough for our current needs. It allows you to define a hierarchical structure of UI states, cleanly controlling transitions between states and assigning URLs to states to allow good interaction with the browsers back and forward buttons and to allow bookmarking of application states. The only problem? Not something that the
yesod-angular code supports...
Template Haskell for fun and for profit
Anyone who’s worked with Yesod for a while knows that it makes heavy use of Template Haskell, mostly to read and manage page templates and scripts. The
yesod-angular code is no exception, so although I’m very far from being an expert with Template Haskell, it seemed natural to try to adapt the
yesod-angular code to work with
ui-router. This turned out to be a real success, and it wasn’t as hard as I thought it would be, mostly because Michael’s code provided a good example to follow and adapt.
What I wanted to have was a setup where I could, in a Yesod handler function, write something like the following:
runAngularUIWith (dashWrapper mdocnm) $ do $(addSharedModule "radian" True) $(addSharedModule "pretty-print" True) ... $(addRedirection "/build/linreg" "/build/linreg/selectdata") ... $(addDefaultRedirection "/folder/") injectLibraryModule "ui" injectLibraryModule "ngCookies" injectLibraryModule "ngGrid" $(buildStateUI "dashboard")
$(...) syntax is an invocation of a Template Haskell function that does some “behind the scenes” work and splices some more complicated code in in place of the simple calls. The
injectLibraryModule functions help to manage Angular modules that we need throughout our client-side code and the
addDefaultRedirection functions help to set up some simple URL redirections in the
All of this monadic code is wrapped up in a call to
runAngularUIWith, which unpicks the structures generated by the calls inside the
AngularUI monad to construct the (rather complicated) structures needed to build the tree of UI states for the state-based router, to associate URLs with these states (sometimes with associated parameters) and to set up partial pages and controller scripts for everything.
The real work of setting up all this information is done by the
buildStateUI. This is a Template Haskell function that constructs Angular
ui-router state definitions based on the contents of a directory under the
angular/ui-router directory. The top-level directory is named after the module name supplied to
buildStateUI. For a module named
mod, this directory should contain the following items:
mod.hamlet–Outer Hamlet template for module. This should contain an HTML element with an
ng-controllerdirective accessing the controller defined in the
mod.juliusfile, and should contain a single
ui-viewelement to contain the state-based content.
mod.julius–Controller file for top-level page. This should define a controller with the name used in the
mod.lucius–Optional top-level stylesheet definitions.
states–Directory containing the UI state definitions.
states directory contains a hierarchical representation of the states implemented by the UI. Each state has a name made up of a list of period-separated components (e.g.
build.dynsys.step1, etc.). The template files for each state are found in a directory path made from the elements of the state name. For instance, for a state
folder in a module
test, you would put
angular/ui-router/test/states/folder.hamlet, and so on. For a state
doc.view in the same module, you would have files
angular/ui-router/test/states/doc/view.hamlet and so on.
For each state, you must provide a Hamlet file (the Hamlet files are how the
// params: a b c, i.e. a space separated list of state parameters introduced by the string
Some states are abstract, i.e. they exist only to provide a framework for sub-states. When a sub-state of an abstract state is active, the parent abstract state is implicitly also active. Here, abstract states are identified by a comment at the top of their Julius file of the form
// abstract: true. All states not specifically marked as abstract are assumed to be non-abstract. Both abstract and non-abstract states may have sub-states, indicated by the presence of a directory with the same name of the state which contains the definitions for the sub-states. For example, if there are states
doc.options in module
test, the following files may exist:
.../doc/options.julius. In this case, the
doc state may or may not be abstract: in either case, the template in
doc.hamlet provides a wrapper around the templates of the sub-states, and controller code defined in
doc.julius is shared between the sub-states. Non-abstract states with sub-states are useful for implementing the case where some action is required to determine which of the sub-states should be entered at transition time: the code to determine the appropriate sub-state can live in the parent state controller and can trigger a transition to the appropriate sub-state once the initial transition processing is done.
(There is also some extra stuff to make it easier to manage modal dialogues on the client-side, but I don’t want to get into that here.)
Hiding the complexity
This all sounds horrifically complicated, I know. The
buildStateUI function has to traverse the directory tree defining the states, pick information out of the Hamlet and Julius files it finds, and use that information to set up all of the necessary structures to initialise the Angular state provider service. As usual with Template Haskell,
buildStateUI has a completely innocuous type signature (
Text -> Q Exp) which completely hides all this frantic scrabbling around that’s going on under the surface (at compile time...).
That complexity in
buildStateUI is deliberate though–the Template Haskell processing makes it possible to build very complicated UIs with really very little effort. All of the page partials and controller scripts are modularised and organised in the same hierarchy as the UI states, and once you have things set up, you can completely forget that
buildStateUI is running at compile time to generate your routing code for you (apart from some debug output it dumps to show you the states it’s found).
We now have a web app with a file browser/explorer type interface, document editing and rendering to HTML (with embedded MathML and Radian plots), data viewing and exploration, and wizard-based model builders for constructing different kinds of Bayesian statistical models (one of which includes a whole dynamical systems simulation tool). All of the transitions between the different UI states are managed through Angular’s
ui-router, and all of the setup code required to handle the 50-odd states is managed via the one
buildStateUI function. It’s quite cute. I don’t think I would have thought of it all on my own though–having Michael’s code to start from was a real help.