Skip to content

Commit 7b4a860

Browse files
lrlopezpetebacondarwin
authored andcommitted
feat($timeout): Add debouncing support to $timeout service
This feature allows resetting the timer on an ongoing `$timeout` promise. A fourth optional argument is added to `$timeout` which is the old promise to be reset. I.e.: `promise = $timeout(fn, 2000, true, promise);` This will call `fn()` 2 seconds after the last call to the `$timeout()` function.
1 parent 1b9395e commit 7b4a860

File tree

2 files changed

+59
-1
lines changed

2 files changed

+59
-1
lines changed

src/ng/timeout.js

+29-1
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,48 @@ function $TimeoutProvider() {
2424
* In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
2525
* synchronously flush the queue of deferred functions.
2626
*
27+
* You can also use `$timeout` to debounce the call of a function using the returned promise
28+
* as the fourth parameter in the next call. See the following example:
29+
*
30+
* <pre>
31+
* var debounce;
32+
* var doRealSave = function() {
33+
* // Save model to DB
34+
* }
35+
* $scope.save = function() {
36+
* // debounce call for 2 seconds
37+
* debounce = $timeout(doRealSave, 2000, false, debounce);
38+
* }
39+
* </pre>
40+
*
41+
* And in the form:
42+
*
43+
* <pre>
44+
* <input type="text" ng-model="name" ng-change="save()">
45+
* </pre>
46+
*
2747
* @param {function()} fn A function, whose execution should be delayed.
2848
* @param {number=} [delay=0] Delay in milliseconds.
2949
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
3050
* will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
51+
* @param {Promise=} debounce If set to an outgoing promise, it will reject it before creating
52+
* the new one. This allows debouncing the execution of the function.
3153
* @returns {Promise} Promise that will be resolved when the timeout is reached. The value this
3254
* promise will be resolved with is the return value of the `fn` function.
3355
*
3456
*/
35-
function timeout(fn, delay, invokeApply) {
57+
function timeout(fn, delay, invokeApply, debounce) {
3658
var deferred = $q.defer(),
3759
promise = deferred.promise,
3860
skipApply = (isDefined(invokeApply) && !invokeApply),
3961
timeoutId;
4062

63+
// debouncing support
64+
if (debounce && debounce.$$timeoutId in deferreds) {
65+
deferreds[debounce.$$timeoutId].reject('debounced');
66+
$browser.defer.cancel(debounce.$$timeoutId);
67+
}
68+
4169
timeoutId = $browser.defer(function() {
4270
try {
4371
deferred.resolve(fn());

test/ng/timeoutSpec.js

+30
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,34 @@ describe('$timeout', function() {
213213
expect(cancelSpy).toHaveBeenCalledOnce();
214214
}));
215215
});
216+
217+
218+
describe('debouncing', function() {
219+
it('should allow debouncing tasks', inject(function($timeout) {
220+
var task = jasmine.createSpy('task'),
221+
successtask = jasmine.createSpy('successtask'),
222+
errortask = jasmine.createSpy('errortask'),
223+
promise = null;
224+
225+
promise = $timeout(task, 10000, true, promise);
226+
promise.then(successtask, errortask);
227+
228+
expect(task).not.toHaveBeenCalled();
229+
expect(successtask).not.toHaveBeenCalled();
230+
expect(errortask).not.toHaveBeenCalled();
231+
232+
promise = $timeout(task, 10000, true, promise);
233+
expect(task).not.toHaveBeenCalled();
234+
expect(successtask).not.toHaveBeenCalled();
235+
expect(errortask).not.toHaveBeenCalled();
236+
237+
$timeout.flush();
238+
239+
expect(task).toHaveBeenCalled();
240+
// it's a different promise, so 'successtask' should not be called but 'errortask' should
241+
expect(successtask).not.toHaveBeenCalled();
242+
expect(errortask).toHaveBeenCalled();
243+
}));
244+
245+
});
216246
});

0 commit comments

Comments
 (0)