diff --git a/angularFiles.js b/angularFiles.js index f1fda79e1cb6..e2b384fcac36 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -28,6 +28,7 @@ var angularFiles = { 'src/ng/httpBackend.js', 'src/ng/interpolate.js', 'src/ng/interval.js', + 'src/ng/jsonpCallbacks.js', 'src/ng/locale.js', 'src/ng/location.js', 'src/ng/log.js', diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 20c35f52f658..376294c49d34 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -76,6 +76,7 @@ $HttpParamSerializerJQLikeProvider, $HttpBackendProvider, $xhrFactoryProvider, + $jsonpCallbacksProvider, $LocationProvider, $LogProvider, $ParseProvider, @@ -237,6 +238,7 @@ function publishExternalAPI(angular) { $httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider, $httpBackend: $HttpBackendProvider, $xhrFactory: $xhrFactoryProvider, + $jsonpCallbacks: $jsonpCallbacksProvider, $location: $LocationProvider, $log: $LogProvider, $parse: $ParseProvider, diff --git a/src/ng/jsonpCallbacks.js b/src/ng/jsonpCallbacks.js new file mode 100644 index 000000000000..180990516308 --- /dev/null +++ b/src/ng/jsonpCallbacks.js @@ -0,0 +1,83 @@ +'use strict'; + +/** + * @ngdoc service + * @name $jsonpCallbacks + * @requires $window + * @description + * This service handles the lifecycle of callbacks to handle JSONP requests. + * Override this service if you wish to customise where the callbacks are stored and + * how they vary compared to the requested url. + */ +var $jsonpCallbacksProvider = function() { + this.$get = ['$window', function($window) { + var counter = 0; + $window.angular.callbacks = {}; + var callbackMap = {}; + + function createCallback(callbackId) { + var callback = function(data) { + callback.data = data; + callback.called = true; + }; + callback.id = callbackId; + return callback; + } + + return { + /** + * @ngdoc method + * @name $jsonpCallbacks#createCallback + * @param {string} url the url of the JSONP request + * @returns {string} the callback path to send to the server as part of the JSONP request + * @description + * {@link $httpBackend} calls this method to create a callback and get hold of the path to the callback + * to pass to the server, which will be used to call the callback with its payload in the JSONP response. + */ + createCallback: function(url) { + var callbackId = '_' + (counter++).toString(36); + var callbackPath = 'angular.callbacks.' + callbackId; + var callback = createCallback(callbackId); + callbackMap[callbackPath] = $window.angular.callbacks[callbackId] = callback; + return callbackPath; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#wasCalled + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @returns {boolean} whether the callback has been called, as a result of the JSONP response + * @description + * {@link $httpBackend} calls this method to find out whether the JSONP response actually called the + * callback that was passed in the request. + */ + wasCalled: function(callbackPath) { + return callbackMap[callbackPath].called; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#getResponse + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @returns {*} the data received from the response via the registered callback + * @description + * {@link $httpBackend} calls this method to get hold of the data that was provided to the callback + * in the JSONP response. + */ + getResponse: function(callbackPath) { + return callbackMap[callbackPath].data; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#removeCallback + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @description + * {@link $httpBackend} calls this method to remove the callback after the JSONP request has + * completed or timed-out. + */ + removeCallback: function(callbackPath) { + var callback = callbackMap[callbackPath]; + delete $window.angular.callbacks[callback.id]; + delete callbackMap[callbackPath]; + } + }; + }]; +}; diff --git a/test/ng/jsonpCallbacksSpec.js b/test/ng/jsonpCallbacksSpec.js new file mode 100644 index 000000000000..75d421857552 --- /dev/null +++ b/test/ng/jsonpCallbacksSpec.js @@ -0,0 +1,73 @@ +'use strict'; + +describe('$jsonpCallbacks', function() { + + beforeEach(module(function($provide) { + // mock out the $window object + $provide.value('$window', { angular: {} }); + })); + + describe('createCallback(url)', function() { + + it('should return a new unique path to a callback function on each call', inject(function($jsonpCallbacks) { + var path = $jsonpCallbacks.createCallback('http://some.dummy.com/jsonp/request'); + expect(path).toEqual('angular.callbacks._0'); + + path = $jsonpCallbacks.createCallback('http://some.dummy.com/jsonp/request'); + expect(path).toEqual('angular.callbacks._1'); + + path = $jsonpCallbacks.createCallback('http://some.dummy.com/jsonp/request'); + expect(path).toEqual('angular.callbacks._2'); + + path = $jsonpCallbacks.createCallback('http://some.dummy.com/jsonp/request'); + expect(path).toEqual('angular.callbacks._3'); + })); + + it('should add a callback method to the $window.angular.callbacks collection on each call', inject(function($window, $jsonpCallbacks) { + $jsonpCallbacks.createCallback('http://some.dummy.com/jsonp/request'); + expect($window.angular.callbacks._0).toEqual(jasmine.any(Function)); + + $jsonpCallbacks.createCallback('http://some.dummy.com/jsonp/request'); + expect($window.angular.callbacks._1).toEqual(jasmine.any(Function)); + + $jsonpCallbacks.createCallback('http://some.dummy.com/jsonp/request'); + expect($window.angular.callbacks._2).toEqual(jasmine.any(Function)); + + $jsonpCallbacks.createCallback('http://some.dummy.com/jsonp/request'); + expect($window.angular.callbacks._3).toEqual(jasmine.any(Function)); + })); + }); + + + describe('wasCalled(callbackPath)', function() { + + it('should return true once the callback has been called', inject(function($window, $jsonpCallbacks) { + var path = $jsonpCallbacks.createCallback('http://some.dummy.com/jsonp/request'); + expect($jsonpCallbacks.wasCalled(path)).toBeFalsy(); + var response = {}; + $window.angular.callbacks._0(response); + expect($jsonpCallbacks.wasCalled(path)).toBeTruthy(); + })); + }); + + + describe('getResponse(callbackPath)', function() { + + it('should retrieve the data from when the callback was called', inject(function($window, $jsonpCallbacks) { + var path = $jsonpCallbacks.createCallback('http://some.dummy.com/jsonp/request'); + var response = {}; + $window.angular.callbacks._0(response); + var result = $jsonpCallbacks.getResponse(path); + expect(result).toBe(response); + })); + }); + + describe('removeCallback(calbackPath)', function() { + + it('should remove the callback', inject(function($window, $jsonpCallbacks) { + var path = $jsonpCallbacks.createCallback('http://some.dummy.com/jsonp/request'); + $jsonpCallbacks.removeCallback(path); + expect($window.angular.callbacks._0).toBeUndefined(); + })); + }); +});