From 1f9d7305fcd7cd520f9cc3bdbd26166a8e4e807f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Ram=C3=B3n=20L=C3=B3pez?= Date: Sun, 5 May 2013 02:18:28 +0200 Subject: [PATCH] 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. --- src/ng/timeout.js | 30 +++++++++++++++++++++++++++++- test/ng/timeoutSpec.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/ng/timeout.js b/src/ng/timeout.js index 33a4dcde81ed..0986bb685ae3 100644 --- a/src/ng/timeout.js +++ b/src/ng/timeout.js @@ -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: + * + *
+      *   var debounce;
+      *   var doRealSave = function() {
+      *      // Save model to DB
+      *   }
+      *   $scope.save = function() {
+      *      // debounce call for 2 seconds
+      *      debounce = $timeout(doRealSave, 2000, false, debounce);
+      *   }
+      * 
+ * + * And in the form: + * + *
+      *   
+      * 
+ * * @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()); diff --git a/test/ng/timeoutSpec.js b/test/ng/timeoutSpec.js index 97c8448eedce..6747865cc2b7 100644 --- a/test/ng/timeoutSpec.js +++ b/test/ng/timeoutSpec.js @@ -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(); + })); + + }); });