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...
IgorMinar committed May 23, 2012
1 parent 15b8f20 commit 4511d39cc748288df70bdc258f98a8f36652e683
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',
View
@@ -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;
});
});
View
@@ -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.