I have a controller that performs a http request.
This request can take anywhere between 2 seconds to 4 minutes to return some data .
I have added a button, that users should click to cancel the request if searches take too long to complete.
Controller:
$scope.search = function() {
myFactory.getResults()
.then(function(data) {
// some logic
}, function(error) {
// some logic
});
}
Service:
var myFactory = function($http, $q) {
return {
getResults: function(data) {
var deffered = $q.dafer();
var content = $http.get('someURL', {
data: {},
responseType: json
)}
deffered.resolve(content);
returned deffered.promise;
}
}
}
Button click:
$scope.cancelGetResults = function() {
// some code to cancel myFactory.getResults() promise
}
How can I implement a button click to cancel the myFactory.getResults() promise?
The question uses deferred antipattern which usually should be avoided, but it fits the cancellation case:
getResults: function(data) {
var deffered = $q.defer();
$http.get('someURL', {
data: {},
responseType: json
}).then(deffered.resolve, deferred.reject);
deffered.promise.cancel = function () {
deferred.reject('CANCELLED')
};
returned deffered.promise;
}
getResult is a service in which we are implementing cancellation.
getResult = function(){
var deferred = $q.defer();
$http.get(url).success(function(result){
deffered.resolve(result);
}).error(function(){
deffered.reject('Error is occured!');
});
return deferred.promise;
};
where url variable is used in place of any Restful API url. You can use it with given code.
getResult().then(function (result) { console.log(result); };
You could use .resolve() method which should be available.
Pass the promise in the controller to the variable.
Create e.g. cancel method which takes the promise as an argument in you factory. Then call this method in the cancelGetResults() function in the controller.
In the cancel method you just call .resolve on the passed promise.
This should actually do.
https://www.bennadel.com/blog/2731-canceling-a-promise-in-angularjs.htm
Related
I'm fairly new to AngularJS and have just begun to grasp many of the concepts I especially like the MVC design pattern. But I am having a difficult time implementing the Service layer in my application.
What I am finding is that after my Controller calls a method within the service, it continues with code execution before the service returns the data; so that by the time the service does return the data, it isn't of any use to the controller.
To give a better example of what I'm saying, here is the code:
var InsightApp = angular.module('InsightApp', ['chart.js']);
// Module declaration with factory containing the service
InsightApp.factory("DataService", function ($http) {
return {
GetChartSelections: function () {
return $http.get('Home/GetSalesData')
.then(function (result) {
return result.data;
});
}
};
});
InsightApp.controller("ChartSelectionController", GetAvailableCharts);
// 2nd Controller calls the Service
InsightApp.controller("DataController", function($scope, $http, DataService){
var response = DataService.GetChartSelections();
// This method is executed before the service returns the data
function workWithData(response){
// Do Something with Data
}
}
All the examples I've found seem to be constructed as I have here or some slight variation; so I am not certain what I should be doing to ensure asynchronous execution.
In the Javascript world, I'd move the service to the inside of the Controller to make certain it executes async; but I don't how to make that happen in Angular. Also, it would be counter intuitive against the angular injection to do that anyway.
So what is the proper way to do this?
http return a promise not the data, so in your factory your returning the $http promise and can use it like a promise with then, catch, finally method.
see: http://blog.ninja-squad.com/2015/05/28/angularjs-promises/
InsightApp.controller("DataController", function($scope, $http, DataService){
var response = DataService.GetChartSelections()
.then(function(res) {
// execute when you have the data
})
.catch(function(err) {
// execute if you have an error in your http call
});
EDIT pass params to model service:
InsightApp.factory("DataService", function ($http) {
return {
GetChartSelections: function (yourParameter) {
console.log(yourParameter);
return $http.get('Home/GetSalesData')
.then(function (result) {
return result.data;
});
}
};
});
and then :
InsightApp.controller("DataController", function($scope, $http, DataService){
var response = DataService.GetChartSelections('only pie one')
.then(function(res) {
// execute when you have the data
})
.catch(function(err) {
// execute if you have an error in your http call
});
You should proceed like this :
DataService.GetChartSelections().then(function (data) {
workWithData(data);
}
Actually $http.get returns a Promise. You can call the method then to handle the success or failure of your Promise
Should it not be like this, when your $http returns a promise or you pass a callback.
With passing callback as a param.
InsightApp.factory("DataService", function ($http) {
return {
GetChartSelections: function (workWithData) {
return $http.get('Home/GetSalesData')
.then(function (result) {
workWithData(result.data);
});
}
};
});
Controller code:
InsightApp.controller("DataController", function($scope, $http, DataService){
var response = DataService.GetChartSelections(workWithData);
// This method is executed before the service returns the data
function workWithData(response){
// Do Something with Data
}
}
Or use then or success:
var response = DataService.GetChartSelections().then(function(res){
//you have your response here... which you can pass to workWithData
});
Return the promise to the controller, dont resolve it in the factory
var InsightApp = angular.module('InsightApp', ['chart.js']);
// Module declaration with factory containing the service
InsightApp.factory("DataService", function ($http) {
return {
GetChartSelections: function () {
return $http.get('Home/GetSalesData');
}
};
});
In the controller,
var successCallBk =function (response){
// Do Something with Data
};
var errorCallBK =function (response){
// Error Module
};
var response = DataService.GetChartSelections().then(successCallBk,errorCallBK);
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 unit test a function in an Angular controller that relies on an asynchronous API call (a promise).
I have a function in a controller that calls a service and sets a $scope variable in the controller based on the result of the service:
$scope.display = 'loading';
$scope.loadData = function() {
ApiService.getSummary().then(
function (response) {
$scope.displayData(response.data);
$scope.display = 'boxes';
},
function (error) {
$scope.display = 'error';
}
);
};
I have the following Jasmine unit test
var FunctionFixtures = {
successFunc: function() {
var deferred = $q.defer();
var response = {
'data': dataVar
};
deferred.resolve(response);
return deferred.promise;
},
errorFunc: function() {
var deferred = $q.defer();
deferred.reject();
return deferred.promise;
}
};
it('should show the boxes on data load success', function(){
spyOn(ApiService, 'getSummary').and.callFake(FunctionFixtures.successFunc);
$scope.loadData();
expect($scope.display).toBe('boxes');
});
When I run the test, I get the following error:
Error: Expected 'loading' to be 'boxes'.
I essentially want to fake the api call and return either a success or a failure and ensure that $scope.display is set to the appropriate string i.e. 'boxes' for success and 'error' failure. Any thoughts on how to do this?
It looks like you're missing a call to $rootScope.apply() after the call to loadData() but before the call to expect.
Because of how Angular promises are integrated with the $rootScope, you need to run a digest cycle for resolved promises to call their callbacks, as shown at https://code.angularjs.org/1.2.26/docs/api/ng/service/$q#testing.
Given a Ajax request in AngularJS
$http.get("/backend/").success(callback);
what is the most effective way to cancel that request if another request is launched (same backend, different parameters for instance).
This feature was added to the 1.1.5 release via a timeout parameter:
var canceler = $q.defer();
$http.get('/someUrl', {timeout: canceler.promise}).success(successCallback);
// later...
canceler.resolve(); // Aborts the $http request if it isn't finished.
Cancelling Angular $http Ajax with the timeout property doesn't work in Angular 1.3.15.
For those that cannot wait for this to be fixed I'm sharing a jQuery Ajax solution wrapped in Angular.
The solution involves two services:
HttpService (a wrapper around the jQuery Ajax function);
PendingRequestsService (tracks the pending/open Ajax requests)
Here goes the PendingRequestsService service:
(function (angular) {
'use strict';
var app = angular.module('app');
app.service('PendingRequestsService', ["$log", function ($log) {
var $this = this;
var pending = [];
$this.add = function (request) {
pending.push(request);
};
$this.remove = function (request) {
pending = _.filter(pending, function (p) {
return p.url !== request;
});
};
$this.cancelAll = function () {
angular.forEach(pending, function (p) {
p.xhr.abort();
p.deferred.reject();
});
pending.length = 0;
};
}]);})(window.angular);
The HttpService service:
(function (angular) {
'use strict';
var app = angular.module('app');
app.service('HttpService', ['$http', '$q', "$log", 'PendingRequestsService', function ($http, $q, $log, pendingRequests) {
this.post = function (url, params) {
var deferred = $q.defer();
var xhr = $.ASI.callMethod({
url: url,
data: params,
error: function() {
$log.log("ajax error");
}
});
pendingRequests.add({
url: url,
xhr: xhr,
deferred: deferred
});
xhr.done(function (data, textStatus, jqXhr) {
deferred.resolve(data);
})
.fail(function (jqXhr, textStatus, errorThrown) {
deferred.reject(errorThrown);
}).always(function (dataOrjqXhr, textStatus, jqXhrErrorThrown) {
//Once a request has failed or succeeded, remove it from the pending list
pendingRequests.remove(url);
});
return deferred.promise;
}
}]);
})(window.angular);
Later in your service when you are loading data you would use the HttpService instead of $http:
(function (angular) {
angular.module('app').service('dataService', ["HttpService", function (httpService) {
this.getResources = function (params) {
return httpService.post('/serverMethod', { param: params });
};
}]);
})(window.angular);
Later in your code you would like to load the data:
(function (angular) {
var app = angular.module('app');
app.controller('YourController', ["DataService", "PendingRequestsService", function (httpService, pendingRequestsService) {
dataService
.getResources(params)
.then(function (data) {
// do stuff
});
...
// later that day cancel requests
pendingRequestsService.cancelAll();
}]);
})(window.angular);
Cancelation of requests issued with $http is not supported with the current version of AngularJS. There is a pull request opened to add this capability but this PR wasn't reviewed yet so it is not clear if its going to make it into AngularJS core.
If you want to cancel pending requests on stateChangeStart with ui-router, you can use something like this:
// in service
var deferred = $q.defer();
var scope = this;
$http.get(URL, {timeout : deferred.promise, cancel : deferred}).success(function(data){
//do something
deferred.resolve(dataUsage);
}).error(function(){
deferred.reject();
});
return deferred.promise;
// in UIrouter config
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
//To cancel pending request when change state
angular.forEach($http.pendingRequests, function(request) {
if (request.cancel && request.timeout) {
request.cancel.resolve();
}
});
});
For some reason config.timeout doesn't work for me. I used this approach:
let cancelRequest = $q.defer();
let cancelPromise = cancelRequest.promise;
let httpPromise = $http.get(...);
$q.race({ cancelPromise, httpPromise })
.then(function (result) {
...
});
And cancelRequest.resolve() to cancel. Actually it doesn't not cancel a request but you don't get unnecessary response at least.
Hope this helps.
This enhances the accepted answer by decorating the $http service with an abort method as follows ...
'use strict';
angular.module('admin')
.config(["$provide", function ($provide) {
$provide.decorator('$http', ["$delegate", "$q", function ($delegate, $q) {
var getFn = $delegate.get;
var cancelerMap = {};
function getCancelerKey(method, url) {
var formattedMethod = method.toLowerCase();
var formattedUrl = encodeURI(url).toLowerCase().split("?")[0];
return formattedMethod + "~" + formattedUrl;
}
$delegate.get = function () {
var cancelerKey, canceler, method;
var args = [].slice.call(arguments);
var url = args[0];
var config = args[1] || {};
if (config.timeout == null) {
method = "GET";
cancelerKey = getCancelerKey(method, url);
canceler = $q.defer();
cancelerMap[cancelerKey] = canceler;
config.timeout = canceler.promise;
args[1] = config;
}
return getFn.apply(null, args);
};
$delegate.abort = function (request) {
console.log("aborting");
var cancelerKey, canceler;
cancelerKey = getCancelerKey(request.method, request.url);
canceler = cancelerMap[cancelerKey];
if (canceler != null) {
console.log("aborting", cancelerKey);
if (request.timeout != null && typeof request.timeout !== "number") {
canceler.resolve();
delete cancelerMap[cancelerKey];
}
}
};
return $delegate;
}]);
}]);
WHAT IS THIS CODE DOING?
To cancel a request a "promise" timeout must be set.
If no timeout is set on the HTTP request then the code adds a "promise" timeout.
(If a timeout is set already then nothing is changed).
However, to resolve the promise we need a handle on the "deferred".
We thus use a map so we can retrieve the "deferred" later.
When we call the abort method, the "deferred" is retrieved from the map and then we call the resolve method to cancel the http request.
Hope this helps someone.
LIMITATIONS
Currently this only works for $http.get but you can add code for $http.post and so on
HOW TO USE ...
You can then use it, for example, on state change, as follows ...
rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
angular.forEach($http.pendingRequests, function (request) {
$http.abort(request);
});
});
here is a version that handles multiple requests, also checks for cancelled status in callback to suppress errors in error block. (in Typescript)
controller level:
requests = new Map<string, ng.IDeferred<{}>>();
in my http get:
getSomething(): void {
let url = '/api/someaction';
this.cancel(url); // cancel if this url is in progress
var req = this.$q.defer();
this.requests.set(url, req);
let config: ng.IRequestShortcutConfig = {
params: { id: someId}
, timeout: req.promise // <--- promise to trigger cancellation
};
this.$http.post(url, this.getPayload(), config).then(
promiseValue => this.updateEditor(promiseValue.data as IEditor),
reason => {
// if legitimate exception, show error in UI
if (!this.isCancelled(req)) {
this.showError(url, reason)
}
},
).finally(() => { });
}
helper methods
cancel(url: string) {
this.requests.forEach((req,key) => {
if (key == url)
req.resolve('cancelled');
});
this.requests.delete(url);
}
isCancelled(req: ng.IDeferred<{}>) {
var p = req.promise as any; // as any because typings are missing $$state
return p.$$state && p.$$state.value == 'cancelled';
}
now looking at the network tab, i see that it works beatuifully. i called the method 4 times and only the last one went through.
You can add a custom function to the $http service using a "decorator" that would add the abort() function to your promises.
Here's some working code:
app.config(function($provide) {
$provide.decorator('$http', function $logDecorator($delegate, $q) {
$delegate.with_abort = function(options) {
let abort_defer = $q.defer();
let new_options = angular.copy(options);
new_options.timeout = abort_defer.promise;
let do_throw_error = false;
let http_promise = $delegate(new_options).then(
response => response,
error => {
if(do_throw_error) return $q.reject(error);
return $q(() => null); // prevent promise chain propagation
});
let real_then = http_promise.then;
let then_function = function () {
return mod_promise(real_then.apply(this, arguments));
};
function mod_promise(promise) {
promise.then = then_function;
promise.abort = (do_throw_error_param = false) => {
do_throw_error = do_throw_error_param;
abort_defer.resolve();
};
return promise;
}
return mod_promise(http_promise);
}
return $delegate;
});
});
This code uses angularjs's decorator functionality to add a with_abort() function to the $http service.
with_abort() uses $http timeout option that allows you to abort an http request.
The returned promise is modified to include an abort() function. It also has code to make sure that the abort() works even if you chain promises.
Here is an example of how you would use it:
// your original code
$http({ method: 'GET', url: '/names' }).then(names => {
do_something(names));
});
// new code with ability to abort
var promise = $http.with_abort({ method: 'GET', url: '/names' }).then(
function(names) {
do_something(names));
});
promise.abort(); // if you want to abort
By default when you call abort() the request gets canceled and none of the promise handlers run.
If you want your error handlers to be called pass true to abort(true).
In your error handler you can check if the "error" was due to an "abort" by checking the xhrStatus property. Here's an example:
var promise = $http.with_abort({ method: 'GET', url: '/names' }).then(
function(names) {
do_something(names));
},
function(error) {
if (er.xhrStatus === "abort") return;
});
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!