Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS multiple selection model data binding

I am using AngularJS v1.2.0-rc.3.

I have a model y with a 1 to many relationship with model x.

Initially, I had a form for model y with a multiple select for xs, something like this:

Controller:

function test($scope) {
  $scope.xs = [
    {id:1, value:'value 1'},
    {id:2, value:'value 2'},
    {id:3, value:'value 3'}
  ];
  $scope.y = {xs:[2]};
}

View:

<div ng-controller="test">
  <select multiple ng-model="y.xs" ng-options="x.id as x.value for x in xs">
  </select>    
</div>

The result is an array of the selected items.

http://plnkr.co/edit/s3tvvHeyE17TVH5KNkPZ

All fine and good, but I needed to change it to a checkbox list and found I couldn't use an array anymore.

I tried using the repeater's index, like this:

<div ng-repeat="x in xs">
  <input type="checkbox" ng-model="y.xs[$index]" ng-true-value="{{x.id}}"/>
  {{x.value}}
</div>

but to pre-select the 2nd item for example, I needed to use this:

$scope.y = {xs: [null, '2']};

which was useless.

http://plnkr.co/edit/9UfbKF2gFLnhTOKu3Yep

After a bit of searching, it seems the recommended method is to use an object hash, like so

<div ng-repeat="x in xs">
  <input type="checkbox" ng-model="y.xs[x.id]"/>
  {{x.value}}
</div>

http://plnkr.co/edit/Xek8alEJbwq3g0NAPMcF

but if items are de-selected, you end up with something that looks like this:

y={
  "xs": {
    "1": false,
    "2": false,
    "3": false
  }
}

so I ended up adding a watch expression to filter out the false values, like this:

$scope.$watch('y.xs', function(n) {
  for (var k in n) 
    if (n.hasOwnProperty(k) && !n[k]) 
      delete n[k];
}, true);

http://plnkr.co/edit/S1C1g5fYKzUZb7b0pRtp

It works but it feels unsatisfactory.

As this is such a common use case, I'm interested to know how others are solving it.

Update

Following the suggestion to use a custom directive, I came up with this solution which maintains the selection as a list.

Directive:

angular.module('checkbox', [])
.directive('checkboxList', function () {
  return {
    restrict: 'A',
    replace: true,
    scope: {
      selection: '=',
      items: '=',
      value: '@',
      label: '@'
    },
    template: '<div ng-repeat="item in list">' +
      '<label>' +
      '<input type="checkbox" value="{{item.value}}" ng-checked="item.checked" ng-click="toggle($index)"/>' +
      '{{item.label}}' +
      '</label>' +
      '</div>',
    controller: ['$scope', function ($scope) {
      $scope.toggle = function (index) {
        var item = $scope.list[index],
          i = $scope.selection.indexOf(item.value);
        item.checked = !item.checked;
        if (!item.checked && i > -1) {
          $scope.selection.splice(i, 1);
        } else if (item.checked && i < 0) {
          $scope.selection.push(item.value);
        }
      };
      $scope.$watch('items', function (value) {
        $scope.list = [];
        if (angular.isArray(value)) {
          angular.forEach(value, function (item) {
            $scope.list.push({
              value: item[$scope.value],
              label: item[$scope.label],
              checked: $scope.selection.indexOf(item[$scope.value]) > -1
            });
          });
        }
      }, true);
    }]
  };
});

View:

<div checkbox-list
     selection="a.bs"
     items="bs"
     value="id"
     label="name">
</div>

http://plnkr.co/edit/m7yH9bMPuRCg5OP2u0VX

like image 882
gwhn Avatar asked Oct 21 '22 21:10

gwhn


1 Answers

I had to write a multi select directive myself in the past, feel free to grab it at http://isteven.github.io/angular-multi-select.

As for the data binding approach, my data structure is actually quite similar with yours, but in my approach I added one more property which represent the checkbox state.

Example, with your input above, I added "checked":

$scope.xs = [
    { id:1, value:'value 1', checked: false },
    { id:2, value:'value 2', checked: false },
    { id:3, value:'value 3', checked: false }
];

And I pass it to the directive like this:

<div 
    multi-select 
    input-model="xs" 
    button-label="value" 
    item-label="id value" 
    tick-property="checked" >
</div>

When you tick / untick a checkbox, the directive will modify the input model $scope.xs.checked accordingly. To achieve this, i attach a click handler to each checkbox. This handler will call a function in which I pass the checkbox object as the function parameter. This function will then synchronize the checkbox state and the model.

To get the selected / ticked checkboxes, you will only need to loop $scope.xs over where .checked === true

Example:

angular.forEach( $scope.xs, function( value, key ) {
    if ( value.checked === true ) {
         // Do your stuff here
    }
});

Pardon my bad English, and hope this helps. Cheers.

like image 170
user3587521 Avatar answered Nov 11 '22 14:11

user3587521