Permalink
Browse files

feat($timeout): add $timeout service that supersedes $defer

$timeout has a better name ($defer got often confused with something related to $q) and
is actually promise based with cancelation support.

With this commit the $defer service is deprecated and will be removed before 1.0.

Closes #704, #532
  • Loading branch information...
1 parent 15b8f20 commit 4511d39cc748288df70bdc258f98a8f36652e683 @IgorMinar IgorMinar committed May 23, 2012
View
@@ -29,6 +29,7 @@ angularFiles = {
'src/ng/http.js',
'src/ng/httpBackend.js',
'src/ng/locale.js',
+ 'src/ng/timeout.js',
'src/ng/filter.js',
'src/ng/filter/filter.js',
@@ -125,6 +125,7 @@ function publishExternalAPI(angular){
$q: $QProvider,
$sniffer: $SnifferProvider,
$templateCache: $TemplateCacheProvider,
+ $timeout: $TimeoutProvider,
$window: $WindowProvider
});
}
View
@@ -3,6 +3,8 @@
/**
* @ngdoc function
* @name angular.module.ng.$defer
+ * @deprecated Made obsolete by $timeout service. Please migrate your code. This service will be
+ * removed with 1.0 final.
* @requires $browser
*
* @description
@@ -29,7 +31,9 @@
* @returns {boolean} Returns `true` if the task hasn't executed yet and was successfuly canceled.
*/
function $DeferProvider(){
- this.$get = ['$rootScope', '$browser', function($rootScope, $browser) {
+ this.$get = ['$rootScope', '$browser', '$log', function($rootScope, $browser, $log) {
+ $log.warn('$defer service has been deprecated, migrate to $timeout');
+
function defer(fn, delay) {
return $browser.defer(function() {
$rootScope.$apply(fn);
View
@@ -0,0 +1,87 @@
+'use strict';
+
+
+function $TimeoutProvider() {
+ this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler',
+ function($rootScope, $browser, $q, $exceptionHandler) {
+ var deferreds = {};
+
+
+ /**
+ * @ngdoc function
+ * @name angular.module.ng.$timeout
+ * @requires $browser
+ *
+ * @description
+ * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch
+ * block and delegates any exceptions to
+ * {@link angular.module.ng.$exceptionHandler $exceptionHandler} service.
+ *
+ * The return value of registering a timeout function is a promise which will be resolved when
+ * the timeout is reached and the timeout function is executed.
+ *
+ * To cancel a the timeout request, call `$timeout.cancel(promise)`.
+ *
+ * In tests you can use {@link angular.module.ngMock.$timeout `$timeout.flush()`} to
+ * synchronously flush the queue of deferred functions.
+ *
+ * @param {function()} fn A function, who's execution should be delayed.
+ * @param {number=} [delay=0] Delay in milliseconds.
+ * @param {boolean=} [invokeApply=true] If set to false skips model dirty checking, otherwise
+ * will invoke `fn` within the {@link angular.module.ng.$rootScope.Scope#$apply $apply} block.
+ * @returns {*} Promise that will be resolved when the timeout is reached. The value this
+ * promise will be resolved with is the return value of the `fn` function.
+ */
+ function timeout(fn, delay, invokeApply) {
+ var deferred = $q.defer(),
+ promise = deferred.promise,
+ skipApply = (isDefined(invokeApply) && !invokeApply),
+ timeoutId, cleanup;
+
+ timeoutId = $browser.defer(function() {
+ try {
+ deferred.resolve(fn());
+ } catch(e) {
+ deferred.reject(e);
+ $exceptionHandler(e);
+ }
+
+ if (!skipApply) $rootScope.$apply();
+ }, delay);
+
+ cleanup = function() {
+ delete deferreds[promise.$$timeoutId];
+ };
+
+ promise.$$timeoutId = timeoutId;
+ deferreds[timeoutId] = deferred;
+ promise.then(cleanup, cleanup);
+
+ return promise;
+ }
+
+
+ /**
+ * @ngdoc function
+ * @name angular.module.ng.$timeout#cancel
+ * @methodOf angular.module.ng.$timeout
+ *
+ * @description
+ * Cancels a task associated with the `promise`. As a result of this the promise will be
+ * resolved with a rejection.
+ *
+ * @param {Promise} promise Promise returned by the `$timeout` function.
+ * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully
+ * canceled.
+ */
+ timeout.cancel = function(promise) {
+ if (promise.$$timeoutId in deferreds) {
+ deferreds[promise.$$timeoutId].reject('canceled');
+ return $browser.defer.cancel(promise.$$timeoutId);
+ }
+ return false;
+ };
+
+ return timeout;
+ }];
+}
@@ -1328,6 +1328,25 @@ function MockXhr() {
this.abort = angular.noop;
}
+
+/**
+ * @ngdoc function
+ * @name angular.module.ngMock.$timeout
+ * @description
+ *
+ * This service is just a simple decorator for {@link angular.module.ng.$timeout $timeout} service
+ * that adds a "flush" method.
+ */
+
+/**
+ * @ngdoc method
+ * @name angular.module.ngMock.$timeout#flush
+ * @methodOf angular.module.ngMock.$timeout
+ * @description
+ *
+ * Flushes the queue of pending tasks.
+ */
+
/**
* @ngdoc overview
* @name angular.module.ngMock
@@ -1341,6 +1360,13 @@ angular.module('ngMock', ['ng']).provider({
$exceptionHandler: angular.mock.$ExceptionHandlerProvider,
$log: angular.mock.$LogProvider,
$httpBackend: angular.mock.$HttpBackendProvider
+}).config(function($provide) {
+ $provide.decorator('$timeout', function($delegate, $browser) {
+ $delegate.flush = function() {
+ $browser.defer.flush();
+ };
+ return $delegate;
+ });
});
@@ -5,6 +5,7 @@ describe('$defer', function() {
$provide.factory('$exceptionHandler', function(){
return jasmine.createSpy('$exceptionHandler');
});
+ $provide.value('$log', {warn: noop});
}));
View
@@ -0,0 +1,146 @@
+'use strict';
+
+describe('$timeout', function() {
+
+ beforeEach(module(provideLog));
+
+
+ it('should delegate functions to $browser.defer', inject(function($timeout, $browser) {
+ var counter = 0;
+ $timeout(function() { counter++; });
+
+ expect(counter).toBe(0);
+
+ $browser.defer.flush();
+ expect(counter).toBe(1);
+
+ expect(function() {$browser.defer.flush();}).toThrow('No deferred tasks to be flushed');
+ expect(counter).toBe(1);
+ }));
+
+
+ it('should call $apply after each callback is executed', inject(function($timeout, $rootScope) {
+ var applySpy = spyOn($rootScope, '$apply').andCallThrough();
+
+ $timeout(function() {});
+ expect(applySpy).not.toHaveBeenCalled();
+
+ $timeout.flush();
+ expect(applySpy).toHaveBeenCalledOnce();
+
+ applySpy.reset();
+
+ $timeout(function() {});
+ $timeout(function() {});
+ $timeout.flush();
+ expect(applySpy.callCount).toBe(2);
+ }));
+
+
+ it('should NOT call $apply if skipApply is set to true', inject(function($timeout, $rootScope) {
+ var applySpy = spyOn($rootScope, '$apply').andCallThrough();
+
+ $timeout(function() {}, 12, false);
+ expect(applySpy).not.toHaveBeenCalled();
+
+ $timeout.flush();
+ expect(applySpy).not.toHaveBeenCalled();
+ }));
+
+
+ it('should allow you to specify the delay time', inject(function($timeout, $browser) {
+ var defer = spyOn($browser, 'defer');
+ $timeout(noop, 123);
+ expect(defer.callCount).toEqual(1);
+ expect(defer.mostRecentCall.args[1]).toEqual(123);
+ }));
+
+
+ it('should return a promise which will be resolved with return value of the timeout callback',
+ inject(function($timeout, log) {
+ var promise = $timeout(function() { log('timeout'); return 'buba'; });
+
+ promise.then(function(value) { log('promise success: ' + value); }, log.fn('promise error'));
+ expect(log).toEqual([]);
+
+ $timeout.flush();
+ expect(log).toEqual(['timeout', 'promise success: buba']);
+ }));
+
+
+ describe('exception handling', function() {
+
+ beforeEach(module(function($exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ }));
+
+
+ it('should delegate exception to the $exceptionHandler service', inject(
+ function($timeout, $exceptionHandler) {
+ $timeout(function() {throw "Test Error";});
+ expect($exceptionHandler.errors).toEqual([]);
+
+ $timeout.flush();
+ expect($exceptionHandler.errors).toEqual(["Test Error"]);
+ }));
+
+
+ it('should call $apply even if an exception is thrown in callback', inject(
+ function($timeout, $rootScope) {
+ var applySpy = spyOn($rootScope, '$apply').andCallThrough();
+
+ $timeout(function() {throw "Test Error";});
+ expect(applySpy).not.toHaveBeenCalled();
+
+ $timeout.flush();
+ expect(applySpy).toHaveBeenCalled();
+ }));
+
+
+ it('should reject the timeout promise when an exception is thrown in the timeout callback',
+ inject(function($timeout, log) {
+ var promise = $timeout(function() { throw "Some Error"; });
+
+ promise.then(log.fn('success'), function(reason) { log('error: ' + reason); });
+ $timeout.flush();
+
+ expect(log).toEqual('error: Some Error');
+ }));
+ });
+
+
+ describe('cancel', function() {
+ it('should cancel tasks', inject(function($timeout) {
+ var task1 = jasmine.createSpy('task1'),
+ task2 = jasmine.createSpy('task2'),
+ task3 = jasmine.createSpy('task3'),
+ promise1, promise3;
+
+ promise1 = $timeout(task1);
+ $timeout(task2);
+ promise3 = $timeout(task3, 333);
+
+ $timeout.cancel(promise3);
+ $timeout.cancel(promise1);
+ $timeout.flush();
+
+ expect(task1).not.toHaveBeenCalled();
+ expect(task2).toHaveBeenCalledOnce();
+ expect(task3).not.toHaveBeenCalled();
+ }));
+
+
+ it('should return true if a task was successfully canceled', inject(function($timeout) {
+ var task1 = jasmine.createSpy('task1'),
+ task2 = jasmine.createSpy('task2'),
+ promise1, promise2;
+
+ promise1 = $timeout(task1);
+ $timeout.flush();
+ promise2 = $timeout(task2);
+
+ expect($timeout.cancel(promise1)).toBe(false);
+ expect($timeout.cancel(promise2)).toBe(true);
+ }));
+ });
+});
@@ -313,6 +313,23 @@ describe('ngMock', function() {
});
+ describe('$timeout', function() {
+ it('should expose flush method that will flush the pending queue of tasks', inject(
+ function($timeout) {
+ var logger = [],
+ logFn = function(msg) { return function() { logger.push(msg) }};
+
+ $timeout(logFn('t1'));
+ $timeout(logFn('t2'), 200);
+ $timeout(logFn('t3'));
+ expect(logger).toEqual([]);
+
+ $timeout.flush();
+ expect(logger).toEqual(['t1', 't3', 't2']);
+ }));
+ });
+
+
describe('angular.mock.dump', function(){
var d = angular.mock.dump;

0 comments on commit 4511d39

Please sign in to comment.