14

Angular's $watch function allows events to be fired when the specified attribute changes, as shown below. Is there is a similar way to listen for events when any change happens on the scope?

// works
$scope.$watch("someval", function() { }, true);
$scope.$watch(function(scope){ return scope.someval; }, function() { }, true);

// doesn't work
$scope.$watch("this", function() { }, true);
$scope.$watch(function(scope){ return scope; }, function() { }, true);
Luke Dennis
  • 14,212
  • 17
  • 56
  • 69
  • 2
    I would suggest to put all properties that can change into some object and make it watchable property of $scope. It is not a solution, but it works and may be will make you scope more structured/maintainable (is you can pass that object to another function and not whole scope that can even lead to memory leaks. – Valentyn Shybanov Jan 27 '13 at 09:29
  • @ValentynShybanov: That is a good approach, but I would then have to prefix every single bound value ("container.someval", etc). Is there an easy way to create child scopes from within markup? (See related question: http://stackoverflow.com/questions/14546039/is-there-a-shorthand-in-angularjs-for-creating-child-scopes-in-markup) – Luke Dennis Jan 27 '13 at 09:50

4 Answers4

24

I believe you cannot watch for changes on the $scope (apart from using @ValentynShybanov's sugestion). The closest thing you can do is to watch for digest calls:

$scope.$watch(function() { 
    console.log("digest called"); 
});

The above function will be called each time a $digest is invoked on the scope, even if there were no changes on the scope's properties.

bmleite
  • 26,850
  • 4
  • 71
  • 46
9

That is not straightforward, cause the scope is a plain object. That is Backbone where you can subscribe to all events, but for that your models should extend a proprietary interface - Backbone.Model. Angular puts aside the observer pattern in it's classical look. That is an unusual design decision. Basically there is a loop where Angular evaluates expressions being watched and when it detects a difference from the previous iteration it notifies watchers. A really interesting approach, probably the only alternative to the observer pattern (except perhaps reactive programming, which is not common yet).

I propose to use as a watch expression a function, which returns a hash of the scope. Unfortunately scopes contain circular references somewhere so quick solutions like JSON.stringify($scope) don't work. I wrote quick and dirty proof of concept. http://jsfiddle.net/ADukg/2544. Whatever textfield you type in it prints to the console. String hash function is taken from here Generate a Hash from string in Javascript/jQuery.

function MyCtrl($scope) {
    $scope.$watch(
        function($scope) {return hash($scope);},
        function() {console.log("changed");}
    );
}
Community
  • 1
  • 1
Yaroslav
  • 4,543
  • 5
  • 26
  • 36
  • I should have skip angular properties while calculating object hashes. – Yaroslav Apr 26 '13 at 19:18
  • 3
    You can use `angular.toJson` instead of `JSON.stringify` to do that. – r3m0t Feb 04 '14 at 14:41
  • I prefixes my scope properties with a name, so it's easier to calculate the hash without any conflict, it typescript it gives something like `$scope.$watch(() => angular.toJson($scope.hypothesis,false),() => { /*do whatever you need*/ });` – Vincent May 04 '14 at 20:49
  • I think that using angular.toJson on scope will only generate the string '$SCOPE' thus making this solution irrelevant. in which case the right approach would be to do a custom `JSON.stringify` as suggested. – guy mograbi Sep 27 '14 at 21:00
3

I ran into this problem myself, and I solved it with something similar to @ValentynShybanov's comment but not quite the same. my solution does not require you to prefix all properties as you stated in the comment.

see a working plunkr

var entireScope = {};
  for ( var i in $scope ){
    if ( i[0] !== '$' && i !== 'this' && i !== 'constructor'){
      console.log(i);
      entireScope[i] = $scope[i];
    }
  }

  $scope.entireScope = entireScope;

  $scope.$watch('entireScope', function( newValue, oldValue ){
   $log.info('entireScope has changed',newValue, oldValue);
  }, true);

as you see the solution is quite the same, but in fact it adds another property to the scope rather than replacing the structure you already have. this new property represents the entire scope (hence its name) and you can watch it.

the loop I wrote might be too generic, you can replace it with a customized initialization. for example:

$scope.entireScope = { 'someVal' : $scope.someVal }

this solution answers your needs and spares you the need to refactor the code.

guy mograbi
  • 27,391
  • 16
  • 83
  • 122
2

A easy solution could be that you should check a variable of the scope only but it could be and object.

app.js

app.controller('MainController', ['$scope', function($scope){
   $scope.watchable = {
       'param1':'',
       'param2':''
   };
   $scope.update = -1;
   $scope.$watch('watchable', function(scope){
       scope.update++;
   }, true);

});

index.html

<div ng-controller="MainController">
    <input type='text' ng-model="watchable.param1">
    <input type='text' ng-model="watchable.param2">
    <p>param1:{{watchable.param1}}, param2:{{watchable.param2}}</p>
    <p>update:{{update}}
</div>

The trick here is to pass 'true' in the third parameter in the $watch function which will do a angular.equal check which is check by value rather than a simple reference check. Please let me know if its correct.

Tejaswi Sharma
  • 148
  • 1
  • 9