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:
app.bottom.page is entered
ui-router attempts to resolve MyConfigDependingResource, but the injection fails,
because Config has never been initialized
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.
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.
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.
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.
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
}
});
Related
I know there is already a similar question Angular-UI Router - Resolve not waiting for promise to resolve? but is not exactly like my problem.
I have two states:
.state('entities', {
url: '/',
...
resolve: {
data: function (xyzService) {
return xyzService.listEntities();
}
})
.state('entities1', {
url: '/entities1',
...
resolve: {
data: function (xyzService) {
return xyzService.listEntities();
}
})
And both states require or depends on the same collection of entities, which I return from xyzServce.listEntities(). This service method from its side returns an array from entities and returns a promise.
So I expect to have this 'data' in the controller before to use it.
All works perfectly when to use the controller's 'data' for the fist time when the state is called. The problem comes, when I call for example first
state 'entities' after that state 'entities1' and finally state 'entities'.
So at this step in state 'entities' resolve does not wait to resolve all dependency and in the controller for this state I have 'data' with no array elements and $resolved property to false and an empty $promise object.
So could you tell me why is this behavior? Should I expect each time when the state is called to have resolved the dependency, or this works by specification only for the first time when the page is called?
Could you give me some ideas how to resolve this problem?
Best Regards,
Requested example of the service:
angular
.module(SECURITY_MODULE)
.factory('roleService', roleService);
roleService.$inject = ['$resource', 'DOMAINS', 'notificationService', '$filter'];
function roleService($resource, DOMAINS, notificationService, $filter) {
var service = {
...
listRoles: listRoles
...
};
var roleResource = $resource(DOMAINS.api + 'roles/:id/:command/:command2', {id : '#id'}, {
....
'listRoles': {method: 'GET', isArray: true}
});
function listRoles(callbackSuccess, errorCallback) {
var roles = roleResource.listRoles(function() {
callbackSuccess && callbackSuccess(roles);
}, function(error) {
errorCallback && errorCallback(error);
});
return roles;
}
....
return service;
}
As listed in the docs, the resource immediately returns a value (empty object or empty array), then fetches the data and later assigns it.
https://docs.angularjs.org/api/ngResource/service/$resource
It is important to realize that invoking a $resource object method immediately returns an empty reference (object or array depending on isArray). Once the data is returned from the server the existing reference is populated with the actual data. This is a useful trick since usually the resource is assigned to a model which is then rendered by the view.
This works fine when used directly inside a controller but doesn't work with the router's resolve, because for the router library, the value is already resolved with the initial value.
So as #Dmitriy noted, you should use the promise of the resource, that is provided via the $promise field. This will not resolve before the value is present or an error occured.
For more details see this great post on the topic: http://www.jvandemo.com/how-to-resolve-angularjs-resources-with-ui-router/
If you use angular $resource you should return xyzService.listEntities().$promise
https://docs.angularjs.org/api/ngResource/service/$resource
My dependencies to be resolved rely on data which changes. How do you force angular to resolve dependencies again?
$routeProvider.
when('/blah', {
templateUrl: '/static/views/myView.html',
controller: 'myCtrl',
resolve: {
theData: function(myFactory) {
return myFactory.promise;
}
}
}).
i.e, every time this when block is executed I want to re-resolve the dependency.
Is this trivial, or not so much?
Resolve block is rechecked every time route changes. In your case you need to return new promise each time, instead of how you are doing it currently. Looks like you cache and return always the same promise object.
resolve: {
theData: function(myFactory) {
return myFactory.getSomething();
}
}
Make myFactory.getSomething return new promise in each invocation.
I request some data from Firebase using AngularFire like below.
$rootScope.userTags = $firebase($fbCurrent.child('tags')).$asArray();
On logging this data in following ways I get the outputs as in the image below. So,
console.log($rootScope.userTags);
console.log($rootScope.userTags[0]);
$rootScope.userTags.$loaded().then(function () {
console.log($rootScope.userTags[0]);
});
In first log statement you can notice an $FirebaseArray object with five elements. In second log statement when I try to access one of the elements, I get undefined although you can clearly see those elements in the un$loaded array. Now in the last log statement, calling $loaded lets me access those elements without any issues.
Why does it behave this way? And is there a way I can access those elements, since those are already there, without calling $loaded on the array?
The $asArray() or $asObject() methods are asynchronous actions. The data has to be downloaded from Firebase before it can be accessed. This is just the nature of asynchronous data and not anything specific to Firebase.
The way we handle this issue of asynchrony is through the $loaded promise.
It seems though the real issue here ensuring data is loaded upon instantiation of the controller. You can solve this be resolving the data in the router.
Resolve require a promise to be returned. When the user routes to that page, Angular will not load the page until the promise has been resolved. The data resolved from the promise will injected into the controller.
.config(['$routeProvider',
function($routeProvider) {
$routeProvider
.when('/resolve', {
templateUrl: 'resolve.html',
controller: 'ResolveCtrl',
resolve: {
// data will be injected into the ResolveCtrl
data: function($firebase) {
// resolve requires us to return a promise
return $firebase(new Firebase('your-firebase')).$asArray().$loaded();
}
}
})
}
])
Now that we are resolving the data in the route we can access it in our controller.
.controller('ResolveCtrl', function($scope, data) {
$scope.userTags = data;
$scope.userTags[0]; // data has been resolved from the promise
});
My AngularJS resolve doesn't finish before the controller is loaded.
Background: Say I have a "my profile" page that is requested (user loads website.com/user). I would like to have angular intercept the request and ask for the appropriate data from the server.
The server will reply differently depending on whether the user is logged in/active/etc. I would like angular to get the data from the server before it loads the template, and before it initiates the appropriate controller. So far it does not.
App.js code:
.when('/user',
{
templateUrl:'/templates/user',
controller: 'my_profile',
resolve: {
some_cute_function_name: function($timeout){
$timeout(function(){
console.log('here123');
},5)
}
}
Meanwhile, controller code:
console.log('about to update scope');
It turns out that the controller is initiated before the resolve is done. I know this because the greater the number of seconds before the $timeout is done, the more likely I am to see this:
about to update scope
here123
as opposed to the other way around.
Is there any way to ensure that the resolve is resolved before the controller is initiated?
Much appreciated!
Your resolver needs to return a promise:
.when('/user',
{
templateUrl:'/templates/user',
controller: 'my_profile',
resolve: {
some_cute_function_name: function($timeout){
return $timeout(function(){
console.log('here123');
return { some: 'data' };
},5);
}
}
AngularJS describes resolve as:
"An optional map of dependencies which should be injected into the controller. If any of these dependencies are promises, the router will wait for them all to be resolved or one to be rejected before the controller is instantiated." (emphasis added)
I take it that unless any of the dependencies are promises, angular won't wait for resolve to finish before instantiating the controller (even if I have operations that take a long time to finish, like a $timeout).
Solution: you have to make sure the resolve involves a promise.
I want my Angular application to resolve a promise before changing the route to a certain path:
va.config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/sendungen', {templateUrl: '../partials/sendungen.php', controller: 'OverviewCtrl',resolve: {
shipments: oc.fetchAllShipments
}}).
// ...
}]);
The funcion fetchAllShipments():
oc.fetchAllShipments = function(shipment){
shipment.fetchAllShipments().then(function(promise){
shipment.allShipments = promise.data;
});
};
The controller shall then copy the data from the shipment service to its $scope:
va.controller('OverviewCtrl',function($scope,$http,shipment){
$scope.allShipments = shipment.allShipments;
});
Everything is working fine as long as i change routes from within the application, e.g I load the mainpage, then switch to /sendungen
However, if i am already on that path and decide to refresh the page, the application is loaded before the data seems to be resolved. This happens only occasionally and seems to be depending on how fast he script was executed.
How do i prevent that behaviour?
The function in the resolve should return a promise, not like in your oc.fetchAllShipments method.
resolve - An optional map of
dependencies which should be injected into the controller. If any of
these dependencies are promises, the router will wait for them all to
be resolved or one to be rejected before the controller is
instantiated. If all the promises are resolved successfully, the
values of the resolved promises are injected and $routeChangeSuccess
event is fired.
For example:
resolve: {
shipments: ['$q', function($q){
var deffered = $q.defer();
shipment.fetchAllShipments().then(function(res){
deffered.resolve(res);
});
return deffered.promise;
}]
}
The quick and dirty fix will be to use $timeout:
va.controller('OverviewCtrl',function($scope,$http,shipment, $timeout){
$timeout(function(){$scope.allShipments = shipment.allShipments}, 1000);
});