EDIT: this actually works, the problem was coming from something else.
I am struggling with an edge-case scenario, basically:
$stateProvider.state('myState', {
resolve:{
resolveA: function($q){
return $q.when('whatever');
},
resolveB: function(resolveA){
return fetchSomethingBasedOn(resolveA);
}
}
);
This does not work as resolveA can't be injected in resolveB function.
There are a few options I already considered but rejected:
Make the resolveA returned value an object {resolveA, resolveB}. Discarded because this would have many side effects on my existing controllers.
Handle the resolveB behaviour at the controller level. Discarded because this state is an abstract high level one, I don't wan't to change all its children states/controllers
ui-router nested resolve doesn't work on the same state, but it should work on nested states. You can access resolved objects from the parent state.
$stateProvider
.state('parent', {
resolve:{
resolveA: function($q){
return $q.when('whatever');
}
}
)
.state('parent.child', {
resolve:{
resolveB: function(resolveA){
return fetchSomethingBasedOn(resolveA);
}
}
);
The option that seems OK-ish is to attach the result of resolveB to the resolveA object and have only one resolve. i.e:
$stateProvider.state('myState', {
resolve:{
resolveA: function($q){
return $q.when('whatever').then(function(resolveA){
return fetchSomethingBasedOnResolveA(resolveA).then(function(resolveB){
resolveA.resolveB = resolveB;
return resolveA;
})
});
}
}
);
The solution to your problem is by using promises. You must be sure that the promise for resolveA is resolved before start with resolveB.
The code should be something like this.
$stateProvider.state('myState', {
resolve:{
resolveA: function($q){
var def = $q.defer();
//whatever you want i.e.
User.resource.get(function(perfil){
def.resolve(perfil);
});
//
return def.promise;
},
resolveB: function(resolveA){
return fetchSomethingBasedOn(resolveA);
}});
And this should solve your problems.
Greetings.
Related
I have a service function that I need to call each time a route is visited or refreshed. The function returns an Angular promise. The result of the promise needs to be loaded into the controller's scope each time the function is called.
I'm currently using a resolve argument on the state definition to call the function.
.state('app.theState', {
url: '/theRoute/:theRouteId',
views: {
'menuContent': {
templateUrl: 'templates/theRoute.html',
controller: 'TheRouteCtrl'
}
},
resolve: {theResolvedResult: function(theService){
return theService.theAsyncCall().then(function(result){
return result;
},function(errMsg){
return errMsg;
});}
})
The data is passed into the controller as an argument.
.controller( 'TheRouteCtrl', function($scope, $state, $log, theResolvedResult)
I am updating the controller $scope in a watch on the global that holds theResolvedResult. (Using the workaround described at the end of this post). I tried a watch on the argument itself, but it never gets triggered.
$scope.$state=$state;
$scope.$watch('$state.$current.locals.globals.theResolvedResult', function (result) {
$scope.aValue = result.aValue;
});
Unfortunately, I guess because the watch is on a global, the watch is triggered when any route runs, and all other routes throw an error for every route except the one where the resolve is defined.
ionic.bundle.js:26794 TypeError: Cannot read property 'aValue' of undefined
at route_ctrl.js:234
How can I fix these errors, or is there a better way to do this?
Maybe just guard against the case where result is not defined:
$scope.$watch('$state.$current.locals.globals.theResolvedResult', function (result) {
if (result) {
$scope.aValue = result.aValue;
}
});
Breaking change for Angular V1.6
Move initialization logic into $onInit function
/* REPLACE
$scope.$state=$state;
$scope.$watch('$state.$current.locals.globals.theResolvedResult', function (result) {
$scope.aValue = result.aValue;
});
*/
//WITH
this.$onInit = function() {
$scope.aValue = $state.$current.locals.globals.theResolvedResult.aValue;
};
Initialization logic that relies on bindings being present should be put in the controller's $onInit() method, which is guaranteed to always be called after the bindings have been assigned.
— AngularJS Migration Guide (V1.6 $compile breaking change bcd0d4)
Erroneous rejection handler
The resolve is converting a rejected promise to a fulfilled promise:
/* ERRONEOUS
resolve: {theResolvedResult: function(theService){
return theService.theAsyncCall().then(function(result){
return result;
},function(errMsg){
//ERRONEOUS converts rejection
return errMsg;
});}
*/
// BETTER
resolve: {
theResolvedResult: function(theService){
return theService.theAsyncCall().then(function(result){
console.log(result);
return result;
},function catchHandler(errMsg){
console.log(errMsg);
//return errMsg;
//IMPORTANT re-throw error
throw errMsg;
});
}
It is important to re-throw the error in the catch handler. Otherwise the $q service converts the rejected promise to a fulfilled promise. This is similar to the way a try...catch statement works in vanilla JavaScript.
The $watch method take a function as its first argument:
/* HACKY
$scope.$state=$state;
$scope.$watch('$state.$current.locals.globals.theResolvedResult', function (result) {
$scope.aValue = result.aValue;
});
*/
//BETTER
//$scope.$state=$state;
//$scope.$watch('$state.$current.locals.globals.theResolvedResult',
$scope.$watch(
function () {
return $state.$current.locals.globals.theResolvedResult;
},
function (result) {
if (result) {
$scope.aValue = result.aValue;
};
});
When the first argument of the $watch method is a string, it evaluates it as an Angular Expression using $scope as its context. Since the $state service is not a property of $scope a function should be used instead.
The best bet is to avoid the $watch hack in the first place. Making sure that the value is good should happen in the Service, or at the very least in the resolve.
Check out this Plunker for a demo of how resolve should work.
The general idea is that resolve has one job; to make sure that the data is ready when we want it. The docs has a few examples on how ui.router handles resolve resolutions. They're a little limited, but generally show that "good" results are expected. Error handling is ultimately left up to your own choice.
Enjoy!
So, UI-router resolves were a thing of beauty in angular 1:
$stateProvider.state('myState', {
resolve:{
myVarFromResolve: function(){
return 'test';
}
}
})
controller: function($scope, myVar){
$scope.myVar= myVarFromResolve;
if(true){console.log($scope.myVar}; //logs 'test'
}
How do I do the same with an Angular 1.5. component (example below)?
export default function FooComponent() {
let fooComponent = {
restrict: 'E',
templateUrl: 'foo.html',
controller: controller,
controllerAs: 'foo',
bindToController: true,
};
return landingComponent;
}
And the resolve...
.state('fooState', {
parent: 'someParent',
url: '/fooUrl',
template: '<foo></foo>',
resolve:{
myVarFromResolve: function(){
return 'test';
}
}
})
I read a guide on how to do this, but I don't really understand it. Seems like the functionality isn't fully in place and that's a hack.
Looks like the answer is "Yes, kinda"...
I was given a work-around to this problem by a team member that works for angular-ui-router 0.2.18 (use npm list angular-ui-router --depth=0 to check your version quickly). It does work while using ES6 classes.
The work around is based on the fact that there is no (easy/known) way to get the variable passed into the controller, like you would in Angular 1 before component architecture introduced in 1.5. Instead, we create a service method that creates a promise (or returns an http call). Then you simply put the promise in the resolve. The promise method WILL return before the controller loads. So while you do not have a easy to use var, you have the data you need in a service.
resolve: {
preloadSomeVariableIntoAService: function (MyExampleService) {
return MyExampleService.presetSomeVariable();
}
//Note that 'preloadSomeVariableIntoAService' won't be used anywhere in our case
}
The service could be something like this:
export default class MyExampleService {
constructor( $q ) {
this.q = $q;
this.myVariableFromResolve = undefined;
}
presetStreamStatus(){
var deferred = this.q.defer();
return $http.get(someUrl).then(response) => {
this.myVariableFromResolve = response.data;
deferred.resolve(events);
});
return deferred;
};
};
You will then be able to get myVariableFromResolve from the service. I am not sure you have to use $q deferred, but it seems to be necessary since I am not simply returning the call but also setting the variable. Not sure. Hope this helps someone.
I am using UI Router Multiple Views concept.
The documentation says that i can have resolve on individual views. As you can see in my plunker, I have a resolve on my child view. The resolve returns a promise, which in my case, is resolved in the parent controller. I have simulated my problem with a setTimeOut. Basically, after 5 seconds, the child view should load (with an alert saying "In Child State") but it doesn't !!!
Relevant code is below but please instead refer to Plunker.
'child#parent': {
template: '<h4>This is Child State</h4>',
controller: childCtrl,
resolve: {
trd: function(tradeFactory) {
console.log('in resolve of child');
return tradeFactory.ready();
}
}
}
(Check updated and working plunker) The point here is:
all resolve must be already resolved, before the views (with its controllers) are rendered
And that is not true in your scenario. In fact, there is a race condition, ending up in the deadlock.
one view is waiting for a resolve (is provided with a promise)
other view's controller is ready to trigger resolve (make a promise resolved), but... it cannot be triggered, while waiting for its sibling
The original snippet
views: {
'': {
...
// inside of this, you expect to resolve the below promise
controller: parentCtrl
},
'child#parent': {
...
resolve: {
trd: function(tradeFactory) {
// this is a promise waiting for resolve
// implemented in parent controller ... deadlock
return tradeFactory.ready();
}
}
}
So, the above is not working. We have to do all inside of the resolve, e.g. this way:
views: {
'': {
...
controller: parentCtrl,
// here do the resolve
resolve: {
parentReady: function(tradeFactory){
setTimeout(function(){
tradeFactory.ResolveReady()
}, 1500);
}
}
},
'child#parent': {
...
resolve: {
trd: function(tradeFactory) {
console.log('in resolve of child');
return tradeFactory.ready();
}
}
}
There is updated plunker
So to summarise I am using angular-ui router resolve function to retrieve state specific data. However it doesn't seem to full wait for the promise to resolve:
state('parent.ChildState', {
url: '/myUrl?param1¶m1',
templateUrl: 'views/list.view.html',
controller: 'MyController',
resolve: {
data: resolveData
}
}).
function resolveData($stateParams, Utils) {
var filters = Utils.getFilters($stateParams);
DataService.myDataObj = DataService.get(filters, function(result, headers) {
DataService.myDataObj = result;
});
return DataService.myDataObj;
// Note I have also tried returning directly the DataService.get call however this makes all the below console log statements as undefined (see below for the controller code to know what I mean). So I do the assignment first and then return that.
}
Now in the controller I had a function that executes on load like so:
function ExpensesController(DataService) {
$scope.viewData = DataService;
initData();
function initData() {
// this generally logs a ngResource and shows the full data obj on console
console.log($scope.viewData.myDataObj);
// this gets undefined on console
console.log($scope.viewData.myDataObj.someField1);
// this log fine, however why do I need to do promise
// resolve? should be resolved already right?
$scope.viewData.myDataObj.$promise.then(function() {
console.log($scope.viewData.myDataObj.someField1);
});
As your required data to resolve is async, you need to return a promise and add return statement inside your callback function.
function resolveData($stateParams, Utils) {
var filters = Utils.getFilters($stateParams);
return DataService.get(filters, function(result, headers) {
DataService.myDataObj = result;
return DataService.myDataObj
});
}
You can read ui-router resolve docs more about how resolver works and when they should return promise or pure values.
I don;t know if I have got your problem :), but here is what I feel is wrong
1) in the resolve return a promise, it should not be resolved
function resolveData($stateParams, Utils) {
var filters = Utils.getFilters($stateParams);
return DataService.get(filters);
}
2) In the controller you should inject the data that is declared in resolve not the DataService so your controller should be
function ExpensesController(data) {
$scope.viewData = data;
}
I have an angularjs route with a resolve object with multiple properties like so:
.state('user', {
url: '/user/signup',
controller: 'CreateAccountCtrl',
templateUrl: 'createaccount.tpl.html',
resolve: {
practice: {
// Returns a promise
},
someOtherFunction: {
// Returns a promise, needs resolved object from practice
},
}
})
The problem is that I need the result from one of the resolves to process the other resolve. How can I implement this? Obviously I can just put all the http calls in one function and build a custom object but I am wondering if there's a more idiomatic solution.
Turn both properties of the resolve method into functions and pass one method by name to the other as an argument.
.state('user', {
url: '/user/signup',
controller: 'CreateAccountCtrl',
templateUrl: 'createaccount.tpl.html',
resolve: {
practice: function() {
// Returns a promise
},
someOtherFunction: function(practice) {
// Returns a promise, needs resolved object from practice
}
}
})
Also, this blog post is a great resource for using Angular UI Router. I learned this approach there.
Use an IIFE to return the resolve object:
...
resolve: (function () {
var practicePromise;
var someOtherPromise;
/* initialize the variables whichever way you want*/
return { /* this is the actual "resolve" object*/
practice: practicePromise,
someOther: someOtherPromise
};
} ()),
...