Angular ui-router with Yesod

26 Aug 2013haskellyesod

For BayesHive, we have a fairly big Yesod web application plus a lot of client-side JavaScript code. We’ve gone through a couple of iterations of how we organise all this. We started with a quick and nasty manual approach to chaining state between different pages of the app. That was pretty horrible. Then, as described in an earlier article, we switched to using AngularJS in the browser, which made the JavaScript side of things much nicer, along with Michael Snoyman’s 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")

Here, the $(...) 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 addSharedModule and injectLibraryModule functions help to manage Angular modules that we need throughout our client-side code and the addRedirection and addDefaultRedirection functions help to set up some simple URL redirections in the ui-router state-based router. (It’s also possible to set up “commands” for use within client-side JavaScript code, though I don’t do that here.)

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:

The 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. folder, doc.view, 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 folder.hamlet and folder.julius in 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 buildStateUI function discovers the states that are required). You can also optionally provide a Julius file. States can have parameters that are passed to the state controller on transition into the given state. Parameters are given names taken from a JavaScript comment line at the beginning of the state’s Julius file, of the form // params: a b c, i.e. a space separated list of state parameters introduced by the string params:.

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, doc.view, doc.edit and doc.options in module test, the following files may exist: .../doc.hamlet, .../doc.julius, .../doc/view.hamlet, .../doc/view.julius, .../doc/edit.hamlet, .../doc/edit.julius, .../doc/options.hamlet, .../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.