Yesterday, I was fixing up a web view/page that contained nested Angular ui-router states to achieve parent/child detail. Interestingly, I discovered I was doing things the hard way.
My parent state contained a view with links to open the details. The route definitions looked like this:
.state('parent', { url: 'parent', templateUrl: 'parent.cshtml', controller: 'parentCtrl as vm' }) .state('parent.child', { url: '/child?id', templateUrl: 'child.cshtml', controller: 'childCtrl as vm' });
And then, the “parent” view displayed the list of parents with a clickable ui-sref to set the parent Id on the child state. The problem with this approach (and this where the nuance comes in) is that the parent Id is, effectively, not part of the state. That is to say, no matter how the URL changes by clicking the parent and changing the Id in the URL, ui-routers state events never fire. Rather than digging into this problem, I utilized the $locationChanceSuccess event to detect when the URL changed. This is the wrong approach. Not only is it wrong, it effectively made this particular view not work with the other navigation services I’ve written to deal with cancelling navigation if the user’s form is dirty and such.
The other problem with using $locationChangeSuccess or $locationChangeStart when, really, your state should reflect the change, is that you cannot prevent the location change. That is to say, logically, you can determine that your state hasn’t change, but your URL becomes out of sync with your actually state. My Id could be 2, but I may still be editing my previous record with an Id of 1. By using the $location events, you wind up having to create your own state management to know what to do with URL changes.
ui-router handles this for you. Back to the nuance, how then, do we make the parent Id part of our state to trigger ui-router’s state change events? The subtlety here is that the the Id should not be specified as a standard URL query parameter with the “?id” syntax. If we change the route’s URL as shown below, the Id becomes part of our state:
.state('parent.child', { url: '/child/:id', templateUrl: 'child.cshtml', controller: 'childCtrl as vm' });
This one little changed allowed me to remove all of this state management code that I had written. And to get the Id within the child view, we only need to use $stateParams.id. Finally, if we wanted to create a new detail without an associate parent, we could navigate to a state with the Id undefined like this:
$state.go("parent.child", { id: undefined });
With these changes in place and my “state management” code removed, the out of the box ui-router $stateChangeStart and $stateChangeSuccess events fired as expected. In the case of preventing navigation, the events could auto-magically be cancelled by the navigation service I already have in place.