How do I access the 'resolve' property of a UI-Router root state from $stateChangeStart?

All we need is an easy explanation of the problem, so here it is.

I’m trying to implement a user auth check and access check system in my app, however I keep hitting roadblocks. I think I have it the correct way this time but I have one last hurdle.

A little background: I tried putting all of the code into the $rootScope.on($startChangeStart) and it worked, horribly… but it worked. The route was always redirected but due to the auth check on the backend it displayed the first request page for 1/2 a second and then the redirect page every time. Thus I tried ‘pausing’ page load by calling evt.preventDefault() right at the start of the $startChangeStart function, which worked, but trying to put the user back to the original route afterwards caused an infinite loop in the router.

So after more research and reading a lot of stack posts I’m certain that ‘resolve:’ is the proper place to put the auth check to ensure the page is not loading while it occurs, and then redirect the user if needed from the $startChangeStart. ($state and event are always undefined in my attempts to inject them into a resolve function) It seems like the winning combination.

My problem: I have the resolve on the root state in my app: ‘main’

This was to avoid code redundancy, however I cannot determine how to access the root state’s properties, and therefore the resolve result, from the $stateChangeStart function. The toState is the child state, while the fromState is either the previous state or an abstract state with the ‘^’ route…

Do I have to put the resolve on every child state for this to work, or is there a way to access the root state from this point?

Basic app setup:

angular.module('App', ['ui.router', 'ui.bootstrap', 'ui.event', 'AngularGM', 'ngResource'])
.config(['$urlRouterProvider', '$stateProvider', function($urlRouterProvider, $stateProvider){
    $urlRouterProvider
        .when('/home', '/')
        .when('', '/')
        .when('/sign-up/joe', '/sign-up')
        .otherwise('/');

    $stateProvider
        .state('main', {
            url: '',
            abstract: true,
            templateUrl: 'views/main.html',
            controller: 'MainCtrl',
            resolve: {
                checkAccess: ['accountService', function(accountService) {
                    accountService.checkAuth(function(){
                        accountService.checkAccess(function (access){
                            return access;
                        });
                    });
                }]
            }
        })
        .state('main.home', {
            url: '',
            abstract: true,
            templateUrl: 'views/home.html',
            controller: 'HomeCtrl'
        })
        .state('main.home.index', {
            url: '/',
            templateUrl: 'views/home/index.html'
        });


.run(['$rootScope', '$state', '$stateParams', 'accountService', function ($rootScope, $state, $stateParams) {
    $rootScope.$state = $state;
    $rootScope.$stateParams = $stateParams;
    $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
        console.dir(toState);
        console.dir(toParams);
        console.dir(fromState);
        console.dir(fromParams);
        if (!toState.checkAccess.allowed) {
            event.preventDefault();
            $state.transitionTo(toState.checkAccess.newState);
        }
    });
}]);

This is the output from the console.dir() calls on the two state objects:

Object
 name: "main.home.index"
 templateUrl: "views/home/index.html"
 url: "/"
 __proto__: Object

Object
 controller: "PlacesCtrl"
 name: "main.places.search"
 templateUrl: "views/places.html"
 url: "/places"
 __proto__: Object

Update

Oops, forgot to mention AngularJS version is v1.2.0-rc.2

$state.current console.dir()

Object
 abstract: true
 name: ""
 url: "^"
 views: null
 __proto__: Object

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

Yes, I believe you can access root state from the $stateChangeStart function.

When using pure AngularJS I normally use current.$$route

For example, using the following route

.when('/home', {
  title:'Home',
  bodyClass: 'meetings',
  controler: 'HomeCtrl'
})

I can access the root state like so

  $rootScope.$on('$routeChangeSuccess', function (event, current, previous) {

   if (current.$$route) {

     $rootScope.title      = current.$$route.title;

     $rootScope.bodyClass  = current.$$route.bodyClass;
   }

 });

Using ui-router it’s just a bit different as it’s called $state.current. And you can access all the properties associated to whatever route you hit (e.g: $state.current.url)

So on your code you could have something like this

  .run(['$rootScope', '$state', '$stateParams', function ($rootScope, $state, $stateParams) {

      $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
          console.log($state.current.url);
      });
  }]);

Method 2

You need not to use resolve. Take a look at my solution:

app.run ($rootScope, $state, Auth, ngDialog) ->
  $rootScope.$on '$stateChangeStart', (e, to) ->
    if to.authRequired and not Auth.isAuthenticated()
      e.preventDefault()
      Auth.currentUser().then(
        (user) ->
          $state.go(to)
        (failure) ->
          $rootScope.storedState = to
          $state.go('main')
          ngDialog.closeAll()
          ngDialog.open
            template: 'modals/login.html'
            controller: 'loginCtrl'
            className: 'ngdialog-theme-default'
      )

I use angular_devise and ngDialog but they are optional and you can implement it with your own user’s service.

Method 3

Is it possible to do the redirect from within your accountService? If you detect that the user fails your checkAuth or checkAccess functions, you could prevent the callback from executing and redirect the user to your error (or login) page.

Something else to consider is implementing some sort of variable/queue of states if you’d like to redirect someone to the login page to refresh their authorization/authentication and then return to the previous state.

Method 4

If you initialize your state with a default, empty object on resolve, you’ll be able to manipulate it within $stateChangeStart.

$stateProvider
  .state 'home',
    url: "/"
    resolve: {}

...

  $rootScope.$on '$stateChangeStart', (e, toState, toParams, fromState, fromParams) ->
    toState.resolve.x = ->
      $timeout ->
        alert "done"
      , 3000

See https://github.com/angular-ui/ui-router/issues/1165

Method 5

This answer is very late but it can be useful.

Resolves of a state and $stateChangeStart event are executed at the same time. By the time you try to access resolved data in $stateChangeStart, it’ll not be available but it’ll be available when $stateChangeSuccess event fires.

If you use $stateChangeStart then you’ll need to do checkAuth from two places $stateChangeStart event and main resolve. Since they have parallel execution, at least 2 network requests will be sent to server for the same data.

Instead use $stateChangeSuccess. Using this will ensure that your resolves are resolved and you can then check access. Also, instead of accessing resolved properties,access resolved data using angular service.

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply