Skip to content

Commit

Permalink
feat($timeout): Add debouncing support to $timeout service
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
lrlopez committed Mar 7, 2014
1 parent 186a68f commit 1f9d730
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 1 deletion.
30 changes: 29 additions & 1 deletion src/ng/timeout.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,48 @@ function $TimeoutProvider() {
* In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
* synchronously flush the queue of deferred functions.
*
* You can also use `$timeout` to debounce the call of a function using the returned promise
* as the fourth parameter in the next call. See the following example:
*
* <pre>
* var debounce;
* var doRealSave = function() {
* // Save model to DB
* }
* $scope.save = function() {
* // debounce call for 2 seconds
* debounce = $timeout(doRealSave, 2000, false, debounce);
* }
* </pre>
*
* And in the form:
*
* <pre>
* <input type="text" ng-model="name" ng-change="save()">
* </pre>
*
* @param {function()} fn A function, whose 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 ng.$rootScope.Scope#$apply $apply} block.
* @param {Promise=} debounce If set to an outgoing promise, it will reject it before creating
* the new one. This allows debouncing the execution of the function.
* @returns {Promise} 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) {
function timeout(fn, delay, invokeApply, debounce) {
var deferred = $q.defer(),
promise = deferred.promise,
skipApply = (isDefined(invokeApply) && !invokeApply),
timeoutId;

// debouncing support
if (debounce && debounce.$$timeoutId in deferreds) {
deferreds[debounce.$$timeoutId].reject('debounced');
$browser.defer.cancel(debounce.$$timeoutId);
}

timeoutId = $browser.defer(function() {
try {
deferred.resolve(fn());
Expand Down
30 changes: 30 additions & 0 deletions test/ng/timeoutSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,34 @@ describe('$timeout', function() {
expect(cancelSpy).toHaveBeenCalledOnce();
}));
});


describe('debouncing', function() {
it('should allow debouncing tasks', inject(function($timeout) {
var task = jasmine.createSpy('task'),
successtask = jasmine.createSpy('successtask'),
errortask = jasmine.createSpy('errortask'),
promise = null;

promise = $timeout(task, 10000, true, promise);
promise.then(successtask, errortask);

expect(task).not.toHaveBeenCalled();
expect(successtask).not.toHaveBeenCalled();
expect(errortask).not.toHaveBeenCalled();

promise = $timeout(task, 10000, true, promise);
expect(task).not.toHaveBeenCalled();
expect(successtask).not.toHaveBeenCalled();
expect(errortask).not.toHaveBeenCalled();

$timeout.flush();

expect(task).toHaveBeenCalled();
// it's a different promise, so 'successtask' should not be called but 'errortask' should
expect(successtask).not.toHaveBeenCalled();
expect(errortask).toHaveBeenCalled();
}));

});
});

2 comments on commit 1f9d730

@petebacondarwin
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this is quite efficient, I find it is not as intuitive from the developer's point of view as just adding a new debounce wrapper.

@petebacondarwin
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it is just the way it is described in the documentation and commit message.

Please sign in to comment.