diff --git a/src/ng/http.js b/src/ng/http.js index ca746ea3e67d..09df456ed57d 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -801,6 +801,12 @@ function $HttpProvider() { * - **headers** – `{Object}` – Map of strings or functions which return strings representing * HTTP headers to send to the server. If the return value of a function is null, the * header will not be sent. Functions accept a config object as an argument. + * - **eventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest object. + * To bind events to the XMLHttpRequest upload object, use `uploadEventHandlers`. + * The handler will be called in the context of a `$apply` block. + * - **uploadEventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest upload + * object. To bind events to the XMLHttpRequest object, use `eventHandlers`. + * The handler will be called in the context of a `$apply` block. * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token. * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token. * - **transformRequest** – @@ -1259,11 +1265,31 @@ function $HttpProvider() { } $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout, - config.withCredentials, config.responseType); + config.withCredentials, config.responseType, + createApplyHandlers(config.eventHandlers), + createApplyHandlers(config.uploadEventHandlers)); } return promise; + function createApplyHandlers(eventHandlers) { + if (eventHandlers) { + var applyHandlers = {}; + forEach(eventHandlers, function(eventHandler, key) { + applyHandlers[key] = function() { + if (useApplyAsync) { + $rootScope.$applyAsync(eventHandler); + } else if ($rootScope.$$phase) { + eventHandler(); + } else { + $rootScope.$apply(eventHandler); + } + }; + }); + return applyHandlers; + } + } + /** * Callback registered to $httpBackend(): diff --git a/src/ng/httpBackend.js b/src/ng/httpBackend.js index 7340be43deee..727d11169eb0 100644 --- a/src/ng/httpBackend.js +++ b/src/ng/httpBackend.js @@ -54,7 +54,7 @@ function $HttpBackendProvider() { function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { // TODO(vojta): fix the signature - return function(method, url, post, callback, headers, timeout, withCredentials, responseType) { + return function(method, url, post, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) { $browser.$$incOutstandingRequestCount(); url = url || $browser.url(); @@ -114,6 +114,14 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc xhr.onerror = requestError; xhr.onabort = requestError; + forEach(eventHandlers, function(value, key) { + xhr.addEventListener(key, value); + }); + + forEach(uploadEventHandlers, function(value, key) { + xhr.upload.addEventListener(key, value); + }); + if (withCredentials) { xhr.withCredentials = true; } diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index b8fc8d065985..e39fcfcd4191 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -1322,12 +1322,15 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { } // TODO(vojta): change params to: method, url, data, headers, callback - function $httpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType) { + function $httpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) { var xhr = new MockXhr(), expectation = expectations[0], wasExpected = false; + xhr.$$events = eventHandlers; + xhr.upload.$$events = uploadEventHandlers; + function prettyPrint(data) { return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) ? data @@ -2010,6 +2013,20 @@ function MockXhr() { }; this.abort = angular.noop; + + // This section simulates the events on a real XHR object (and the upload object) + // When we are testing $httpBackend (inside the angular project) we make partial use of this + // but store the events directly ourselves on `$$events`, instead of going through the `addEventListener` + this.$$events = {}; + this.addEventListener = function(name, listener) { + if (angular.isUndefined(this.$$events[name])) this.$$events[name] = []; + this.$$events[name].push(listener); + }; + + this.upload = { + $$events: {}, + addEventListener: this.addEventListener + }; } diff --git a/test/ng/httpBackendSpec.js b/test/ng/httpBackendSpec.js index 5dbb6cfdff2b..af3175acfd6d 100644 --- a/test/ng/httpBackendSpec.js +++ b/test/ng/httpBackendSpec.js @@ -241,6 +241,17 @@ describe('$httpBackend', function() { }); + it('should set up event listeners', function() { + var progressFn = function() {}; + var uploadProgressFn = function() {}; + $backend('GET', '/url', null, callback, {}, null, null, null, + {progress: progressFn}, {progress: uploadProgressFn}); + xhr = MockXhr.$$lastInstance; + expect(xhr.$$events.progress[0]).toBe(progressFn); + expect(xhr.upload.$$events.progress[0]).toBe(uploadProgressFn); + }); + + describe('responseType', function() { it('should set responseType and return xhr.response', function() { diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index b2cabf668010..1f948f33307a 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -1,5 +1,7 @@ 'use strict'; +/* global MockXhr: false */ + describe('$http', function() { var callback, mockedCookies; @@ -1019,7 +1021,7 @@ describe('$http', function() { }); - describe('scope.$apply', function() { + describe('callbacks', function() { it('should $apply after success callback', function() { $httpBackend.when('GET').respond(200); @@ -1047,6 +1049,33 @@ describe('$http', function() { $exceptionHandler.errors = []; })); + + + it('should pass the event handlers through to the backend', function() { + var progressFn = jasmine.createSpy('progressFn'); + var uploadProgressFn = jasmine.createSpy('uploadProgressFn'); + $httpBackend.when('GET').respond(200); + $http({ + method: 'GET', + url: '/some', + eventHandlers: {progress: progressFn}, + uploadEventHandlers: {progress: uploadProgressFn} + }); + $rootScope.$apply(); + var mockXHR = MockXhr.$$lastInstance; + expect(mockXHR.$$events.progress).toEqual(jasmine.any(Function)); + expect(mockXHR.upload.$$events.progress).toEqual(jasmine.any(Function)); + + spyOn($rootScope, '$digest'); + + mockXHR.$$events.progress(); + expect(progressFn).toHaveBeenCalledOnce(); + expect($rootScope.$digest).toHaveBeenCalledTimes(1); + + mockXHR.upload.$$events.progress(); + expect(uploadProgressFn).toHaveBeenCalledOnce(); + expect($rootScope.$digest).toHaveBeenCalledTimes(2); + }); });