21

I'm using angularJS 1.4.8, yesterday i noticed that the $scope.$watch doesn't trigger on every change which caused bug in my application.

is there a way to force it to work on every change immediately ? like in this code, in every change on message i want the function in watch to trigger:

(function(){

  angular.module('myApp', [])
  .controller('myAppController', myAppController)

  function myAppController($scope){
    console.log('controller loading !');
    $scope.message = 'message1';

    $scope.$watch('message', function(newMessage){
      console.log('newMessage', newMessage)
    });

    function changeMessage(){
      $scope.message='hi';
      $scope.message='hi12';
    }

    changeMessage(); 
  }

})();

the console will print:

controller loading !
newMessage hi22

plunker link https://plnkr.co/edit/SA1AcIVwr04uIUQFixAO?p=preview

edit: I would really like to know if there are any other ways than wrapping the change with timeout and using scope apply, in my original code iv'e multiple places where i change the scope property and i would like to avoid using this every change.

Sahar Sabin
  • 227
  • 2
  • 10
  • 1
    check [this](https://stackoverflow.com/a/44521627/3551786) – Durga Sep 05 '17 at 06:39
  • @Durga iv'e multiple place where i change my object in my original code,is there any other way than wrap everything in timeout with scope apply? – Sahar Sabin Sep 05 '17 at 06:42
  • I think is because the `$watch` function only activates after angular has finished compiling. So when ever you change the `$scope`, after the controller is initialized, the `$watch` will trigger. – Red Sep 05 '17 at 06:46
  • you need that only for `message` change in `scope` to trigger a listener? – Koushik Chatterjee Sep 05 '17 at 06:51
  • 1
    Possible duplicate of [Why is $watch not firing](https://stackoverflow.com/questions/27441460/why-is-watch-not-firing) – Ramesh Rajendran Sep 05 '17 at 06:52

5 Answers5

11

This happens because the watch will only be triggered if the value is changed "between" digest loops.

Your function is changing the message value on the scope in the same function. This will be executed in the same digest loop. When angular moves on to the next loop it will only see the last changed value which in your case will be hi22.

Here's a great article which makes this behaviour clear

Marcus Höglund
  • 16,172
  • 11
  • 47
  • 69
6

update your changeMessage function so that it uses $scope.$apply function which will ensure that your changes are reflected and angular is aware of your changes to the variable.

changeMessage() {
   setTimeout(function () {
        $scope.$apply(function () {
          $scope.message = "Timeout called!";
        });
    }, 2000);
}
Ajinkya Dhote
  • 1,389
  • 3
  • 13
  • 26
  • iv'e multiple place where i change my object in my original code,is there any other way than wrap everything in timeout with scope apply? – Sahar Sabin Sep 05 '17 at 06:43
  • 1
    warping your code in scope apply is best practice as it ensure that the your changes are updated, It is not at all recommended to skip this. As said by you, in your code were your are having multiple instances were your object is being modified, it becomes an ideal way for you to this, i would suggest you to write a function which does this task. you can read more about scope apply and why it is useful to use here http://jimhoskins.com/2012/12/17/angularjs-and-apply.html – Ajinkya Dhote Sep 05 '17 at 06:48
  • 2
    Why do you need `setTimeout` with `$apply`? You can do the same thing with `$timeout` without `$apply` or vice versa. `$apply` is running `$digest` and `$timeout` do the same thing at the end. You can read about it here: https://www.codingeek.com/angularjs/angular-js-apply-timeout-digest-evalasync/ – Kindzoku Sep 05 '17 at 07:13
  • 1
    @Kindzoku we need setTimeout with apply because if we do not use the setTimeout then chances are high that the current digest cycle is in progress and we might be changing our variable while it is in progress which will eventually lead to error. Using setTimeout ensure that our chnages are always made after the current digest cycle is complete. – Ajinkya Dhote Sep 05 '17 at 07:20
  • 2 seconds delay won't guarantee that $digest not in progress. You can use `$$phase` for this. But `$$` is not recommended. I just don't get it, how 2 seconds delay will solve this. setTimeout not wrapped by angular. Angular knows nothing about setTimeout and $digest loop is not connected to it by any means. – Kindzoku Sep 05 '17 at 07:25
  • 1
    @Kindzoku, you are right 2 sec delay wont guaranty that $digest is in progress, and it that nothing to do with 2 sec it can even be 100 ms also, that fact that this works gracefully is that angular will only process the code wrap in setTimeout after the completion of current $digest cycle – Ajinkya Dhote Sep 05 '17 at 07:30
  • `setTimeout` is `async`. It can guarantee nothing and it's not connected to `$digest` by any means. `setTimeout` with `$apply` inside guarantee only the fact that `$apply` will shoot after time mentioned in `setTimeout`. It is not related to `$digest`. And `$timeout` do the same thing after the hood. – Kindzoku Sep 05 '17 at 08:14
  • 2
    This is really not a good way to apply changes to the `$scope`. Much better would be to use the build in `$timeout` function. – Red Sep 05 '17 at 11:35
  • "warping your code in scope apply is best practice" this is so wrong. When do you use $apply? From outside the scope/digest loop...Were are we? In the scope/digest loop... – Marcus Höglund Sep 05 '17 at 12:36
4

If you change value into the same digest cycle the watcher is not triggered and last value is taken. When we run $timeout, we change $scope.message value in next digest cycle and watcher catches it as expected.

Take look on simple test:

 $scope.$watch(function(){
  console.log('trigger');
  return $scope.message;
},
  function(newMessage){
  console.log('newMessage', newMessage)
});

function changeMessage(){
  $scope.message='hi';

  $timeout(function(){
    $scope.message='hi12';
  });      
}

Output:

controller loading !
 trigger
 newMessage hi
 trigger
 trigger
 newMessage hi12
 trigger
Maxim Shoustin
  • 77,483
  • 27
  • 203
  • 225
3

There is no need to wrap changeMessage in setTimeout and $apply at the same time. If you need to skip some time before execution, just use:

function changeMessage(){
    $timeout(function(){
        $scope.message = 'message';
    }/* or add time here, doesn't matter */);
}

Or just:

function changeMessage(){
    $scope.message = 'message';
    $scope.$apply();
}

Both methods calls $rootScope.$digest in the end. Here is more information: https://www.codingeek.com/angularjs/angular-js-apply-timeout-digest-evalasync/

Kindzoku
  • 1,368
  • 1
  • 10
  • 29
2

$watch() only triggers between every $digest().

Detailed explaination about the $apply() and $digest()

In your case you keep updating the $scope.message in the current $digest() cycle.

You could change that by applying each new value to the $scope using $apply(). Like @Ajinkya wrote. The only problem, with setting 2000ms as timeout, doesn't allways ensure it executes after the $digest(). On top of that, Angular has a build in timeout function. See below.

(function(){
  
  angular.module('myApp', [])
  .controller('myAppController', myAppController)
  
  function myAppController($scope, $timeout){
    console.log('controller loading !');
    $scope.message = 'message1';
    
    $scope.$watch('message', function(newMessage){
      console.log('newMessage', newMessage)
    });
    
    function changeMessage(){
    setTimeout(function () {
        $scope.$apply(function () {
          $scope.message='hi12';
        });
    }, 2000);
      
    }
    
    changeMessage(); 
  }
  
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

<div ng-app="myApp" ng-controller="myAppController"></div>

Solution

The best way would be to call the build in $timeout function, without setting the time in milliseconds.

This way, angular allways ensures the $timeout will run after the latest $digest(). On top of that. You dont have to use the $scope.$apply(). Because the $timeout allready runs a $digest(), where $scope.$apply() is manually invoking a new $diggest() cycle.

(function(){
  
  angular.module('myApp', [])
  .controller('myAppController', myAppController)
  
  function myAppController($scope, $timeout){
    console.log('controller loading !');
    $scope.message = 'message1';
    
    $scope.$watch('message', function(newMessage){
      console.log('newMessage', newMessage)
    });
    
    function changeMessage(){
        $timeout(function () {
            $scope.message='hi12';
        });
      
    }
    
    changeMessage(); 
  }
  
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

    <div ng-app="myApp" ng-controller="myAppController"></div>
Red
  • 6,599
  • 9
  • 43
  • 85