Picking a router

So, now that we’ve actually decided to do something with URLs in the client, we’re going to need a router first. Given the fact that we’re working on an Angular app, we’re pretty much left with three choices: Angular’s own ngRoute, which is woefully incomplete for any serious routing; Angular’s new router, aptly called angular-new-router, which is pretty ambitious, and provides a path for migration from 1.x to 2.x; and UI Router, which at least does the things you’d expect from any serious routing library.

Back in August, I settled on angular-new-router. However, the one that’s available for 1.x is pretty unusable, and the release kept getting postponed, so I switched to UI Router, which turned out to be a much more pleasant experience than I would have expected. It does nested views pretty well, and its state-over-URLs approach makes total sense. It does not do everything I want it to do though, so I cracked a couple of fingers and spent a couple of days trying to make routing as slick as I need it to be. (Mind you, this is my first week of working with UI Router, so I’m probably doing a lot of stupid/reckless things, which you should definitely chide me for via the usual channels.)

Make a state always silently redirect to a child state

We have a couple of views where the default child view changes based on the object’s state or the user’s permissions. Suppose that when the user clicks on /case/:id, we want to show /case/:id/phase/register, or /case/:id/phase/resolve, based on case’s value.

Then there’s another thing: I only want to change the state, not the URL. When the user clicked the link, their only intent was to navigate to the object view, not the specific view we inferred based on the object’s state. That’s our decision and our problem. I don’t want to pollute navigation history and make sharing (via copying and pasting the current URL) harder.

UI Router has the concept of an abstract state, but as far as I can tell, it can only redirect to the default (or first) child state. We actually have to wait until all dependencies have been resolved. The most obvious candidate for that is onEnter, but there’s a catch: it doesn’t get called if that state was already activated via a child state. Which means you’re left in no man’s land if the user is on /case/:id/child-view and clicks on /case/:id.

To circumvent this, I decorated $state’s transitionTo method, which returns a promise which is resolved when the state has been successfully transitioned to. When that promise is resolved, I take at look at $state.$current (not $state.current), which has a parent property that allows me to build a list of states that are currently active, including parent states. I then iterate to this list, look for an onActivate method on the state definition, and I then invoke it with the resolved values which are stored in state.locals.globals. At this point, I can call $state.go, with { location: false }, and UI Router updates the state, but not the URL.

Implementation and example usage.

Dynamic view titles

Suppose you want to define a title for your view (for example, to actually put it in <title/>). People will suggest you just add a title property to a state definition, which you can then use in conjunction with $state.current or $stateChangeSuccess. However, suppose that title depends on the data you loaded for your view with the help of resolve. A string won’t do you any good then.

Thankfully, this one’s rather easy to solve — if you’re not afraid to use private API’s, that is. The resolved values of a state are stored in $state.$current.locals.globals. You can then use $injector.invoke on state.title with the aforementioned values to achieve the desired result.

Implementation and example usage.

Reusing the current view when the path changes

One slight downside of the URLs-for-everything approach, is that views get trashed and reconstructed when the path or state changes. When your view is a little expensive to set up, the user experience starts to suffer, as this process can cause a slight delay when the user clicks around. Now, that’s an acceptable trade-off if the user is navigating between completely different views, but it’s a bit jarring when all they’re doing is toggling a side bar, for instance.

UI Router actually supports a reload parameter, but that’s only respected if all that is changed were query parameters. When the path changes, it always reloads the view. What we want here, is to be able to specify, based on a couple of parameters, whether the view should be reloaded. Then there’s notify, which is used to broadcast both $stateChangeStart and $stateChangeSuccess.

To tackle this issue, I again decorate $state.transitionTo, look for a shouldReload method, and invoke that with the current state & stateParams, and the target state along with its parameters. The method can then determine whether or not the view should be reloaded by comparing states & parameters. If it should reload, the transitionTo call is just proxied. When it should reload, I merge the options parameter with { notify: false }. As it turns out, the $stateChangeSuccess event — which is only broadcast if notify is truthy — is used by uiView do determine whether it should rebuild the view. One little catch: the $stateParams object is not updated. As a workaround I implemented an observableStateParams service, which uses the current url and state to return the state parameters when called (which is a better solution anyway).

Implementation and example.

Parallel (or auxiliary) states

Modals are the worst. Really. At least I think they are. Unfortunately for me, and my feelings, there are cases in which a modal absolutely makes sense. However, often URLs are sacrificed: it’s pretty hard to deeplink to modals because they live next to your views, instead of in one of them. The user could have opened the modal from anywhere. And even if you manage to deeplink to a modal, how do you decide what to show in your view?

What you want is parallel states: states that live next to each other, instead of nested states. Angular’s new component router actually supports this, and calls it auxiliary states. There’s something like it in UI Router Extras, but as far as I can tell, it just keeps the view around. The URL no longer has a reference to the active view, only the modal.

Now, this is a little difficult to achieve with UI Router, as it expects only one state to be active. For parallel states, you should have support for more than one. I say should, because there’s a way to fake it, and even though it makes me feel terrible, it kind of works. Here it is: I override $stateProvider.state() to make sure I can keep track of the registered states (UI Router doesn’t provide this functionality as far as I know). Also, when a state is registered with { auxiliary: true }, I lookup the parent state, and its children, and then I simply copy the auxiliary state as a child state of all the aforementioned states. The only thing I do is joining the URLs with a ! in between, so I can separate the states. This works surprisingly OK: for example, I can now use the onEnter and onExit properties on the state definition to open and close modals. This isn’t scalable at all though — if you want to add an auxiliary state at the top level, that’ll cost you hundreds of extra states if your app has anything but super-simple routing.

Implementation and example usage.