Friday, December 13, 2013

AngularUI Router state and location change events

If you're using AngularJS with the AngularUI Router, and you want to intercept location change attempts so you can prevent certain changes or redirect a user to a login page - without flickering that will occur if you redirect after a page is loaded - one of the things you'll need to understand is the order of location change events and state change events. Their orders are different under different circumstances, a fact which tripped me up until I knew about it.

This was tested specifically with AngularJS 1.2.4 and AngularUI Router 0.2.5.

If the user clicks on a link created with ui-sref, or the state is changed with $state
  • $stateChangeStart will precede $locationChangeStart.
  • Calling event.preventDefault() in a $stateChangeStart listener will prevent the $locationChangeStart event, and will prevent the page change completely.
  • Calling event.preventDefault() in a $locationChangeStart listener without doing the same in a $stateChangeStart listener will prevent the URL from changing but will change the state (content), leaving your app in a bad situation.

If instead the user clicks on a link created with href, or types a URL into the browser, or the location is changed using $location
  • $locationChangeStart will precede $stateChangeStart.
  • Calling event.preventDefault() in a $locationChangeStart listener will prevent the $stateChangeStart event, and will prevent the page change completely.
  • Calling event.preventDefault() in a $stateChangeStart listener without doing the same in a $locationChangeStart listener will prevent the state (content) from changing, but the URL will change, leaving your app in a bad situation.

This difference makes sense in a way. If you trigger a state change, it goes through the UI Router, and then the UI Router triggers a location change. If you trigger a location change through $location, that's part of Angular itself, so UI Router will learn of the event after Angular. Regardless, this makes things tricky if you want your app to respond properly in both scenarios.

The solution
Synchronizing logic within both event listeners would be extremely difficult, if not impossible, to get right. My solution is to switch all my ui-sref links to be normal href links, and when I want to trigger a page change programmatically, I do it through $location, never through $state. That way every page change goes through Angular first, and the $locationChangeStart (and $locationChangeSuccess) is all I have to worry about.

I still use UI Router for its nested views, which I could not do without.

One more thing - the first visit
Keep in mind that in the case of a user typing a URL into the browser, the $locationChangeStart and $stateChangeStart event listeners will only be called if the user already has the site loaded. If it is the user's first visit to the site in that browser session, the listeners will not be called because they will not have been registered when the change started. You'll need another way to keep a user out of certain pages in that case. My solution of choice is to redirect the user from within the resolve block of the controller of each page that a non-logged-in user should not be able to access.

4 comments:

  1. Awesome blog post! It's unfortunate you need to do this. I bet it's still the case too. I wonder how we might be able to make this more developer friendly while still allowing people to use ui-sref.

    ReplyDelete
  2. Hey man thanks a lot for the post! It really helped me fixing some issues with StateChanges!!

    ReplyDelete
  3. Such good post, solve lots of my confusion.
    Use href instead of ui-href is a good solution, but, what about the $state.go('state')? Does it go to $locationChangeStart directly also?

    ReplyDelete
  4. heyy thanks for the post, just switched from ngRoute to ui-router and ws thinking of how to do the same.

    thanks for the blog. it has helped me a lot

    ReplyDelete