Angular JS ui-router how to redirect to a child st

2019-01-17 22:05发布

问题:

When using onEnter to redirect to a state, if the new state is a child of the current state, an infinite loop occurs.

Example:

$stateProvider
  .state 'inventory',
    url: '/inventory'
    templateUrl: 'views/inventory.html'
    controller: 'InventoryCtrl'
    onEnter: () ->
      $state.go 'inventory.low'
  .state 'inventory.low',
    url: '/low'
    templateUrl: 'views/inventory-table.html'
    controller: 'LowInventoryCtrl'

When:

$state.go 'inventory.low'

Is called, the state inventory is re-initialized, causing it to be called again = infinite loop.

However, if the redirect state is:

$state.go 'otherStateThatIsNotAChild'

This issue does not occur. I assume that the parent state is being re-initialized, but why?

  1. Why is the parent state being reinitialized when .go is called on a child state?
  2. How then, would you handle redirecting to a child state?

回答1:

1) Why is the parent state being reinitialized when .go is called on a child state?

While a transition is in process, any $state.go/transitionTo will cause the currently in process transition to be Superseded. An in-process transition that is superseded is cancelled immediately. Since your original transition to inventory is not completed by the time all the states' onEnters are called, the original transition is cancelled and a new transition to inventory.low is started from the previously active state.

See ui-router src https://github.com/angular-ui/ui-router/blob/master/src/state.js#L897 ...

2) How then, would you handle redirecting to a child state?

You could...

  • Wrap $state.go in a $timeout() to allow the original transition to complete before redirecting.
  • Call $state.go from your controller instead. UI-router invokes the controller from the ui-view directive AFTER the transition is completed.

In any case, be very sure you want your app to redirect like this. If a user navigated directly to any other child state of inventory (such as inventory.high), the redirect will still occur, forcing them to inventory.low which would not be what they intended.



回答2:

I had the same problem. The simple solution I found is to listen state changes and redirect to your child state from there.

It's kind of hack that makes routes redirecting to child states by default. It's not needed to do url: '' or $state.go(), they don't work correctly.

So, in config file:

$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState) {
  if (toState.redirectTo) {
    event.preventDefault();
    $state.go(toState.redirectTo, toParams);
  }
});

In state file:

.state('home', {
  url: '',
  redirectTo: 'home.list'
});

I've made an example on gist: https://gist.github.com/nikoloza/4ab3a94a3c6511bb1dac



回答3:

You need to step back and think about what you're trying to achieve. What's the point of having a state when all it's doing is redirecting to a child state?

Regarding your first question, parent states are always activated when you arrive at a child state, this behaviour is extremely useful in sharing data among states, and without it nested routing would be impossible (or rather wouldn't make sense).

As for the 2nd question, I've worked on a few big angular apps and so far I haven't found myself needing to do that.

OK, believe it or not, as much I'd hate to say it, right now I came across a scenario where I needed to do this. I have a profile/userName route (technically this should be profile/userName/details) and a profile/userName/products route, I wanted to have a master view for both states but at the same time I wanted the profile/userName route have a clean url, like: profile/mike62, NOT profile/mike62/details. So I ended up doing this:

.state('publicProfile', { url: '/profile/{username}'})//this is the base/shell state
.state('publicProfile.details',{url:null})//I want this to be my de-facto state, with a clean URL, hence the null url
.state('publicProfile.products', {url:'/products/{exceptProductId:/?.*}'})

Ended up achieving it like this, there are many ways though:

in my publicProfile state controller (this is the base state):

$scope.state = $state;
$scope.$watch('state.current', function(v) {
       if(v.name=='publicProfile') {//want to navigate to the 'preferred' state if not going to publicProfile.products
            $state.go('publicProfile.details');
       };
}, true);

Yes, it does feel hacky but now I know that there are some edge cases where we want to do this, althopugh we could rethink our state design altogether. Another, dirtier way would be to check the current state in a $timeout with a small delay inside the base state, if we are not on the publicProfile.products state, we navigate to our preferred/de-facto state of publicProfile.details.



回答4:

It looks like you're trying to set a default child state.

That's a commonly asked question about ui-router: How to: Set up a default/index child state

The tl;dr is to use abstract states by settings abstract: true in the parent state. If you add multiple child states, it'll default to first child state.

$stateProvider

  .state('inventory', {
    abstract: true,
    url: '/inventory',
    templateUrl: 'views/inventory.html',
    controller: 'InventoryCtrl'
  }).

    .state('inventory.low', {
      url: '/low',
      templateUrl: 'views/inventory-table.html',
      controller: 'LowInventoryCtrl'
    });


回答5:

Per pdvorchik's answer on the related GitHub thread, there is a simple fix for this which worked perfectly for me, consisting of wrapping the $state.go call in a .finally() call to make sure the first transition completes before a new one is started.

onEnter: ['$state', function($state){
    if ($state.transition) {
        $state.transition.finally(function() {
            $state.go('home.my.other.state', {})
        });
    }
}]


回答6:

After @Chris T's explanation, it seems that the cleanest solution is to listen for the $stateChangeSuccess event:

redirects =
  inventory: 'inventory.low'

$rootScope.$on '$stateChangeSuccess', (e, toState) ->
  redirect = redirects[toState.name]
  $state.go redirect if redirect

Where redirects will contain any route redirects that may occur. Thanks all!



回答7:

Don't make the low inventory state a child state.

$stateProvider
    .state 'inventory',
        url: '/inventory'
        templateUrl: 'views/inventory.html'
        controller: 'InventoryCtrl'
        onEnter: () ->
            $state.go 'lowinventory'
    .state 'lowinventory',
        url: '/inventory/low'
        templateUrl: 'views/inventory-table.html'
        controller: 'LowInventoryCtrl'