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

Commit 4511d39

Browse files
committed
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
1 parent 15b8f20 commit 4511d39

File tree

8 files changed

+284
-1
lines changed

8 files changed

+284
-1
lines changed

angularFiles.js

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ angularFiles = {
2929
'src/ng/http.js',
3030
'src/ng/httpBackend.js',
3131
'src/ng/locale.js',
32+
'src/ng/timeout.js',
3233

3334
'src/ng/filter.js',
3435
'src/ng/filter/filter.js',

src/AngularPublic.js

+1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ function publishExternalAPI(angular){
125125
$q: $QProvider,
126126
$sniffer: $SnifferProvider,
127127
$templateCache: $TemplateCacheProvider,
128+
$timeout: $TimeoutProvider,
128129
$window: $WindowProvider
129130
});
130131
}

src/ng/defer.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
/**
44
* @ngdoc function
55
* @name angular.module.ng.$defer
6+
* @deprecated Made obsolete by $timeout service. Please migrate your code. This service will be
7+
* removed with 1.0 final.
68
* @requires $browser
79
*
810
* @description
@@ -29,7 +31,9 @@
2931
* @returns {boolean} Returns `true` if the task hasn't executed yet and was successfuly canceled.
3032
*/
3133
function $DeferProvider(){
32-
this.$get = ['$rootScope', '$browser', function($rootScope, $browser) {
34+
this.$get = ['$rootScope', '$browser', '$log', function($rootScope, $browser, $log) {
35+
$log.warn('$defer service has been deprecated, migrate to $timeout');
36+
3337
function defer(fn, delay) {
3438
return $browser.defer(function() {
3539
$rootScope.$apply(fn);

src/ng/timeout.js

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use strict';
2+
3+
4+
function $TimeoutProvider() {
5+
this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler',
6+
function($rootScope, $browser, $q, $exceptionHandler) {
7+
var deferreds = {};
8+
9+
10+
/**
11+
* @ngdoc function
12+
* @name angular.module.ng.$timeout
13+
* @requires $browser
14+
*
15+
* @description
16+
* Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch
17+
* block and delegates any exceptions to
18+
* {@link angular.module.ng.$exceptionHandler $exceptionHandler} service.
19+
*
20+
* The return value of registering a timeout function is a promise which will be resolved when
21+
* the timeout is reached and the timeout function is executed.
22+
*
23+
* To cancel a the timeout request, call `$timeout.cancel(promise)`.
24+
*
25+
* In tests you can use {@link angular.module.ngMock.$timeout `$timeout.flush()`} to
26+
* synchronously flush the queue of deferred functions.
27+
*
28+
* @param {function()} fn A function, who's execution should be delayed.
29+
* @param {number=} [delay=0] Delay in milliseconds.
30+
* @param {boolean=} [invokeApply=true] If set to false skips model dirty checking, otherwise
31+
* will invoke `fn` within the {@link angular.module.ng.$rootScope.Scope#$apply $apply} block.
32+
* @returns {*} Promise that will be resolved when the timeout is reached. The value this
33+
* promise will be resolved with is the return value of the `fn` function.
34+
*/
35+
function timeout(fn, delay, invokeApply) {
36+
var deferred = $q.defer(),
37+
promise = deferred.promise,
38+
skipApply = (isDefined(invokeApply) && !invokeApply),
39+
timeoutId, cleanup;
40+
41+
timeoutId = $browser.defer(function() {
42+
try {
43+
deferred.resolve(fn());
44+
} catch(e) {
45+
deferred.reject(e);
46+
$exceptionHandler(e);
47+
}
48+
49+
if (!skipApply) $rootScope.$apply();
50+
}, delay);
51+
52+
cleanup = function() {
53+
delete deferreds[promise.$$timeoutId];
54+
};
55+
56+
promise.$$timeoutId = timeoutId;
57+
deferreds[timeoutId] = deferred;
58+
promise.then(cleanup, cleanup);
59+
60+
return promise;
61+
}
62+
63+
64+
/**
65+
* @ngdoc function
66+
* @name angular.module.ng.$timeout#cancel
67+
* @methodOf angular.module.ng.$timeout
68+
*
69+
* @description
70+
* Cancels a task associated with the `promise`. As a result of this the promise will be
71+
* resolved with a rejection.
72+
*
73+
* @param {Promise} promise Promise returned by the `$timeout` function.
74+
* @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully
75+
* canceled.
76+
*/
77+
timeout.cancel = function(promise) {
78+
if (promise.$$timeoutId in deferreds) {
79+
deferreds[promise.$$timeoutId].reject('canceled');
80+
return $browser.defer.cancel(promise.$$timeoutId);
81+
}
82+
return false;
83+
};
84+
85+
return timeout;
86+
}];
87+
}

src/ngMock/angular-mocks.js

+26
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,25 @@ function MockXhr() {
13281328
this.abort = angular.noop;
13291329
}
13301330

1331+
1332+
/**
1333+
* @ngdoc function
1334+
* @name angular.module.ngMock.$timeout
1335+
* @description
1336+
*
1337+
* This service is just a simple decorator for {@link angular.module.ng.$timeout $timeout} service
1338+
* that adds a "flush" method.
1339+
*/
1340+
1341+
/**
1342+
* @ngdoc method
1343+
* @name angular.module.ngMock.$timeout#flush
1344+
* @methodOf angular.module.ngMock.$timeout
1345+
* @description
1346+
*
1347+
* Flushes the queue of pending tasks.
1348+
*/
1349+
13311350
/**
13321351
* @ngdoc overview
13331352
* @name angular.module.ngMock
@@ -1341,6 +1360,13 @@ angular.module('ngMock', ['ng']).provider({
13411360
$exceptionHandler: angular.mock.$ExceptionHandlerProvider,
13421361
$log: angular.mock.$LogProvider,
13431362
$httpBackend: angular.mock.$HttpBackendProvider
1363+
}).config(function($provide) {
1364+
$provide.decorator('$timeout', function($delegate, $browser) {
1365+
$delegate.flush = function() {
1366+
$browser.defer.flush();
1367+
};
1368+
return $delegate;
1369+
});
13441370
});
13451371

13461372

test/ng/deferSpec.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ describe('$defer', function() {
55
$provide.factory('$exceptionHandler', function(){
66
return jasmine.createSpy('$exceptionHandler');
77
});
8+
$provide.value('$log', {warn: noop});
89
}));
910

1011

test/ng/timeoutSpec.js

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
'use strict';
2+
3+
describe('$timeout', function() {
4+
5+
beforeEach(module(provideLog));
6+
7+
8+
it('should delegate functions to $browser.defer', inject(function($timeout, $browser) {
9+
var counter = 0;
10+
$timeout(function() { counter++; });
11+
12+
expect(counter).toBe(0);
13+
14+
$browser.defer.flush();
15+
expect(counter).toBe(1);
16+
17+
expect(function() {$browser.defer.flush();}).toThrow('No deferred tasks to be flushed');
18+
expect(counter).toBe(1);
19+
}));
20+
21+
22+
it('should call $apply after each callback is executed', inject(function($timeout, $rootScope) {
23+
var applySpy = spyOn($rootScope, '$apply').andCallThrough();
24+
25+
$timeout(function() {});
26+
expect(applySpy).not.toHaveBeenCalled();
27+
28+
$timeout.flush();
29+
expect(applySpy).toHaveBeenCalledOnce();
30+
31+
applySpy.reset();
32+
33+
$timeout(function() {});
34+
$timeout(function() {});
35+
$timeout.flush();
36+
expect(applySpy.callCount).toBe(2);
37+
}));
38+
39+
40+
it('should NOT call $apply if skipApply is set to true', inject(function($timeout, $rootScope) {
41+
var applySpy = spyOn($rootScope, '$apply').andCallThrough();
42+
43+
$timeout(function() {}, 12, false);
44+
expect(applySpy).not.toHaveBeenCalled();
45+
46+
$timeout.flush();
47+
expect(applySpy).not.toHaveBeenCalled();
48+
}));
49+
50+
51+
it('should allow you to specify the delay time', inject(function($timeout, $browser) {
52+
var defer = spyOn($browser, 'defer');
53+
$timeout(noop, 123);
54+
expect(defer.callCount).toEqual(1);
55+
expect(defer.mostRecentCall.args[1]).toEqual(123);
56+
}));
57+
58+
59+
it('should return a promise which will be resolved with return value of the timeout callback',
60+
inject(function($timeout, log) {
61+
var promise = $timeout(function() { log('timeout'); return 'buba'; });
62+
63+
promise.then(function(value) { log('promise success: ' + value); }, log.fn('promise error'));
64+
expect(log).toEqual([]);
65+
66+
$timeout.flush();
67+
expect(log).toEqual(['timeout', 'promise success: buba']);
68+
}));
69+
70+
71+
describe('exception handling', function() {
72+
73+
beforeEach(module(function($exceptionHandlerProvider) {
74+
$exceptionHandlerProvider.mode('log');
75+
}));
76+
77+
78+
it('should delegate exception to the $exceptionHandler service', inject(
79+
function($timeout, $exceptionHandler) {
80+
$timeout(function() {throw "Test Error";});
81+
expect($exceptionHandler.errors).toEqual([]);
82+
83+
$timeout.flush();
84+
expect($exceptionHandler.errors).toEqual(["Test Error"]);
85+
}));
86+
87+
88+
it('should call $apply even if an exception is thrown in callback', inject(
89+
function($timeout, $rootScope) {
90+
var applySpy = spyOn($rootScope, '$apply').andCallThrough();
91+
92+
$timeout(function() {throw "Test Error";});
93+
expect(applySpy).not.toHaveBeenCalled();
94+
95+
$timeout.flush();
96+
expect(applySpy).toHaveBeenCalled();
97+
}));
98+
99+
100+
it('should reject the timeout promise when an exception is thrown in the timeout callback',
101+
inject(function($timeout, log) {
102+
var promise = $timeout(function() { throw "Some Error"; });
103+
104+
promise.then(log.fn('success'), function(reason) { log('error: ' + reason); });
105+
$timeout.flush();
106+
107+
expect(log).toEqual('error: Some Error');
108+
}));
109+
});
110+
111+
112+
describe('cancel', function() {
113+
it('should cancel tasks', inject(function($timeout) {
114+
var task1 = jasmine.createSpy('task1'),
115+
task2 = jasmine.createSpy('task2'),
116+
task3 = jasmine.createSpy('task3'),
117+
promise1, promise3;
118+
119+
promise1 = $timeout(task1);
120+
$timeout(task2);
121+
promise3 = $timeout(task3, 333);
122+
123+
$timeout.cancel(promise3);
124+
$timeout.cancel(promise1);
125+
$timeout.flush();
126+
127+
expect(task1).not.toHaveBeenCalled();
128+
expect(task2).toHaveBeenCalledOnce();
129+
expect(task3).not.toHaveBeenCalled();
130+
}));
131+
132+
133+
it('should return true if a task was successfully canceled', inject(function($timeout) {
134+
var task1 = jasmine.createSpy('task1'),
135+
task2 = jasmine.createSpy('task2'),
136+
promise1, promise2;
137+
138+
promise1 = $timeout(task1);
139+
$timeout.flush();
140+
promise2 = $timeout(task2);
141+
142+
expect($timeout.cancel(promise1)).toBe(false);
143+
expect($timeout.cancel(promise2)).toBe(true);
144+
}));
145+
});
146+
});

test/ngMock/angular-mocksSpec.js

+17
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,23 @@ describe('ngMock', function() {
313313
});
314314

315315

316+
describe('$timeout', function() {
317+
it('should expose flush method that will flush the pending queue of tasks', inject(
318+
function($timeout) {
319+
var logger = [],
320+
logFn = function(msg) { return function() { logger.push(msg) }};
321+
322+
$timeout(logFn('t1'));
323+
$timeout(logFn('t2'), 200);
324+
$timeout(logFn('t3'));
325+
expect(logger).toEqual([]);
326+
327+
$timeout.flush();
328+
expect(logger).toEqual(['t1', 't3', 't2']);
329+
}));
330+
});
331+
332+
316333
describe('angular.mock.dump', function(){
317334
var d = angular.mock.dump;
318335

0 commit comments

Comments
 (0)