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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With