I have a sails, node-js application in angular-js and I decided to make some tests for it, especifycally in the backend part, for which I am using Jasmine and ngMockE2E tools, because I want to test it with some real server side data.
Here is a part of the code I want to test:
app.controller('IdentificationCtrl', function($scope, $rootScope, ... , ajax) {
_initController = function() {
$scope.loginData = {};
};
$scope.doLogin = function(form) {
if (form.$valid) {
ajax.sendApiRequest($scope.loginData, "POST", "session/login").then(
function(response) {
//$state.go('app.dashboard');
window.localStorage.setItem("sesion", JSON.stringify(response.data));
$rootScope.userTest = response.data;
},
(function(error) {
console.log("error")
})
);
}
};
_initController();
});
Here is my service.js file, in which I provide the ajax service:
angular.module('common.services', [])
.service('ajax', function($http, $rootScope) {
if (window.location.hostname == "localhost") {
var URL = "http://localhost:1349/";
} else {
var URL = "http://TheRealURL/";
}
this.sendApiRequest = function(data, type, method) {
$rootScope.$broadcast('loading:show')
if (method == "session/login" || method == "session/signup") {
var authorization = "";
} else {
var authorization = JSON.parse(window.localStorage["sesion"]).id;
}
data_ajax = {
url: URL + method,
method: type,
headers: {
'Content-Type': 'text/plain',
'authorization': authorization
}
}
if (type === "GET" || type != "delete") {
data_ajax.params = data;
} else {
data_ajax.data = data;
}
if (window.localStorage['admin-language']) {
data_ajax.headers['accept-language'] = window.localStorage['admin-language'];
} else {
data_ajax.headers['accept-language'] = window.navigator.language.toUpperCase();
}
//The test arrives here perfectly
return $http(data_ajax).success(function(data, status, headers, config) {
//But does not enter here
return data;
$rootScope.$broadcast('loading:hide')
}).error(function(data, status, headers, config) {
//Nor here
return data;
$rootScope.$broadcast('loading:hide')
});
//And finally achieves this point, but without making the http call
}
})
Here is the html where I load Jasmine, ngMocks and the test file:
...
<!-- Testing files -->
<link rel="stylesheet" type="text/css" href="lib/jasmine-core/jasmine.css">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.3/jasmine.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.3/jasmine.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.3/jasmine-html.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.3/boot.min.js"></script>
<script type="text/javascript" src="lib/angular-mocks/angular-mocks.js"></script>
<script src="js/TestBackend.js"></script>
...
And here is the above referenced testBackend.js file, in which I intend to make the tests:
describe('FirstCycleController', function() {
beforeEach(module('myApp'));
beforeEach(module('ngMockE2E'));
var $controller;
var $rootScope;
var $httpBackend;
beforeEach(inject(function(_$controller_, _$rootScope_, _$httpBackend_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
}));
describe('User login', function() {
it('verifys that a user is correctly logged.', inject(function() {
var $identificationScope = {};
var identificationController = $controller('IdentificationCtrl', { $scope: $identificationScope });
var form = {
$valid: true
}
$identificationScope.loginData = {
email: 'user#test.com',
password: 'usertest'
};
$rootScope.userTest = null;
//pass through everything
$httpBackend.whenGET(/^\w+.*/).passThrough();
$httpBackend.whenPOST(/^\w+.*/).passThrough();
//call the login function simulating a login
$identificationScope.doLogin({ $valid: true });
setTimeout(function() {
expect($rootScope.userTest).not.toBe(null);
}, 150);
}));
});
});
The problem is that when running the testBackend.js test file, it doesn't make any http call. It seems that passThrough() function isn't doing his job correctly.
I faced and corrected the issue of not having defined the passThrough() function, which was because I didn't load the ngMockE2E module(instead of ngMock). But this time Jasmine is working fine and the error is simply that the spec is false:
Error: Expected null not to be null.
Apologies if this issue is already resolved, I couldn't find the solution anywhere.
There is detailed discussion around this on angular github issue
Related
I'm getting the following syntax error in the console while trying to get data from 'openweathermap'
Uncaught SyntaxError: Unexpected token :
Here is the JS file :
var app = angular.module('App', ['ngResource']);
app.factory('weatherService', function($http) {
return {
getWeather: function() {
var weather = '';
// if (!!prmSearchValue) {
// var searchValue = prmSearchValue;
$http.jsonp('https://api.openweathermap.org/data/2.5/weather?q=London,uk&appid=c19bc0731cec50456576c7b36a675ca7&mode=json').success(function(data) {
weather = 3232;
});
// }
/* else {
weather = {};
} */
return weather;
}
};
});
//Eilat,Israel
app.controller('httpAppCtrlr', function($scope, weatherService) {
$scope.searchText = '';
$scope.searchWeather = function() {
var prmSearchValue = $scope.searchText;
$scope.weather = weatherService.getWeather();
};
});
It looks as if the data that returns is broken in some way..
Fiddle
Use $http Get instead of JSONP. Better way to handle the error is using .then, Change your Factory as follows,
app.factory('weatherService', function ($http) {
return {
getWeather: function () {
var weatherForcast = {};
$http({
method: 'GET',
url: "https://api.openweathermap.org/data/2.5/weather?q=London,uk&appid=c19bc0731cec50456576c7b36a675ca7"
}).then(function successCallback(response) {
angular.extend(weatherForcast, response.data);
}, function errorCallback(response) {
alert('API call failed, possibly due to rate limiting or bad zip code.');
});
return weatherForcast;
}
};
});
WORKING FIDDLE
In AngularJS jsonp, you need to append callback=JSON_CALLBACK to the url. (I'll assume there is a reason you're using jsonp instead of get.)
Replace
https://api.openweathermap.org/data/2.5/weather?q=London,uk&appid=c19bc0731cec50456576c7b36a675ca7&mode=json
with
https://api.openweathermap.org/data/2.5/weather?q=London,uk&appid=c19bc0731cec50456576c7b36a675ca7&mode=json&callback=JSON_CALLBACK
Fiddle
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;
});
app.controller('PageCtrl', ['$scope', '$stateParams', '$state', 'USER_ROLES', 'AuthService', function($scope,$stateParams,$state,USER_ROLES, AuthService){
//console.log("Page Controller reporting for duty.");
$scope.currentUser = null;
$scope.currentUserExists = false; //<- defined in scope of PageCtrl
$scope.userRoles = USER_ROLES;
$scope.isAuthorized = AuthService.isAuthorized;
$scope.setCurrentUser = function (user) {
$scope.currentUser = user;
$scope.currentUserExists = true; //<- set true here!!
};
Now in my html code I'm doing:
<body ng-app="myApp" ng-controller="PageCtrl">
....
<div class="navbar-brand" ng-if="currentUserExists">Welcome!!</div>
<div class="navbar-brand" ng-if="currentUser.data.teacher._id">Welcome2!!</div>
Iv tried ng-show, and I'm trying both examples above to test it.
If i log to console currentUser within my js file everything appears to be working normal. But nothing will show on my page.
Am i missing something??
currentUser is json set like:
$scope.login = function (credentials) {
AuthService.login(credentials).then(function (user) {
$rootScope.$broadcast(AUTH_EVENTS.loginSuccess);
$scope.setCurrentUser(user);
console.log("currentuser");
console.log($scope.currentUser.data.teacher._id); //Yes its working...
}, function () {
$rootScope.$broadcast(AUTH_EVENTS.loginFailed);
});
};
...
authService.login = function (credentials) {
return $http({
method: 'POST',
url: 'proxy2.php',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
transformRequest: function(obj) {
var str = [];
for(var p in obj)
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
return str.join("&");
},
data: {
url: 'http://teach.classdojo.com/api/dojoSession?_u=tid',
login: credentials.login,
password: credentials.password,
type: 'teacher'
}
}).success(function (res) {
alert("login success");
Session.create(res.teacher._id, "admin");
return res.teacher;
}).error(function (res) {
alert(res.error.detail);
});
};
You may want to try this:
<div class="navbar-brand" ng-if="currentUserExists==true">Welcome!!</div>
The problem may be due to your setting a primitive value on the scope.
Try using view.currentUserExists instead- so set $scope.view = {currentUserExists: false} in the initialization and then $scope.view.currentUserExists = true in the promise resolution.
I couldn't understand why the scope wasn't working on the page, apparently due to inheritence or maybe because i was using $scope in two controllers?
Anyway i solved it using a service:
$scope.login = function (credentials) {
AuthService.login(credentials).then(function (user) {
userService.setCurrentUser(user); //<----Added this
$rootScope.$broadcast(AUTH_EVENTS.loginSuccess);
}, function () {
$rootScope.$broadcast(AUTH_EVENTS.loginFailed);
});
};
...
var updateUser = function() {
scope.view.currentUser = userService.getCurrentUser();
};
scope.$on(AUTH_EVENTS.loginSuccess, updateUser);
..
app.service('userService', function() {
var currentUser = null;
return {
setCurrentUser: function(user) {
currentUser = user;
},
getCurrentUser: function() {
return currentUser;
}
};
});
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'm trying to set the headers of a resource (code bellow).
It happens that, when I instantiate my resource ($scope.user = new rsrUser;) angularjs fetches the cookies that aren't yet defined (an "undefined" error is fired from inside "getHMAC()"). The cookies will only be defined when "$scope.login()" is fired (it happens when the user clicks a button in the interface).
Is there a better way of doing this?
controllers.js
angularjsWebInterfaceControllers.controller('loginCtrl', ['$scope', 'rsrUser',
function($scope, rsrUser){
$cookieStore.put("username","therebedragons");
$cookieStore.put("password","therebedragons");
$scope.user = new rsrUser;
$scope.user.username = ""; //bound to input field in interface
$scope.user.password = ""; //bound to input field in interface
$scope.login = function() {
$cookieStore.put("username", $scope.user.username);
$cookieStore.put("password", $scope.user.password);
$cookieStore.put("state", "loggedOUT");
$scope.user.$logIn(
function(){
$cookieStore.put("state", "loggedIN");
}, function() {
$cookieStore.put("username","therebedragons");
$cookieStore.put("password","therebedragons");
$cookieStore.put("state", "loggedOUT");
}
)
};
}]);
services.js
angularjsWebInterfaceServices.service('rsrUser', [ '$resource', '$cookieStore',
function($resource, $cookieStore){
var req = "/login"
var timestamp = getMicrotime(true).toString();
var username = $cookieStore.get("username");
var key = $cookieStore.get("password");
return $resource(baseURL + req, {}, {
logIn: {method:'POST',
isArray:false,
headers:{
'X-MICROTIME': timestamp,
'X-USERNAME': username,
'X-HASH': getHMAC(username,timestamp,req,key)
}
}
});
}]);
EDIT: Actually, the cookies are defiend as soon as the controller is instantiated;
The value for a header can be a function that returns a string (see arguments here: http://docs.angularjs.org/api/ng/service/$http#usage). That way the cookie isn't accessed in your resource until the logIn method is called.
return $resource(baseURL + req, {}, {
logIn: {method:'POST',
isArray:false,
headers: {
'X-MICROTIME': timestamp,
'X-USERNAME': function() {
return $cookieStore.get("username");
},
'X-HASH': function() {
var username = $cookieStore.get("username");
return getHMAC(username,timestamp,req,key)
}
}
}
});