10

As far as I understand ui.router

You have $stateProvider

in it you can write $stateProvider.state()

you can write

    .state('account', {
        url: '/account',
        template: '<ui-view/>'
    })
    .state('account.register', {
        url: '/register',
        templateUrl: '/account/views/register.html',
        data: { title: 'Create Account' },
        controller: 'AccountRegister'
    })

but 'account' is a child of some kind of root state.

So my thought is, the root state of ui.router has a <ui-view/>, just like 'account' has the template '<ui-view/>', so index.html would be the root state. Even 'home' with url: '/' would be or rather is a child of this root state.

and if I'm able to access the root state and say it should resolve a User service this resolved value should be available on all states (or better on every state that is a child of the root state, aka all). The User service promise makes a $http.get('/api/user/status') and it returns {"user":{id: "userid", role: "role"}} or {"user":null} This would guarantee that the value returned by the User service is always populated.

How do I access the root state and say it should resolve e.g. User.get()?

Or let me rephrase that.

A user object should be available to all controllers. The information about the currently logged in user is supplied by a $http.get('/api/user/status') That's the problem and I'm looking for a DRY solution that, assuming the api is serving, is 100% safe to assume a user object is set, aka the promise is always fulfilled, aka "waiting for the promise to resolve before continuing".

  • I solved it differently. When a user logs in `$window.sessionStorage.token` is set. In the module's .config : `$rootScope.$on('$locationChangeStart',function(){ $rootScope.loggedin = !!window.sessionStorage.token;})` then in the view `ng-show="$root.loggedin"` or `ng-show="!$root.loggedin"` works reliable and better than ui-router's $stateChangeStart. The maintainer isn't helpful at all either. –  May 21 '14 at 19:26
  • I'm assuming you are familiar with how to use the state resolve property? https://github.com/angular-ui/ui-router/wiki#resolve – Levi Lindsey Jul 15 '14 at 15:43
  • This article describes how to use the resolve property to pre-load resources for ALL routes: http://www.jvandemo.com/how-to-resolve-application-wide-resources-centrally-in-angularjs-with-ui-router/. – Levi Lindsey Jul 15 '14 at 15:57
  • Yes but I was looking for a solution that is DRY. And, like I wrote, user logs in -> token is saved to sessionStorage. On the server side, the resource serving endpoint the token is checked for validity. And there's an AuthInterceptor service that redirects to a login screen if status is 401 or 403. –  Jul 17 '14 at 22:09
  • The whole point of the second article is that it is a DRY solution for what you are looking for. – Levi Lindsey Jul 18 '14 at 20:55
  • No it's not, I have to specify resolve: in each route definition (state). Also it's not an answer to the original question which was about attaching resolve to the root state aka the root of all states (routes). –  Jul 19 '14 at 15:04

4 Answers4

6

As of ui-router 1.0.0-beta.3 you can now do this easily using transition hooks:

angular
  .module('app')
  .run(run);

function run($transitions, userService) {

  // Load user info on first load of the page before showing content.
  $transitions.onStart({}, trans => {

    // userService.load() returns a promise that does the job of getting
    // user info and populating a field with it (you can do whatever you like of course).
    // Just remember to finally return `true`.
    return userService
      .load()
      .then(() => true);
  });
}

The state manager will wait for the promise to resolve before continuing loading of states.

IMPORTANT: Make sure that userService loads its data only once, and when it's already loaded it should just return an already resolved promise. Because the callback above will be called on every state transition. To deal with login/logout for example, you can create a userService.invalidate() and call it after doing the login/logout but before doing a $state.reload().

mrahhal
  • 3,322
  • 1
  • 22
  • 41
  • 2
    This is excellent, given how cumbersome are popular solutions such as https://stackoverflow.com/a/22540482/113291 – Olivvv Jun 15 '17 at 05:58
  • Implemented, tested, this seems all fine for me. Please everybody upvote this, or point flaws if there is any. – Olivvv Jun 15 '17 at 08:11
  • Very handy! Thanks for sharing. Much less hacky than other solutions – Alex White Aug 11 '17 at 16:32
5

Disclaimer — messing around with you root scope is not a good idea. If you're reading this, please note that the OP, @dalu, wound up canning this route and solved his issue with http interceptors. Still — it was pretty fun answering this question, so you might enjoy experimenting with this yourself:


It might be a little late, but here goes

As mentioned in the comments and from my experience so far, this is not possible to do with the ui-router's api (v0.2.13) seeing as they already declare the true root state, named "". Looks like there's been a pull request in for the past couple years about what you're looking for, but it doesn't look like it's going anywhere.

The root state's properties are as follows:

{
    abstract: true,
    name: "",
    url: "^",
    views: null
}

That said, if you want to extend the router to add this functionality, you can do this pretty easily by decorating the $stateProvider:

$provide.decorator('$state', function($delegate) {
  $delegate.current.resolve = {
    user: ['User', '$stateParams', httpRequest]
  };
  return $delegate
});

Note that there are two currents: $delegate.current - the raw "" state - and $delegate.$current - its wrapper.

I've found a bit of a snag, though, before this becomes the solution you were looking for. Every time you navigate to a new state, you'll make another request which has to be resolved before moving forward. This means that this solution isn't too much better than event handling on $stateChangeStart, or making some "top" state. I can think of three work-arounds off the top of my head:

  • First, cache your http call. Except I can see how this pretty much invalidates certain use-cases, perhaps you're doing something with sessions.
  • Next, use one of your singleton options (controller/service) to conditionally make the call (maybe on just set a flag for first load). Since the state is being torn down, it's controller might be as well - a service might be your only option.
  • Lastly, look into some other way of routing - I haven't used ui.router-extras too much, but sticky states or deep state redirect might do the trick.

I guess, lastly, I'm obligated to remind you to be careful with the fact that that you're working on the root-level. So, i mean, be about as careful as you should be when doing anything in root-level.

I hope this answers your question!

stites
  • 4,903
  • 5
  • 32
  • 43
  • 1
    It does, but note to everyone reading, use http interceptor instead of fiddling with the root state or some resolving on each state change or similar. Can't stress it enough. The way I wanted to solve it originally is not the right way -> Use $http interceptors. –  Feb 11 '15 at 02:14
  • 3
    +1 I'll add a disclaimer at the top — I kind of mention at the bottom that you shouldn't futz around in the root scope... but I didn't want to be too abrasive since that's seemed to be exactly what the question was asking for. – stites Feb 12 '15 at 03:31
0

Here is different approach

Add following code to your app's .run block

// When page is first loaded, it will emit $stateChangeStart on $rootScope
// As per their API Docs, event, toState, toParams, fromState values can be captured
// fromState will be parent for intial load with '' name field
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState){

    // check if root scope
    if(fromState.name == ''){

        // add resolve dependency to root's child
        if(!toState.resolve) toState.resolve = {};
        toState.resolve.rootAuth = ['$_auth', '$q', '$rootScope', function($_auth, $q, $rootScope){


            // from here on, imagine you are inside state that you want to enter
            // make sure you do not reject promise, because then you have to use 
            // $state.go which will again call state from root and you will end up here again
            // and hence it will be non ending infinite loop

            // for this example, I am trying to get user data on page load
            // and store in $rootScope
            var deferred = $q.defer();

            $_auth.user.basic().then(
                function(authData){
                    // store data in rootscope
                    $rootScope.authData = authData;
                    deferred.resolve(authData);
                    console.log($rootScope.authData);
                },
                function(){
                    $rootScope.authData = null;
                    deferred.resolve(null);
                    console.log($rootScope.authData);
                }
            );

            return deferred;
        }];
    }
});

Use $state.go('name', {}, {reload:true}); approach in case you need to refresh authData on state change (so that transition state will always start from root else resolve to load authData will never get called as once root is loaded, it never needs to get called again {except page reload}).

Uday Hiwarale
  • 4,028
  • 6
  • 45
  • 48
-1

Use the resolve attribute on the state definition.

.state('root', {
    resolve: {
        user: ['$http', function($http) {
            return $http.get('/api/user/status')
        }]
    },
    controller: function($scope, user) {
        $scope.user = user;  // user is resolved at this point.
    }
})

see:

craigb
  • 16,827
  • 7
  • 51
  • 62
  • Is the root of all states called 'root'? Imho it can't be done and that should be the answer, except if someone posts an answer where someone describes how to extend ui-router so you can attach a resolver on the root of all states. In other scenarios where you have a top level state, that is a child of the root state @UkuleleFury 's blogpost is correct. –  Jul 25 '14 at 16:10
  • No, you can name a state whatever you want. Also there doesn't need to be a "root" state i.e. nothing has to have url of "/", and there isn't a single state that all other states have to be descendants of (nested). – craigb Jul 25 '14 at 16:21
  • I was "talking" about the root of all states tho not the literal root named state. –  Jul 27 '14 at 13:12