Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
feat($timeout): add $timeout service that supersedes $defer
Browse files Browse the repository at this point in the history
$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 4511d39
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 1 deletion.
1 change: 1 addition & 0 deletions angularFiles.js
Expand Up @@ -29,6 +29,7 @@ angularFiles = {
'src/ng/http.js', 'src/ng/http.js',
'src/ng/httpBackend.js', 'src/ng/httpBackend.js',
'src/ng/locale.js', 'src/ng/locale.js',
'src/ng/timeout.js',


'src/ng/filter.js', 'src/ng/filter.js',
'src/ng/filter/filter.js', 'src/ng/filter/filter.js',
Expand Down
1 change: 1 addition & 0 deletions src/AngularPublic.js
Expand Up @@ -125,6 +125,7 @@ function publishExternalAPI(angular){
$q: $QProvider, $q: $QProvider,
$sniffer: $SnifferProvider, $sniffer: $SnifferProvider,
$templateCache: $TemplateCacheProvider, $templateCache: $TemplateCacheProvider,
$timeout: $TimeoutProvider,
$window: $WindowProvider $window: $WindowProvider
}); });
} }
Expand Down
6 changes: 5 additions & 1 deletion src/ng/defer.js
Expand Up @@ -3,6 +3,8 @@
/** /**
* @ngdoc function * @ngdoc function
* @name angular.module.ng.$defer * @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 * @requires $browser
* *
* @description * @description
Expand All @@ -29,7 +31,9 @@
* @returns {boolean} Returns `true` if the task hasn't executed yet and was successfuly canceled. * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfuly canceled.
*/ */
function $DeferProvider(){ 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) { function defer(fn, delay) {
return $browser.defer(function() { return $browser.defer(function() {
$rootScope.$apply(fn); $rootScope.$apply(fn);
Expand Down
87 changes: 87 additions & 0 deletions src/ng/timeout.js
@@ -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;
}];
}
26 changes: 26 additions & 0 deletions src/ngMock/angular-mocks.js
Expand Up @@ -1328,6 +1328,25 @@ function MockXhr() {
this.abort = angular.noop; 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 * @ngdoc overview
* @name angular.module.ngMock * @name angular.module.ngMock
Expand All @@ -1341,6 +1360,13 @@ angular.module('ngMock', ['ng']).provider({
$exceptionHandler: angular.mock.$ExceptionHandlerProvider, $exceptionHandler: angular.mock.$ExceptionHandlerProvider,
$log: angular.mock.$LogProvider, $log: angular.mock.$LogProvider,
$httpBackend: angular.mock.$HttpBackendProvider $httpBackend: angular.mock.$HttpBackendProvider
}).config(function($provide) {
$provide.decorator('$timeout', function($delegate, $browser) {
$delegate.flush = function() {
$browser.defer.flush();
};
return $delegate;
});
}); });




Expand Down
1 change: 1 addition & 0 deletions test/ng/deferSpec.js
Expand Up @@ -5,6 +5,7 @@ describe('$defer', function() {
$provide.factory('$exceptionHandler', function(){ $provide.factory('$exceptionHandler', function(){
return jasmine.createSpy('$exceptionHandler'); return jasmine.createSpy('$exceptionHandler');
}); });
$provide.value('$log', {warn: noop});
})); }));




Expand Down
146 changes: 146 additions & 0 deletions test/ng/timeoutSpec.js
@@ -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);
}));
});
});
17 changes: 17 additions & 0 deletions test/ngMock/angular-mocksSpec.js
Expand Up @@ -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(){ describe('angular.mock.dump', function(){
var d = angular.mock.dump; var d = angular.mock.dump;


Expand Down

0 comments on commit 4511d39

Please sign in to comment.