Search code examples
javascriptangularjsangularjs-scopeangularjs-service

Issues binding / watching service variable shared between two controllers


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');
        }
    });
})

})();


Solution

  • 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.