Skip to content

Commit 12bccbd

Browse files
committed
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 a61b65d commit 12bccbd

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
@@ -25,10 +25,32 @@ function $TimeoutProvider() {
2525
* In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
2626
* synchronously flush the queue of deferred functions.
2727
*
28+
* You can also use `$timeout` to debounce the call of a function using the returned promise
29+
* as the fourth parameter in the next call. See the following example:
30+
*
31+
* <pre>
32+
* var debounce;
33+
* var doRealSave = function() {
34+
* // Save model to DB
35+
* }
36+
* $scope.save = function() {
37+
* // debounce call for 2 seconds
38+
* debounce = $timeout(doRealSave, 2000, false, debounce);
39+
* }
40+
* </pre>
41+
*
42+
* And in the form:
43+
*
44+
* <pre>
45+
* <input type="text" ng-model="name" ng-change="save()">
46+
* </pre>
47+
*
2848
* @param {function()} fn A function, whose execution should be delayed.
2949
* @param {number=} [delay=0] Delay in milliseconds.
3050
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
3151
* will invoke `fn` within the {@link ng.$rootScope.Scope#methods_$apply $apply} block.
52+
* @param {Promise=} debounce If set to an outgoing promise, it will reject it before creating
53+
* the new one. This allows debouncing the execution of the function.
3254
* @returns {Promise} Promise that will be resolved when the timeout is reached. The value this
3355
* promise will be resolved with is the return value of the `fn` function.
3456
*
@@ -120,12 +142,18 @@ function $TimeoutProvider() {
120142
</doc:source>
121143
</doc:example>
122144
*/
123-
function timeout(fn, delay, invokeApply) {
145+
function timeout(fn, delay, invokeApply, debounce) {
124146
var deferred = $q.defer(),
125147
promise = deferred.promise,
126148
skipApply = (isDefined(invokeApply) && !invokeApply),
127149
timeoutId;
128150

151+
// debouncing support
152+
if (debounce && debounce.$$timeoutId in deferreds) {
153+
deferreds[debounce.$$timeoutId].reject('debounced');
154+
$browser.defer.cancel(debounce.$$timeoutId);
155+
}
156+
129157
timeoutId = $browser.defer(function() {
130158
try {
131159
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)