Is it possible to pass a promise to a UI.Router $state from an outside controller (e.g. the controller that triggered the state)?
I know that $state.go() returns a promise; is it possible to override that with your own promise resolve this promise directly yourself or resolve it using a new promise?
Also, the documentation says the promise returned by $state.go() can be rejected with another promise (indicated by transition superseded), but I can't find anywhere that indicates how this can be done from within the state itself.
For example, in the code below, I would like to be able to wait for the user to click on a button ($scope.buttonClicked()) before continuing on to doSomethingElse().
I know that I can emit an event, but since promises are baked into Angular so deeply, I wondered if there was a way to do this through promise.resolve/promise.reject.
angular.module('APP', ['ui.router'])
.config(['$stateProvider', function ($stateProvider) {
$stateProvider
.state('myState', {
template: '<p>myState</p>',
controller: ['$state', '$scope', '$q', function ($state, $scope, $q) {
var deferred = $q.defer();
$scope.buttonClicked = function () {
deferred.resolve();
}
}]
});
}])
.controller('mainCtrl', ['$state', function ($state) {
$state.go('myState')
.then(doSomethingElse)
}]);
Update
I have accepted #blint's answer as it has got me closest to what I wanted. Below is some code that fleshes out this answer's idea a bit more. I don't think the way I have written this is a very elegant solution and I am happy if someone can suggest a better way to resolve promises from a triggered state.
The solution I've chosen is to chain your promises as you normally would in your controller, but leave a $scope.next() method (or something similar) attached to that scope that resolves/rejects the promise. Since the state can inherit the calling controller's scope, it will be able to invoke that method directly and thus resolve/reject the promise. Here is how it might work:
First, set up your states with buttons/controllers that call a $scope.next() method:
.config(function ($stateProvider) {
$stateProvider
.state('selectLanguage', {
template: '<p>Select language for app: \
<select ng-model="user.language" ng-options="language.label for language in languages">\
<option value="">Please select</option>\
</select>\
<button ng-click="next()">Next</button>\
</p>',
controller: function ($scope) {
$scope.languages = [
{label: 'Deutch', value: 'de'},
{label: 'English', value: 'en'},
{label: 'Français', value: 'fr'},
{label: 'Error', value: null}
];
}
})
.state('getUserInfo', {
template: '<p>Name: <input ng-model="user.name" /><br />\
Email: <input ng-model="user.email" /><br />\
<button ng-click="next()">Next</button>\
</p>'
})
.state('mainMenu', {
template: '<p>The main menu for {{user.name}} is in {{user.language.label}}</p>'
})
.state('error', {
template: '<p>There was an error</p>'
});
})
Next, you set up your controller. In this case, I'm using a local service method, user.loadFromLocalStorage() to get the ball rolling (it returns a promise), but any promise will do. In this workflow, if the $scope.user is missing anything, it will progressively get populated using states. If it is fully populated, it skips right to the main menu. If elements are left empty or are in an invalid state, you get taken to an error view.
.controller('mainCtrl', function ($scope, $state, $q, User) {
$scope.user = new User();
$scope.user.loadFromLocalStorage()
.then(function () {
var deferred;
if ($scope.user.language === null) {
deferred = $q.defer();
$state.go('selectLanguage');
$scope.next = function () {
$scope.next = undefined;
if ($scope.user.language === null) {
return deferred.reject('Language not selected somehow');
}
deferred.resolve();
};
return deferred.promise;
}
})
.then(function () {
var deferred;
if ($scope.user.name === null || $scope.user.email === null) {
deferred = $q.defer();
$state.go('getUserInfo');
$scope.next = function () {
$scope.next = undefined;
if ($scope.user.name === null || $scope.user.email === null) {
return deferred.reject('Could not get user name or email');
}
deferred.resolve();
};
return deferred.promise;
}
})
.then(function () {
$state.go('mainMenu');
})
.catch(function (err) {
$state.go('error', err);
});
});
This is pretty verbose and not yet very DRY, but it shows the overall intention of asynchronous flow control using promises.
The purpose of promises is to guarantee a result... or handle a failure. Promises can be chained, returned in functions and thus extended.
You would have no interest in "overriding" a promise. What you can do, however:
Handle the failure case. Here's the example from the docs:
promiseB = promiseA.then(function(result) {
// success: do something and resolve promiseB
// with the old or a new result
return result;
}, function(reason) {
// error: handle the error if possible and
// resolve promiseB with newPromiseOrValue,
// otherwise forward the rejection to promiseB
if (canHandle(reason)) {
// handle the error and recover
return newPromiseOrValue;
}
return $q.reject(reason);
});
Append a new asynchronous operation in the promise chain. You can combine promises. If a method called in the chain returns a promise, the parent promised will wall the rest of the chain once the new promise is resolved.
Here's the pattern you might be looking for:
angular.module('APP', ['ui.router'])
.config(['$stateProvider', function ($stateProvider) {
$stateProvider
.state('myState', {
template: '<p>myState</p>',
controller: 'myCtrl'
});
}])
.controller('myCtrl', ['$scope', '$state', '$q', '$http', 'someAsyncServiceWithCallback',
function ($scope, $state, $q, $http, myService) {
$scope.buttonClicked = function () {
$state.go('myState')
.then(function () {
// You can return a promise...
// From a method that returns a promise
// return $http.get('/myURL');
// Or from an old-school method taking a callback:
var deferred = $q.defer();
myService(function(data) {
deferred.resolve(data);
});
return deferred.promise;
},
function () {
console.log("$state.go() failed :(");
});
};
}]);
Perhaps one way of achieving this would be to return your promise from the state's resolve
resolve: {
myResolve: function($scope, $q) {
var deferred = $q.defer();
$scope.buttonClicked = function () {
deferred.resolve();
}
return deferred.promise;
}
}
There is also an example in resolve docs that may be of interest
// Another promise example. If you need to do some
// processing of the result, use .then, and your
// promise is chained in for free. This is another
// typical use case of resolve.
promiseObj2: function($http){
return $http({method: 'GET', url: '/someUrl'})
.then (function (data) {
return doSomeStuffFirst(data);
});
},
Related
Right now i am trying to make an Angular JS install application, to install a CMS. So i am trying to block access to a state (ui router), i am doing it with a resolve function. But the problem is, that i make a get request to an API, which returns true or false, and the resolve function do not wait for the get request to complete, so it just loads the state.
Here is my code:
app.run(['$rootScope', '$http', function($rootScope, $http) {
$rootScope.$on('$stateChangeStart', function() {
$http.get('/api/v1/getSetupStatus').success(function(res) {
$rootScope.setupdb = res.db_setup;
$rootScope.setupuser = res.user_setup;
});
});
}]);
app.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("/404");
$stateProvider.state('db-install', {
url: "/install/db",
templateUrl: 'admin/js/partials/db-install.html',
controller: 'DBController',
resolve: {
data: function($q, $state, $timeout, $rootScope) {
var setupStatus = $rootScope.setupdb;
var deferred = $q.defer();
$timeout(function() {
if (setupStatus === true) {
$state.go('setup-done');
deferred.reject();
} else {
deferred.resolve();
}
});
return deferred.promise;
}
}
})
.state('user-registration', {
url: "/install/user-registration",
templateUrl: "admin/js/partials/user-registration.html",
controller: "RegisterController"
})
.state('setup-done', {
url: "/install/setup-done",
templateUrl: "admin/js/partials/setup-done.html"
})
.state('404', {
url: "/404",
templateUrl: "admin/js/partials/404.html"
});
}]);
Here you can see a timeline for the loading of the page:
Here you can see what the API returns:
Your db-install resolver function needs to chain from the $http.get for install status.
$stateProvider.state('db-install', {
url: "/install/db",
templateUrl: 'admin/js/partials/db-install.html',
controller: 'DBController',
resolve: {
data: function($state, $http) {
return $http.get('/api/v1/getSetupStatus'
).then (function(result) {
var setupdb = result.data.db_setup;
var user_setup = result.data.user_setup;
//return for chaining
return setupdb;
}).then (function (setupStatus) {
//use chained value
if (setupStatus === true {
//chain with $state.go promise
return $state.go('setup-done');
} else {
//resolve promise chain
return 'setup-not-done';
};
})
}
}
})
By returning and chaining from the status $http.get, the resolver function waits before executing (or not executing) the $state.go.
For more information on chaining promises, see the AngularJS $q Service API Reference -- chaining promises.
The call to getSetupStatus gets executed in the $stateChangeStart so resolve is not aware that it has to wait. You can put the $http call inside of the resolve function, like this:
function($q, $state, $timeout) {
return $http.get('/api/v1/getSetupStatus')
.then(function(res) {
if(res.db_setup) {
$state.go('setup-done');
}
else {
return true;
}
});
}
By making the resolve parameter return a callback the state will load after the promise is resolved.
I am using AngularJS to call an http service that returns some opening times in an object. I don't understand why, in my controller, the console.log is printed 4 times, instead of one time. Can anyone explain this to me?
Here is my service/factory code:
myApp.factory('BookingFactory', ['$http', '$q', function($http, $q) {
var deferredTime = $q.defer();
return {
GetDealerLocationTimeList: function(websiteId) {
return $http.get('/d/GetDealerLocationTimes?website_id=' + websiteId)
.then(function(response) {
deferredTime.resolve(response.data);
dealerLocationTimeList.push(response.data);
return deferredTime.promise;
}, function(error) {
deferredTime.reject(response);
return deferredTime.promise;
});
}
}
}]);
Here is my controller code that is calling the service:
var promise = BookingFactory.GetDealerLocationTimeList(website_id);
promise.then(
function(da) {
$scope.dealerLocationTimeList = da;
console.log($scope.dealerLocationTimeList);
},
function(error) {
$log.error('failure loading dealer associates', error);
}
);
There are many mistakes in this code ><
If you want to use deferred, then this should be the code:
myApp.factory('BookingFactory', ['$http', '$q', function($http, $q) {
return {
GetDealerLocationTimeList: function(websiteId) {
var deferredTime = $q.defer(); // deferred should be created each time when a function is called. It can only be consumed (resolved/rejected) once.
/* return - don't need to return when you already creating a new deferred*/
$http.get('/d/GetDealerLocationTimes?website_id=' + websiteId)
.then(function(response) {
deferredTime.resolve(response.data);
// dealerLocationTimeList.push(response.data);
}, function(error) {
deferredTime.reject(error); // it should be 'error' here because your function argument name says so...
});
return deferredTime.promise; // promise is returned as soon as after you call the function, not when the function returns
}
}
}]);
But it is a better practice to return the promise if your inner function is a promise itself (like $http.get)
myApp.factory('BookingFactory', ['$http', '$q', function($http, $q) {
return {
GetDealerLocationTimeList: function(websiteId) {
// no need to create new deferred anymore because we are returning the promise in $http.get
return $http.get('/d/GetDealerLocationTimes?website_id=' + websiteId)
.then(function(response) {
// dealerLocationTimeList.push(response.data);
return response.data; // return the data for the resolve part will make it available when the outer promise resolve
}/* this whole part should be omitted if we are not doing any processing to error before returning it (thanks #Bergi)
, function(error) {
return $q.reject(error); // use $q.reject to make this available in the reject handler of outer promise
}*/);
// no need to return promise here anymore
}
}
}]);
You can see I've also commented your dealerLocationTimeList.push(response.data). In this case you should push the data into your scope variable on the outer layer (in the promise.then), because dealerLocationTimeList is not available in the factory.
promise.then(
function(da) {
// you might want to do an isArray check here, or make sure it is an array all the time
$scope.dealerLocationTimeList.push(da);
console.log($scope.dealerLocationTimeList);
},
...
);
I'm getting crazy with this since a couple of hours.
I have an angular service factory to get addresses from my API:
App.factory('storesService', ['$http', '$q', 'endpoint', function ($http, $q, endpoint) {
var deferred = $q.defer();
return {
addresses: function (store_id) {
$http.get(endpoint.store.addresses, {
params: {
id: store_id
}
})
.success(function (data) {
console.log('Data from API:' + data);
deferred.resolve(data);
})
.error(function () {
deferred.reject();
});
return deferred.promise;
}
};
}]);
This service is used in my controller to get addresses of a specific store:
$scope.loadAddresses = function (store_id) {
var load = storesService.addresses(store_id);
load.then(function (data) {
console.log('Deferred data:' + data);
$scope.addresses = data.addresses;
});
};
In my view I have the ng-init="loadAddresses(store_id)", store_id is a right value.
I'm also using angular-xeditable (select-local) to manage my store selection.
I added onaftersave='storeChanged(store.id)' in my view to get the store id selected by the user and it return correctly the new id.
my storeChanged function is very easy, it basically run a new request to the API:
$scope.storeChanged = function (store_id) {
$scope.loadAddresses(store_id);
};
What happen:
At the beginning, with ng-init I see correctly the console.log, first the one from the service and then the one from the controller.
Once I select another store from my select I first see the console.log from the controller and then the one from the service.
Basically the data in the controller is not updated and I can not understand why it happen...
You defined your deferred globally in the service, so there is only one global promise. Because a promise can only be resolved or rejected once, it will stay resolved/rejected forever after your first http call. To fix simply move the line var deferred = $q.defer(); into your service function:
App.factory('storesService', ['$http', '$q', 'endpoint', function ($http, $q, endpoint) {
return {
addresses: function (store_id) {
var deferred = $q.defer();
$http.get(endpoint.store.addresses, {
params: {
id: store_id
}
})
.success(function (data) {
console.log('Data from API:' + data);
deferred.resolve(data);
})
.error(function () {
deferred.reject();
});
return deferred.promise;
}
};
}]);
You've created one defer for potentially many requests. The first time you make a request it will work, but after than it will instantly return, as the one promise you've set up has already resolved. Patterns smilar to this can be very useful for caching, actually.
$http already returns a promise. You don't need to go out of your way to use $q
App.factory('storesService', ['$http', 'endpoint', function ($http, endpoint) {
return {
addresses: function (store_id) {
return $http.get(endpoint.store.addresses, {
params: {
id: store_id
}
}).then(function(response){
//Chain an extra promise here to clean up the response to just return the data.
return response.data;
})
}
};
}]);
You're trying to re-resolve a promise, which you can't do. You only create one deferred that every request uses. That should be inside of the addresses function so that a new one is created for each request, but you don't need it anyway because $http creates and returns a promise already. You need to return the promise from $http rather than creating a new one. See this post for a better understanding: What is the explicit promise construction antipattern and how do I avoid it?
addresses: function (store_id) {
return $http.get(endpoint.store.addresses, {
params: {
id: store_id
}
}).then(function(resp) {
console.log('Data from API:' + resp.data);
return resp.data;
});
}
I would like to create a chained promise for my service provider:
this.$get = function($q, $window, $rootScope) {
var $facebook=$q.defer();
$rootScope.$on("fb.load", function(e, FB) {
$facebook.resolve(FB);
});
$facebook.api = function () {
var args=arguments;
args[args.length++] = function(response) {
$facebook.resolve(response);
};
$facebook.promise.then(function(FB) {
FB.api.apply(FB, args);
});
return $facebook.promise;
};
return $facebook;
};
Than I call to the promise: $scope.user=$facebook.api("/me");
The problem is that because the deferred was already resolved its not wait until the api method will resolve it..
How can I chain them in a way the last promise will wait until the last promise will resolved?
It seems like you need two separate promise objects:
One for the fb.load event and another one for the result of the API call.
Try chaning your code to read -
this.$get = function($q, $window, $rootScope) {
var apiLoaded=$q.defer();
$rootScope.$on("fb.load", function(e, FB) {
apiLoaded.resolve(FB);
});
// You should reject the promise if facebook load fails.
$facebook.api = function () {
var resultDefer = $q.defer(),
args=arguments;
args[args.length++] = function(response) {
$rootScope.$apply(function() {
resultDefer.resolve(response);
// you should reject if the response is an error
});
};
return apiLoaded.promise.then(function(FB) {
FB.api.apply(FB, args);
return resultDefer.promise;
});
};
return $facebook;
};
Also note that whenever you call resolve() from non-angularish code, you will need to wrap it with $rootScope.$apply(), otherwise then promise 'then' handlers will not get executed. Good luck!
I am trying to use resolve in a $routeProvider to display the new route only when a $http request is finished. If the request is successful, the promise resulting from the $http.post() is resolved and the view is rendered. But if the request fails (timeout or internal error for eg.), the promise is never resolved and the the view is never rendered. How can I deal with request failure using resolve ?
The most important parts of the code is bellow :
app.js
$routeProvider.when('/warrantyResult', {
templateUrl : 'partials/warranty-result.html',
controller : 'WarrantyResultCtrl',
resolve : {
response : [ 'Warranty', function(Warranty) {
return Warranty.sendRequest();
} ]
}
});
controllers.js
angular.module('adocDemo.controllers', []).controller('HomeCtrl', [ '$scope', function($scope) {
} ]).controller('WarrantyCtrl', [ '$scope', '$http', '$location', 'Warranty', function($scope, $http, $location, Warranty) {
$scope.submitWarranty = function() {
$scope.loading = true;
Warranty.setRequestData($scope.data);
$location.path('/warrantyResult');
};
} ]).controller('WarrantyResultCtrl', [ '$scope', 'Warranty', function($scope, Warranty) {
$scope.request = Warranty.getRequestData();
$scope.response = Warranty.getResponseData();
} ]);
services.js
angular.module('adocDemo.services', []).factory('Warranty', [ '$http', '$timeout', function($http, $timeout) {
/**
* This service is used to query the Warranty Webmethod. The sendRequest
* method is automaticcaly called when the user is redirected to
* /warrantyResult route.
*/
var isDataSet = false;
var requestData = undefined;
var responseData = undefined;
return {
setRequestData : function(data) {
//Setting the data
isDataSet = true;
},
getRequestData : function() {
return requestData;
},
sendRequest : function(data) {
if(isDataSet) {
var request = $http.post('url/to/webservice', requestData);
request.success(function(data) {
responseData = data;
});
return request;
}
},
getResponseData : function() {
return responseData;
}
};
} ]);
I know i could use a promise around the $http call and resolve it even if the request is a failure, but I'm wondering if there is a simpler solution.
Thanks for reading and, hopefully, helping :)
I think the only way to do it from resolve is to manually resolve the promise returned by Warranty.sendRequest and rewrap it in a new promise:
resolve : {
response : [ 'Warranty' '$q', function(Warranty, $q) {
var dfd = $q.defer();
Warranty.sendRequest().then(function(result) {
dfd.resolve({ success: true, result : result });
}, function(error) {
dfd.resolve({ success : false, reason : error });
});
return dfd.promise;
} ]
}
In WarrantyResultCtrl, you could check if an error occurred and generate a redirect.
EDIT: much cleaner solution:
// WarrantyCtrl
$scope.$on('$routeChangeError', function() {
// handle the error
});
$scope.submitWarranty = function() {
$scope.loading = true;
Warranty.setRequestData($scope.data);
$location.path('/warrantyResult');
};
(plunker demo)
What I have found is that the controller is not fired at all if the Promise is rejected, and your view is never rendered--same as you.
What I also discovered is that if you handle the Promise with a .then() in your $routeProvider's resolve, the .then() will return a new Promise that is resolved and your controller is fired after all, albeit without the data you are expecting.
For example:
$routeProvider.when('/warrantyResult', {
templateUrl : 'partials/warranty-result.html',
controller : 'WarrantyResultCtrl',
resolve : {
response : [ 'Warranty', function(Warranty) {
return Warranty.sendRequest()
.then(null, function(errorData) {
// Log an error here?
// Or do something with the error data?
});
}]
}
});
Now in your controller you will want to check whether response is undefined. If it is undefined then you'll know that the call to Warranty.sendRequest() failed, and you can act accordingly.
For what it's worth, I did not go this route. Instead, I injected the $location service into the resolve handler and redirected to an error page if the $http call gets rejected.
UPDATE
Just noticed that in your controller you are injecting the Warranty service when you should instead be injecting the response that you defined in your resolve. That will prevent your view from rendering until the Promise returned from Warranty.sendRequest() is resolved.
After deep searching, I could not find a solution for this problem.
I decided to drop the resolve statment in my route definition and I use the following snippet of code in the WarrantyCtrl.
$scope.submitWarranty = function() {
$scope.formatUserData();
if ($scope.verifyUserData()) {
$scope.loading = true;
Warranty.setRequestData($scope.data);
Warranty.sendRequest().success(function() {
$location.path('/warrantyResult');
}).error(function() {
$location.path('/webserviceError');
});
}
};
Not very clever, but works as intented ... If someone still have the solution for the original problem, I would be very pleased to read it !