324

I want to watch for changes in a dictionary, but for some reason watch callback is not called.

Here is a controller that I use:

function MyController($scope) {
    $scope.form = {
        name: 'my name',
        surname: 'surname'
    }

    $scope.$watch('form', function(newVal, oldVal){
        console.log('changed');
    });
}

Here is fiddle.

I expect $watch callback to be fired each time name or surname is changed, but it doesn't happen.

What is the correct way to do it?

Prashant Pokhriyal
  • 3,727
  • 4
  • 28
  • 40
Vladimir Sidorenko
  • 4,265
  • 3
  • 19
  • 13
  • 1
    possible duplicate of [How to deep watch an array in angularjs?](http://stackoverflow.com/questions/14712089/how-to-deep-watch-an-array-in-angularjs) – lort Oct 18 '13 at 20:58

8 Answers8

583

Call $watch with true as the third argument:

$scope.$watch('form', function(newVal, oldVal){
    console.log('changed');
}, true);

By default when comparing two complex objects in JavaScript, they will be checked for "reference" equality, which asks if the two objects refer to the same thing, rather than "value" equality, which checks if the values of all the properties of those objects are equal.

Per the Angular documentation, the third parameter is for objectEquality:

When objectEquality == true, inequality of the watchExpression is determined according to the angular.equals function. To save the value of the object for later comparison, the angular.copy function is used. This therefore means that watching complex objects will have adverse memory and performance implications.

rossipedia
  • 56,800
  • 10
  • 90
  • 93
  • 2
    This is a good answer, but I imagine there are times when you wouldn't want to recursively watch an object – Jason Oct 18 '13 at 17:26
  • 16
    There are. By default if you don't pass `true`, then it will check for reference equality if the watched value is an object or array. In that case, you should specify the properties explicitly, like `$scope.$watch('form.name')` and `$scope.$watch('form.surname')` – rossipedia Oct 18 '13 at 17:29
  • 3
    Thanks. That's exactly what I missed. Jason, you're right - you don't always want recursive objects watching, but in my case it was exactly what I needed. – Vladimir Sidorenko Oct 18 '13 at 18:17
  • 1
    ImmutableJS and reference comparison may help increase the performance. – Dragos Rusu Dec 13 '15 at 21:49
14

The form object isn't changing, only the name property is

updated fiddle

function MyController($scope) {
$scope.form = {
    name: 'my name',
}

$scope.changeCount = 0;
$scope.$watch('form.name', function(newVal, oldVal){
    console.log('changed');
    $scope.changeCount++;
});
}
Prashant Pokhriyal
  • 3,727
  • 4
  • 28
  • 40
Jason
  • 15,915
  • 3
  • 48
  • 72
12

Little performance tip if somebody has a datastore kind of service with key -> value pairs:

If you have a service called dataStore, you can update a timestamp whenever your big data object changes. This way instead of deep watching the whole object, you are only watching a timestamp for change.

app.factory('dataStore', function () {

    var store = { data: [], change: [] };

    // when storing the data, updating the timestamp
    store.setData = function(key, data){
        store.data[key] = data;
        store.setChange(key);
    }

    // get the change to watch
    store.getChange = function(key){
        return store.change[key];
    }

    // set the change
    store.setChange = function(key){
        store.change[key] = new Date().getTime();
    }

});

And in a directive you are only watching the timestamp to change

app.directive("myDir", function ($scope, dataStore) {
    $scope.dataStore = dataStore;
    $scope.$watch('dataStore.getChange("myKey")', function(newVal, oldVal){
        if(newVal !== oldVal && newVal){
            // Data changed
        }
    });
});
Ferenc Takacs
  • 597
  • 7
  • 8
  • 1
    I think it's worth mentioning that in this case you'd loose the functionality of having access to old value of the data you're storing. `newVal`, `oldVal` in this example are the timestamp values not the observed objects values. It doesn't make this answer invalid, I just think that it's a drawback that is worth mentioning. – Michał Schielmann Nov 14 '16 at 14:41
  • True, this method i mostly use when i want to load a complicated data structure as a source for something (a new version of some data for example). If i care about new and old values i wouldn't store them in big complicated objects, rather i would have a small text representation of the thing that i care about and watch that for change or get it when the timestamp changes. – Ferenc Takacs Nov 16 '16 at 13:58
6

The reason why your code doesn't work is because $watch by default does reference check. So in a nutshell it make sure that the object which is passed to it is new object. But in your case you are just modifying some property of form object not creating a new one. In order to make it work you can pass true as the third parameter.

$scope.$watch('form', function(newVal, oldVal){
    console.log('invoked');
}, true);

It will work but You can use $watchCollection which will be more efficient then $watch because $watchCollection will watch for shallow properties on form object. E.g.

$scope.$watchCollection('form', function (newVal, oldVal) {
    console.log(newVal, oldVal);
});
sol4me
  • 15,233
  • 5
  • 34
  • 34
4

As you are looking for form object changes, the best watching approach is to use
$watchCollection. Please have a look into official documentation for different performance characteristics.

Prashant Pokhriyal
  • 3,727
  • 4
  • 28
  • 40
Ramesh Papaganti
  • 7,311
  • 3
  • 31
  • 36
1

Try this:

function MyController($scope) {
    $scope.form = {
        name: 'my name',
        surname: 'surname'
    }

    function track(newValue, oldValue, scope) {
        console.log('changed');
    };

    $scope.$watch('form.name', track);
}
Ilia Choly
  • 18,070
  • 14
  • 92
  • 160
Tarun
  • 11
  • 1
0

For anyone that wants to watch for a change to an object within an array of objects, this seemed to work for me (as the other solutions on this page didn't):

function MyController($scope) {

    $scope.array = [
        data1: {
            name: 'name',
            surname: 'surname'
        },
        data2: {
            name: 'name',
            surname: 'surname'
        },
    ]


    $scope.$watch(function() {
        return $scope.data,
    function(newVal, oldVal){
        console.log(newVal, oldVal);
    }, true);
Maximillion Bartango
  • 1,533
  • 1
  • 19
  • 34
-3

you must changes in $watch ....

function MyController($scope) {
    $scope.form = {
        name: 'my name',
    }

    $scope.$watch('form.name', function(newVal, oldVal){
        console.log('changed');
     
    });
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.22/angular.min.js"></script>
<div ng-app>
    <div ng-controller="MyController">
        <label>Name:</label> <input type="text" ng-model="form.name"/>
            
        <pre>
            {{ form }}
        </pre>
    </div>
</div>
  • 8
    Answering a two-year old question with an answer that's an almost exact duplicate of an existing one? Not cool. – Jared Smith Jan 06 '16 at 13:33