19

I have set up a top-level controller that is instantiated only when a promise (returned by a Config factory) is successfully resolved. That promise basically downloads the Web app configuration, with RESTful endpoints and so on.

$stateProvider
  .state('app', {
    url: '/',
    templateUrl: 'views/_index.html',
    controller: 'MainCtrl',
    resolve: {
      config: 'Config'
    }
  });

This setup allows me to kind-of assert that the configuration is properly loaded before any lower controller gets a chance to use it.

Now I need to inject, in a deeper nested controller, another factory that uses Config and only works when it is resolved (look at it like a $resource wrapper that needs some Web service URLs). If I do:

$stateProvider
  .state('app.bottom.page', {
    url: '/bottom/page',
    templateUrl: 'views/_a_view.html',
    controller: 'BottomLevelCtrl',
    resolve: {
      TheResource: 'MyConfigDependingResource'
    }
  });

it looks like the resolve evaluation order does not follow the controller hierarchy from top to bottom, but from bottom to top, therefore:

  1. app.bottom.page is entered
  2. ui-router attempts to resolve MyConfigDependingResource, but the injection fails, because Config has never been initialized
  3. The ui-router resolution stops because of an error (without even throwing Errors, but that's another issue), and Config is never initialized by the top level controller

Why is ui-router resolving dependencies in a reverse order? How can I easily resolve my TheResource object after the top level MainCtrl has resolved Config (without relying on $inject, of course)?

UPDATE: from this plnkr's log you can see that the top level resolve is attempted only after the nested controller has started its own resolving process.

frapontillo
  • 10,499
  • 11
  • 43
  • 54
  • 1
    The parent should be resolved first. Can you share a plunker? – Ilan Frumer Feb 24 '14 at 09:43
  • There you go: http://plnkr.co/edit/LmDKZ1nhyGwqDF19Ckhb?p=preview. As you can see from the console `$log` information, the first Config request is made because the `MyConfigDependingResource` is resolved by the nested controller, before the `Config` is resolved in the root controller. – frapontillo Feb 24 '14 at 10:17
  • @IlanFrumer have you tried executing the plunker? – frapontillo Feb 26 '14 at 17:47

4 Answers4

61

Similarly to @Kasper Lewau's answer, one may specify a dependency on resolves withing a single state. If one of your resolves depends on one or more resolve properties from the same resolve block. In my case checkS relies on two other resolves

.state('stateofstate', {
    url: "/anrapasd",
    templateUrl: "views/anrapasd.html",
    controller: 'SteofsteCtrl',
    resolve: {
        currU: function(gamMag) {
            return gamMag.checkWifi("jabadabadu")
        },
        userC: function(gamUser, $stateParams) {
            return gamUser.getBipi("oink")
        },
        checkS: ['currU', 'userC', 'gamMag', function(currU, userC, gamMag) {
            return gamMag.check(currU, userC);
        }]
    }
})

**PS: **Check the "Resolves" section of the following document for more details about the inner-workings of resolve.

Nikolay Melnikov
  • 1,395
  • 1
  • 15
  • 25
16

Resolve objects run in parallel, and as such doesn't follow a certain hierarchy. However, nested resolves are possible by defining a higher order resolve object as a dependency to your secondary resolves.

$stateProvider
  .state('topState', {
    url: '/',
    resolve: {
      mainResolveObj: ['$someService', function ($someService) {
        return 'I am needed elsewhere!';
      }]
    }
  })
  .state('topState.someOtherState', {
    url: '/some-other-place',
    resolve: {
      someOtherResolveObj: ['mainResolveObj', function (mainResolveObj) {
        console.log(mainResolveObj); //-> 'I am needed elsewhere!'. 
      }]
    }
  });

Kind of a bad example, but I take it you get the gist of it. Define the name of a higher level resolve object as a dependency to your lower level resolve and it'll wait for it to resolve first.

This is how we've solved preloading certain data before lower order resolve objects, aswell as authentication requirements (among other things).

Good luck.

1

I agree with you that the resolves should chain, and I've hit many problems around this area.

However, they don't, so the solution I have come up with is to use my own promise stored in a service which you resolve when the data is complete. I have tried my best to edit your plunkr to work but to no avail. I am no longer hitting your errors though, so hopefully you can work from this: http://plnkr.co/edit/Yi65ciQPu7lxjkFMBKLn?p=preview

What it's doing:

Store the config in a state alongside a new promise object

myapp.service('state', function($q) {
  this.load = $q.defer();
  this.config = {}
})

On your first resolve, store your config in this service and once ready resolve the new promise we created above.

myapp.factory('Config', function($http, $log, state) {
  return $http.get('example.json')
    .then(function (data) {
      angular.extend(state.config, data.data);
      state.load.resolve();
    });
});

The final and most important step is to not call our second the content of the childs resolve function until after our promise above is resolved:

myapp.factory('MyConfigDependingResource', function($log, state) {
  return state.load.promise.then(function() {
    if (!state.config.text) {
      $log.error('Config is uninitialized!');
    }
    else {
      // Do wonderful things
    }
    return
  });
});

The main thing you will need to be aware of is that the config is now stored in a state. There should be ways around this but this is how I've done it before.

Josh
  • 155
  • 1
  • 2
  • 10
1

While they cannot be chained, you can call one from another:

var resolve2 = ['$stateParams',function($stateParams){
  // do resolve 2 stuff
  ...
  return true;
}];

var resolve1 = ['$stateParams',function($stateParams){
  // do resolve 1 stuff
  ...
  // now do resolve2 stuff
  return $injector.invoke(resolve2,this,{$stateParams:$stateParams});
}];


$stateProvider.state("myState", {
  resolve: {
    resolve1: resolve1
  }
});
Remento
  • 927
  • 4
  • 6