Implement a delay on $scope.$watch

All we need is an easy explanation of the problem, so here it is.

I was wondering whether or not it is possible to implement a slight delay on $scope.$watch. I have the following which queries the server, so I’d like to implement a slight delay before it evaluates the query before querying the server. I’ve noticed that if you type to quickly it gets confused and doesn’t send the correct information:

$scope.$watch("query", function () {
    $scope.loading = true;
    returnFactory.query($scope.query).then(function (returns) {
        $scope.returns = returns;
        $scope.loading = false;
    });
});

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

Normally i’d say use angular’s $timeout for this delay but you cant clear this timeout yet.

//EDIT:you can.

Set a timeout and clear it, if this watcher gets triggered fast enought.

Like this:

var timeoutCode;
var delayInMs = 2000;
$scope.$watch("query", function(query) {
 clearTimeout(timeoutCode);  //does nothing, if timeout alrdy done
 timeoutCode = setTimeout(function(){   //Set timeout
     $scope.loading = true;
     returnFactory.query(query).then(function(returns) {
       $scope.returns = returns;
       $scope.loading = false;
     });
 },delayInMs);
});

http://jsfiddle.net/4FuyY/

UPDATE Thanks to stewie
this can be achieved with angular’s $timeout.

    var timeoutPromise;
    var delayInMs = 2000;
    $scope.$watch("query", function(query) {
     $timeout.cancel(timeoutPromise);  //does nothing, if timeout alrdy done
     timeoutPromise = $timeout(function(){   //Set timeout
         $scope.loading = true;
         returnFactory.query(query).then(function (returns) {
           $scope.returns = returns;
           $scope.loading = false;
         });
     },delayInMs);
    });

Method 2

you can use ng-model-option, if the model ‘query’ is a Html tag or Angular directive, for Ej:

<input type ng-model="query" ng-model-options="{ updateOn: 'default blur', debounce: { 'default':
 2000, 'blur': 1 } }" />

You can see the Angular Doc here: https://docs.angularjs.org/api/ng/directive/ngModelOptions

Method 3

I like to use Lo-Dash which provides two really useful capabilities: debounce and throttle which does exactly what you want. Let’s say you want to make sure it only calls the function once per 150 ms:

function update() {
 $scope.loading = true;
    returnFactory.query($scope.query).then(function (returns) {
        $scope.returns = returns;
        $scope.loading = false;
    });
}

$scope.$watch("query", function () {
   _.throttle(update, 150);
});

The throttle function lets you control when the update function is called (trailing or leading edge).

I use Lo-Dash all the time in my app. It is a must-have library for me… more useful than jQuery. But, you can create a custom build of Lo-Dash which only includes the throttle and debounce functions if you don’t want to include the entire library.

Method 4

You can use the current value of query to decide when you want to fire the call:

$scope.$watch("query", function (value) {

    //implement rule here for value
    //example value is at least 3 characters
    if (value && value.length > 3) {

        $scope.loading = true;
        returnFactory.query($scope.query).then(function (returns) {
            $scope.returns = returns;
            $scope.loading = false;
        });
    }
});

Method 5

Just a snippet that I found useful for similar case:

function watchWithDelay(scope, prop, callback, delayMs) {
  delayMs = delayMs || 1000;
  var lastTimeChanged = new Date();
  scope.$watch(prop, function(n, o) {
    lastTimeChanged = new Date();
    setTimeout(function() {
      var diff = new Date().getTime() - lastTimeChanged.getTime();
      if (diff < delayMs-100 || diff > delayMs+100) {
        return;
      }
      callback(n, o);
    }, delayMs);
  });
}

You can use it in a controller like this:

watchWithDelay($scope, 'client.phone', function(n, o) {
  if (n === o) {
    return;
  }
  // any custom validations, for example
  if (!n) {
    return alert('Phone is required');
  }
  if (n.length < 11) {
    return alert('Phone is shorter than 11 digits');
  }
  // here I should save it somehow
  console.log('Phone is changed to ' + n);
});

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply