54

I have the following which does a watch on an <input> field that's bound to $scope.id. Every time the input field value changes the watch function gets executed:

$scope.$watch("id", function (id) {

   // code that does something based on $scope.id

});

Is there a way I can put a timeout on this or debounce this with _lodash so that the code does not execute on each keypress while the user is changing the value.

What I would like is for a delay of one second so that after the user has stopped typing for one second then the code block inside the watch runs. Note that the input value is something that could change at any time. For example I need the function to be called if the value is "1" or "10" or "1000". This is something similar to the way the search box with suggestions works in Google. If the user types in 999 then I need the function to be called. If he deletes a 9 so it's 99 then I need the function to be called.

I do have _lodash available so a solution that uses that might be the best fit for my needs.

George Stocker
  • 57,289
  • 29
  • 176
  • 237

4 Answers4

80

You can use ngModelOptions in Angular 1.3.0

HTML:

<div ng-controller="Ctrl">
  <form name="userForm">
    Name:
    <input type="text" name="userName"
           ng-model="user.name"
           ng-model-options="{ debounce: 1000 }" />
    <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button><br />
  </form>
  <pre>user.name = <span ng-bind="user.name"></span></pre>
</div>

More Info: https://docs.angularjs.org/api/ng/directive/ngModelOptions

AhmedRiyad
  • 1,018
  • 9
  • 9
69

Is that what are you looking for?

$scope.$watch("id", _.debounce(function (id) {
    // Code that does something based on $scope.id
    // This code will be invoked after 1 second from the last time 'id' has changed.
}, 1000));

Note, however, that if you want to change $scope inside that function you should wrap it $scope.$apply(...) as unless _.debounce function uses $timeout internally (which as far as I understand it doesn't do) Angular will not be aware of the changes you did on the $scope.

UPDATE

As to the updated question - yes you need to wrap the entire callback function body with

$scope.$apply():

$scope.$watch("id", _.debounce(function (id) {
    // This code will be invoked after 1 second from the last time 'id' has changed.
    $scope.$apply(function(){
        // Code that does something based on $scope.id
    })
}, 1000));
T J
  • 42,762
  • 13
  • 83
  • 138
Alex Vayda
  • 6,154
  • 5
  • 34
  • 50
  • Wajda - Yes I think this is what is needed but I am not sure if I should use debounce or throttle? I tried to look for examples out there but there are not many. What do you think and would throttle achieve the same thing here? –  Jan 13 '14 at 11:54
  • 2
    The difference between `debounce` and `throttle` can be describe with an elevator analogy. If several people enter the elevator it will not run unless it is given enough time to close the doors - this is 'debounce'. If the elevator is small and there are too many people wants to enter it must run eventually (say with 5 people at max) - this is 'throttle' – Alex Vayda Jan 13 '14 at 12:07
  • I updated the question to show your suggestion. Should I just wrap everything inside $scope.$apply() or could I run a $scope.$apply() at the end of my code and within the function? –  Jan 13 '14 at 12:08
  • So if you want to wait forever while the user is typing use `debounce'. If you want the actions to be performed after some maximum delay even if the user didn't stop typing - use `throttle` – Alex Vayda Jan 13 '14 at 12:09
  • I've updated my answer - yes, it's better to wrap the code with $apply. It is recommended approach. Calling $apply() in the end will only work if no exceptions is thrown from the code block and your $apply() is actually called. Wrapping the code with $apply() is safe in this respect - Angualar will be aware of whatever happens in your code. – Alex Vayda Jan 13 '14 at 12:23
  • I have this return: debounce is not defined – Marcelo Aymone Oct 15 '15 at 14:12
34

I know the question asks for a lodash solution. Anyway here is an angular only solution:

app.factory('debounce', function($timeout) {
    return function(callback, interval) {
        var timeout = null;
        return function() {
            $timeout.cancel(timeout);
            var args = arguments;
            timeout = $timeout(function () { 
                callback.apply(this, args); 
            }, interval);
        };
    }; 
}); 

In the controller:

app.controller('BlaCtrl', function(debounce) {

    $scope.$watch("id", debounce(function (id) {
        ....
    }, 1000));

});
S. Buda
  • 727
  • 7
  • 27
jdachtera
  • 749
  • 1
  • 6
  • 11
  • 3
    But, shouldn't it be more like `var args = arguments; timeout = $timeout(function () { callback.apply(this, args); }, interval);` ? The id parameter would be undefined with your code and completely lost. – Jirka Helmich Jun 19 '15 at 12:39
  • Yep, you're right, thanks. In simple watches you could just get the value from the scope, but of course it's much better to pass the arguments to the callback. – jdachtera Jul 02 '15 at 09:16
6

You can encapsulate this in a directive. Source: https://gist.github.com/tommaitland/7579618

<input type="text" ng-model="id" ng-debounce="1000">

Javascript

app.directive('ngDebounce', function ($timeout) {
  return {
      restrict: 'A',
      require: 'ngModel',
      priority: 99,
      link: function (scope, elm, attr, ngModelCtrl) {
          if (attr.type === 'radio' || attr.type === 'checkbox') {
              return;
          }

          var delay = parseInt(attr.ngDebounce, 10);
          if (isNaN(delay)) {
              delay = 1000;
          }

          elm.unbind('input');

          var debounce;
          elm.bind('input', function () {
              $timeout.cancel(debounce);
              debounce = $timeout(function () {
                  scope.$apply(function () {
                      ngModelCtrl.$setViewValue(elm.val());
                  });
              }, delay);
          });
          elm.bind('blur', function () {
              scope.$apply(function () {
                  ngModelCtrl.$setViewValue(elm.val());
              });
          });
      }
  };
});
jessegavin
  • 74,067
  • 28
  • 136
  • 164