2

I am having a really hard time deciphering what is going on here. I understand the basics of Angular's $digest cycle, and according to this SO post, I am doing things correctly by simply assigning a scoped var to a service's property (an array in this case). As you can see the only way I can get CtrlA's 'things' to update is by re-assigning it after I've updated my service's property with a reference to a new array.

Here is a fiddle which illustrates my issue:

http://jsfiddle.net/tehsuck/Mujun/

(function () {
angular.module('testApp', [])
    .factory('TestService', function ($http) {
    var service = {
        things: [],
        setThings: function (newThings) {
            service.things = newThings;
        }
    };
    return service;
})
    .controller('CtrlA', function ($scope, $timeout, TestService) {
    $scope.things = TestService.things;
    $scope.$watch('things.length', function (n, o) {
        if (n !== o) {

            alert('Things have changed in CtrlA');
        }
    });
    $timeout(function () {
        TestService.setThings(['a', 'b', 'c']);

        // Without the next line, CtrlA acts like CtrlB in that
        // it's $scope.things doesn't receive an update
        $scope.things = TestService.things;
    }, 2000);
})
    .controller('CtrlB', function ($scope, TestService) {
    $scope.things = TestService.things;
    $scope.$watch('things.length', function (n, o) {
        if (n !== o) {
            // never alerts
            alert('Things have changed in CtrlB');
        }
    });
})

})();

Community
  • 1
  • 1
Chief
  • 952
  • 8
  • 15

2 Answers2

4

There are two issues with your code:

  1. Arrays don't have a count property; you should use length instead.

    $scope.$watch('things.length', ...);
    

    But there's a caveat: if you add and remove elements to/from the things array and end up with a different list with the same length then the watcher callback won't get triggered.

  2. The setThings method of TestService replaces the reference to the things array with a new one, making TestService.things point to a new array in memory while both CtrlA.$scope.things and CtrlB.$scope.things remain pointing to the old array, which is empty. The following code illustrates that:

    var a = [];
    var b = a;
    
    a = [1, 2, 3];
    console.log(a); // prints [1, 2, 3];
    console.log(b); // prints [];
    

    So in order for you code to work you need to change the way TestService.setThings updates its things array. Here's a suggestion:

    setThings: function (newThings) {
        service.things.length = 0; // empties the array
        newThings.forEach(function(thing) { 
            service.things.push(thing);
        });                
    

    }

And here's a working version of your jsFiddle.

Michael Benford
  • 13,826
  • 2
  • 59
  • 59
  • :smacks head: Ok, well I attribute the Array.count thing to trying to write this up quickly this AM. Sorry. – Chief Aug 18 '13 at 18:16
  • I see what you're saying about the array reference, and I guess that's what I don't understand. The way I understood it, during $digest I thought angular would compare the reference if the data-binding was an Object or Array? I guess I am wrong. Thanks though! – Chief Aug 18 '13 at 18:23
  • It's not an Angular issue. When you make `TestService.things` point to another array, `CtrlA.$scope.things` remains pointing to the old one, which is empty, and Angular renders it accordingly. As far as Angular knows, nothing has changed with `CtrlA.$scope.things`. – Michael Benford Aug 18 '13 at 21:34
  • Right,sorry - I didn't mean it was an NG issue, it was just me not understanding how NG's $digest / $watch work. Thanks. – Chief Aug 18 '13 at 21:59
  • 1
    You can also implement `setThings` with the following line of code: `angular.copy(newThings, service.things)` – Mark Rajcok Aug 19 '13 at 15:10
1

I don't really know why, but it seems to be corrected if you use a function to return the data in your service, and then you watch that function instead of the property. As it seems unclear, you can see it here : http://jsfiddle.net/DotDotDot/Mujun/10/
I added a getter in your service :

var service = {
        things: [],
        setThings: function (newThings) {
            service.things = newThings;
        },
        getThings:function(){
             return service.things;   
        }
    };

then, I modified your code in both controller by this :

    $scope.things = TestService.getThings();
    $scope.getThings=function(){return TestService.getThings();};
    $scope.$watch('getThings()', function (n, o) {
        if (n !== o) {
            // never alerts
            alert('Things have changed in CtrlA');
        }
    }, true);

and in the HTML :

<li ng-repeat="thing in getThings()">{{thing}}</li>

It defines a function getThings, which will simply get the property in your service, then I watch this function (AFAIK $watch do an eval on the parameter, so you can watch functions), with a deep inspection ( the true parameter at the end). Same thing in your other controller. Then, when you modifies the value of your service, it is seen by the two $watchers, and the data is binded correctly

Actually, I don't know if it's the best method, but it seems to work with your example, so I think you can look in this way

Have fun :)

DotDotDot
  • 3,548
  • 2
  • 17
  • 22