3

I'm having trouble with a bi-directional binding in an ng-repeat. I would expect for the below $watch to be triggered when you select a color from the list.

$scope.$watch('favoriteColors', function (newValue) {
    console.log('example-favoriteColors', newValue);
});

I would expect for Orange to appear in $scope.favoriteColors when checked.

Example: http://plnkr.co/edit/k5SEQw4XFnxriD2I8ZG7?p=preview

directive('checkBox', function () {
    return {
        replace: true,
        restrict: 'E',
        //require: '^ngModel',
        scope: {
            'externalValue': '=ngModel',
            'value': '&'
        },
        template: function (el, attrs) {
            var html =
                '<div class="ngCheckBox">'+
                    '<span ng-class="{checked: isChecked}">' +
                        '<input type="checkbox" ng-model="isChecked"/>'+
                    '</span>'+
                '</div>';
            return html;
        },
        controller: ['$scope', '$timeout', function ($scope, $timeout) {
            var initialized = false;
            console.log($scope.value());
            if (angular.isArray($scope.externalValue)) {
                $scope.isChecked = $scope.externalValue.indexOf($scope.value()) > 0;
            } else {
                $scope.isChecked = !!$scope.externalValue;
            }

            $scope.$watch('isChecked', function (newValue) {
                if (angular.isDefined(newValue)) {
                    //add or remove items if this is an array
                    if (angular.isArray($scope.externalValue)) {
                        var index = $scope.externalValue.indexOf($scope.value());
                        if(index > -1) {
                            $scope.externalValue.splice(index, 1);
                        } else if (initialized) {
                            $scope.externalValue.push($scope.value());
                        }
                    } else {
                        //simple boolean value
                        $scope.externalValue = newValue;
                    }
                    if (initialized)
                        console.log($scope.externalValue);
                }
            });

            $timeout(function () {
                initialized = true;
            });
        }],
        link: function (scope, el, attrs) {

        }
    };
});
Ben
  • 60,438
  • 111
  • 314
  • 488

3 Answers3

2

Please check out this plunk: http://plnkr.co/edit/pbHz4ohBPi7iYq6uJI8X?p=preview

There were lots of changes. Some of them are:

  • The template needs not be a function, since it is static.
  • The initialized (and consequently the $timeout) is not needed.
  • I implemented my own indexOf function; there is a chance the objects are not the same in == sense, but equals in the x.name === y.name sense; (I have some doubts about this though)
  • The add or remove items if this is an array part was wrong; you need to update the array based on the value of isChecked, not based on whether the item already exists in the array (indexOf).
  • Initialize favoriteColors as an array, not as a single object, to be consistent, i.e. $scope.favoriteColors = [$scope.colors[1]];
  • (minor) Added a little more descriptive log when favoriteColors change.
  • Use $watch("favoriteColors", function() {...}, true) to watch for changes inside the array (not the true last argument).
Nikos Paraskevopoulos
  • 39,514
  • 12
  • 85
  • 90
0

I think it's because you need to be referencing a property on an object instead of the flat array. When you pass a primitive data structure like an array, it gets passed by reference and thus the updates aren't passed along properly. (Post by Mark Rajcok.)

I went ahead and showed this by hacking your plunkr a little bit. I changed $scope.favoriteColors = $scope.colors[1]; to $scope.favoriteColors = {value:$scope.colors[1]}; and changed <check-box ng-model="favoriteColors" value="color"> to <check-box ng-model="favoriteColors.value" value="color">.

Plunkr

You can see in the plunkr that when you hit the checkboxes the console.log statements now go off under the $watch function.

Community
  • 1
  • 1
KhalilRavanna
  • 5,768
  • 3
  • 28
  • 24
  • That solution doesn't quite work for what I'm trying to accomplish. My objective is for each item checked, to make its way into `$scope.favoriteColors`, which is why ngModel references an array instead of a specific value. – Ben Oct 28 '13 at 16:58
  • In my example, it is still referencing an array. The array has just been assigned as a property on an object (`$scope.favoriteColors.value = []`). – KhalilRavanna Oct 28 '13 at 17:06
  • Right... there's one small issue where you have to click twice before it registers with the $scope.$watch – Ben Oct 28 '13 at 17:15
  • @Webnet, I think if you add a deep watch for the collection it will fire on the first click. http://plnkr.co/edit/o7wxwYtFg0k4kmPT9TKi?p=preview – Davin Tryon Oct 28 '13 at 17:33
  • @DavinTryon - Your example still doesn't show the right console.log() on the first 2 clicks. – Ben Oct 28 '13 at 17:46
0

I see that you're using angular-form-ui's checkbox directive.

  • Use $watchCollection (link to documentation) instead of $watch for watching arrays for changes
  • Initialize $scope.favoriteColors as an array containing the values that should be checked

I've reverted your changes to angular-form-ui.js as those changes broke the directive. They are now exactly as the code appears in the latest commit on Github (checkbox.js). Only one thing has changed, the initialization of the angular-form-ui module by adding [] as the second argument to that first line.

Here is the updated plunker: http://plnkr.co/edit/mlUt46?p=preview

sirhc
  • 6,097
  • 2
  • 26
  • 29