Search code examples
javascriptangularjsangularjs-directiveangularjs-scope2-way-object-databinding

Angular binding is not working in custom directive for local scope variables calculated using passed data


In above example when I change the value array passed to directive from controller, all the changes gets reflected in the directive html. I mean I can see the changes in the UI.

But changes in the value of $scope.message variable doesn’t get reflected, even though the value of $scope.message is being calculated from the value of $scope.myData, whose value is getting changed using $timeout in the parent controller To see those changes in $scope.message, you need to watch the array using $watchCollection. My questions are,

  1. Why angular’s binding is not working for $scope.myData normally?
  2. What are other “known” corner cases where angular’s binding doesn’t work?

Below is the code snippet

(function(){
    angular.module("csjoshi04.2waybinding",[])
            .controller("ParentCtrl",["$scope", "$timeout", function($scope, $timeout){
                $scope.myCars = ["Ford", "BMW", "Toyata"];
                $timeout(function(){
                    $scope.myCars.push("Honda");
                }, 3000);
            }])
            .directive("showMyData",function(){
                return {
                    restrict: "E",
                    scope: {
                        myData : "="
                    },
                    controller : ["$scope", function($scope){
                        $scope.message = ($scope.myData.indexOf("Honda") > -1 && $scope.myData.length >= 4) ? "1 out of 4 cars is always Honda": "OOPS, no honda cars";
                    }],
                    template : '<div>{{message}}</div><ul ng-repeat="data in myData"><li>{{data}}</li></ul>'
                }
            })
})()

Below is html

<body ng-controller="ParentCtrl"><show-my-data my-data="myCars" ></show-my-data></body>

To make above directive work, I made below changes

directive("showMyData",function(){
                return {
                    restrict: "E",
                    scope: {
                        myData : "="
                    },
                    controller : ["$scope", function($scope){
                        $scope.message = ($scope.myData.indexOf("Honda") > -1 && $scope.myData.length >= 4) ? "1 out of 4 cars is always Honda": "OOPS, no honda cars";
                        $scope.$watchCollection(function(){
                            return $scope.myData;
                        }, function(new1, old){
                            $scope.message = ($scope.myData.indexOf("Honda") > -1 && $scope.myData.length >= 4) ? "1 out of 4 cars is always Honda": "OOPS, no honda cars";
                        });
                    }],
                    template : '<div>{{message}}</div><ul ng-repeat="data in myData"><li>{{data}}</li></ul>'
                }
            })

here is the link to plunkr.

plunker


Solution

  • I wouldn't call this a "corner case" of the data binding in angular.

    Angular's data-binding works by continuously checking the values attached to $scope. It's cool but it's not magic, in an imperative language like JavaScript statement $scope.message = condition ? "message": "another message"; will not in itself suggest that the expression would be re-evaluated ever again. Angular initializes the controller only once per attachment to template and that is exactly how many times the statement in the above example will get evaluated. Once. After that it's just another $scope variable whose value is either "message" or "another message" and that's how it's going to stay if we don't change it ourself.

    You have identified the problem yourself. The value of $scope.message depends on $scope.myData but without the $watchCollection there is nothing to keep this linkage up to date. $watchCollection, on the other hand, will keep re-evaluating first the $scope.myData; and then the $scope.message = ... functions because that's what angular makes it to do.

    Personally, though, I'd do it with a function getter instead of assigning the value to $scope and manually keeping it up to date. That's closer to what you were originally trying to do anyway.

    .directive("showMyData",function(){
        return {
            restrict: "E",
            scope: {
               myData : "="
            },
            controller : ["$scope", function($scope){
                $scope.getMessage = function(){
                    return ($scope.myData.indexOf("Honda") > -1 && $scope.myData.length >= 4) ? "1 out of 4 cars is always Honda": "OOPS, no honda cars";
                };    
            }],
            template : '<div>{{getMessage()}}</div><ul ng-repeat="data in myData"><li>{{data}}</li></ul>'
         }
    })
    

    Now that we are using a function call instead of a simple value, the expression that creates the message gets re-evaluated over and over again, updating the view when necessary.