Answer by Gecko IT works for me, but I had to make some modifications in order to:
- Enable resource ajax call to be canceled multiple times without need to recreate resource
- Make resource backward compatible - This means that there is no need to change any application (Controller) code except resource factory
- Make code JSLint compliant
This is complete service factory implementation (you just need to put proper module name):
'use strict';
/**
* ResourceFactory creates cancelable resources.
* Work based on: https://mcmap.net/q/504739/-how-to-cancel-resource-requests
* which is based on: https://developer.rackspace.com/blog/cancelling-ajax-requests-in-angularjs-applications/
*/
/* global array */
angular.module('module_name').factory('ResourceFactory', ['$q', '$resource',
function($q, $resource) {
function abortablePromiseWrap(promise, deferred, outstanding) {
promise.then(function() {
deferred.resolve.apply(deferred, arguments);
});
promise.catch(function() {
deferred.reject.apply(deferred, arguments);
});
/**
* Remove from the outstanding array
* on abort when deferred is rejected
* and/or promise is resolved/rejected.
*/
deferred.promise.finally(function() {
array.remove(outstanding, deferred);
});
outstanding.push(deferred);
}
function createResource(url, options, actions) {
var resource;
var outstanding = [];
actions = actions || {};
Object.keys(actions).forEach(function(action) {
var canceller = $q.defer();
actions[action].timeout = canceller.promise;
actions[action].Canceller = canceller;
});
resource = $resource(url, options, actions);
Object.keys(actions).forEach(function(action) {
var method = resource[action];
resource[action] = function() {
var deferred = $q.defer(),
promise = method.apply(null, arguments).$promise;
abortablePromiseWrap(promise, deferred, outstanding);
return {
$promise: deferred.promise,
abort: function() {
deferred.reject('Aborted');
},
cancel: function() {
actions[action].Canceller.resolve('Call cancelled');
// Recreate canceler so that request can be executed again
var canceller = $q.defer();
actions[action].timeout = canceller.promise;
actions[action].Canceller = canceller;
}
};
};
});
/**
* Abort all the outstanding requests on
* this $resource. Calls promise.reject() on outstanding [].
*/
resource.abortAll = function() {
for (var i = 0; i < outstanding.length; i++) {
outstanding[i].reject('Aborted all');
}
outstanding = [];
};
return resource;
}
return {
createResource: function (url, options, actions) {
return createResource(url, options, actions);
}
};
}
]);
Usage is the same as in Gecko IT example. Service factory:
'use strict';
angular.module('module_name').factory('YourResourceServiceName', ['ResourceFactory', function(ResourceFactory) {
return ResourceFactory.createResource('some/api/path/:id', { id: '@id' }, {
create: {
method: 'POST'
},
update: {
method: 'PUT'
}
});
}]);
Usage in controller (backward compatible):
var result = YourResourceServiceName.create(data);
result.$promise.then(function success(data, responseHeaders) {
// Successfully obtained data
}, function error(httpResponse) {
if (httpResponse.status === 0 && httpResponse.data === null) {
// Request has been canceled
} else {
// Server error
}
});
result.cancel(); // Cancels XHR request
Alternative approach:
var result = YourResourceServiceName.create(data);
result.$promise.then(function success(data, responseHeaders) {
// Successfully obtained data
}).catch(function (httpResponse) {
if (httpResponse.status === 0 && httpResponse.data === null) {
// Request has been canceled
} else {
// Server error
}
});
result.cancel(); // Cancels XHR request
Further improvements:
- I don't like checking if request has been canceled. Better approach would be to attach attribute
httpResponse.isCanceled
when request is canceled, and similar for aborting.