I am looking for something exactly like these (tri-state checkboxes with "parents"). But using that solution wouldn't be elegant, as I do not depend on jQuery right now, and I would need to call $scope.$apply to get the model to recognize the automatically (un)checked checkboxed jQuery clicked.
Here's a bug for angular.js that requests ng-indeterminate-value implemented. But that still wouldn't give me the synchronization to all the children, which is something I don't think should be a part of my controller.
What I am looking for would be something like this:
- A "ng-children-model" directive with syntax like:
<input type="checkbox" ng-children-model="child.isSelected for child in listelements">
. The list of booleans would be computed, and if 0 selected -> checkbox false. If all selected -> checkbox true. Else -> checkbox indeterminate. - In my controller, I would have something like this:
$scope.listelements = [{isSelected: true, desc: "Donkey"},{isSelected: false, desc: "Horse"}]
- The checkboxes would be made as usual with
<tr ng-repeat="elem in listelements"><td><input type="checkbox" ng-model="elem.isSelected"></td><td>{{elem.desc}}</td></tr>
. - As I understand it, the browser will determine which state a clicked indeterminate checkbox goes into.
Since you want a new type/kind of component, this sounds like a good case for a custom directive.
Since the parent/master/tri-stated checkbox and the individual dual-state checkboxes need to interact with each other, I suggest a single directive, with its own controller, to handle the logic.
<tri-state-checkbox checkboxes="listelements"></tri-state-checkbox>
Directive:
app.directive('triStateCheckbox', function() {
return {
replace: true,
restrict: 'E',
scope: { checkboxes: '=' },
template: '<div><input type="checkbox" ng-model="master" ng-change="masterChange()">'
+ '<div ng-repeat="cb in checkboxes">'
+ '<input type="checkbox" ng-model="cb.isSelected" ng-change="cbChange()">{{cb.desc}}'
+ '</div>'
+ '</div>',
controller: function($scope, $element) {
$scope.masterChange = function() {
if($scope.master) {
angular.forEach($scope.checkboxes, function(cb, index){
cb.isSelected = true;
});
} else {
angular.forEach($scope.checkboxes, function(cb, index){
cb.isSelected = false;
});
}
};
var masterCb = $element.children()[0];
$scope.cbChange = function() {
var allSet = true, allClear = true;
angular.forEach($scope.checkboxes, function(cb, index){
if(cb.isSelected) {
allClear = false;
} else {
allSet = false;
}
});
if(allSet) {
$scope.master = true;
masterCb.indeterminate = false;
}
else if(allClear) {
$scope.master = false;
masterCb.indeterminate = false;
}
else {
$scope.master = false;
masterCb.indeterminate = true;
}
};
$scope.cbChange(); // initialize
},
};
});
Change the template to suit your needs, or use an external template with templateUrl.
The directive assumes that the checkboxes array contains objects that have an isSelected
property and a desc
property.
Update: If you prefer to have the directive only render the tri-stated checkbox, hence the individual checkboxes are in the HTML (like @Piran's solution), here's another plunker variation for that. For this plunker, the HTML would be:
<tri-state-checkbox checkboxes="listelements" class="select-all-cb">
</tri-state-checkbox>select all
<div ng-repeat="item in listelements">
<input type="checkbox" ng-model="item.isSelected"> {{item.desc}}
</div>
I think the sample solution you give puts too much code into the controller. The controller should really only be worry about the list, and the HTML/Directives should be handling the display (including displaying the Select All checkbox). Also, all state changes are through the model, not by writing functions.
I've put together a solution on Plunker: http://plnkr.co/edit/gSeQL6XPaMsNSnlXwgHt?p=preview
Now, the controller just sets up the list:
app.controller('MainCtrl', function($scope) {
$scope.list = [{
isSelected: true,
desc: "Donkey"
}, {
isSelected: false,
desc: "Horse"
}];
});
and the view simply renders those out:
<div ng-repeat="elem in list">
<input type="checkbox" ng-model="elem.isSelected" /> {{elem.desc}}
</div>
For the Select All checkbox, I've created a new directive called checkbox-all
:
<input checkbox-all="list.isSelected" /> Select All
And that's it as far as use goes, which is hopefully simple... apart from writing that new directive:
app.directive('checkboxAll', function () {
return function(scope, iElement, iAttrs) {
var parts = iAttrs.checkboxAll.split('.');
iElement.attr('type','checkbox');
iElement.bind('change', function (evt) {
scope.$apply(function () {
var setValue = iElement.prop('checked');
angular.forEach(scope.$eval(parts[0]), function (v) {
v[parts[1]] = setValue;
});
});
});
scope.$watch(parts[0], function (newVal) {
var hasTrue, hasFalse;
angular.forEach(newVal, function (v) {
if (v[parts[1]]) {
hasTrue = true;
} else {
hasFalse = true;
}
});
if (hasTrue && hasFalse) {
iElement.attr('checked', false);
iElement.addClass('greyed');
} else {
iElement.attr('checked', hasTrue);
iElement.removeClass('greyed');
}
}, true);
};
});
The parts
variable breaks down the list.isSelected
into its two parts, so I can get the value of list
from the scope, an the isSelected
property in each object.
I add the type="checkbox"
property to the input element, making it a real checkbox for the browser. That means that the user can click on it, tab to it, etc.
I bind on the onchange
event rather than onclick
, as the checkbox can be changed in many ways, including via the keyboard. The onchange event runs inside a scope.$apply()
to ensure that the model changes get digested at the end.
Finally, I $watch
the input model for changes to the checkbox (the last true
allows me to watch complex objects). That means if the checkboxes are changed by the user or for some other reason, then the Select All checkbox is always kept in sync. That's much better than writing lots of ng-click handlers.
If the checkboxes are both checked and unchecked, then I set the master checkbox to unchecked and add the style 'greyed' (see style.css
). That CSS style basically sets the opacity to 30%, causing the checkbox to appear greyed, but it's still clickable; you can also tab to it and use spacebar to change its value.
I've tested in Firefox, Chrome and Safari, but I don't have IE to hand. Hopefully this works for you.
Here's a refined version of Piran's solution. Using .prop()
instead of .attr()
fixes the checked
issue.
Usage:
<div ng-repeat="elem in list">
<input type="checkbox" ng-model="elem.isSelected" /> {{elem.desc}}
</div>
<ui-select-all items="list" prop="isSelected"></ui-select-all> Select all
I believe that you should only be creating a directive if you only need to do some kind of a DOM manipulation or want to abstract away a lot of DOM manipulative behaviour into a "re-usable" component.
Here is a solution which achieves the same thing that you were attempting, but, this does only the logic in the controllers... If you want to keep the controllers lean, then you could push away all this logic into service...A service would also be a good place to do this, if you want to re-use this in multiple places..
http://plnkr.co/edit/hNTeZ8Tuht3T9NuY7HRi?p=preview
Note that there is no DOM manipulation in the controller. We are achieving the effect we require using a bunch of directives that are provided with Angular. No new directive required.. I really dont think you should use a directive to abstract away logic..
Hope this helps..
If you can't assume that ng-model is assigned to a boolean model (e.g. Y/N, '0'/'1') and/or you prefer to have your own markup, an approach that leverages ngModel capabilities, and makes no assumption on HTML structure is better, IMHO.
Example: http://plnkr.co/edit/mZQBizF72pxp4BvmNjmj?p=preview
Sample usage:
<fieldset indeterminate-group>
<legend>Checkbox Group</legend>
<input type="checkbox" name="c0" indeterminate-cue> Todos <br>
<input type="checkbox" name="c1" ng-model="data.c1" ng-true-value="'Y'" ng-false-value="'F'" indeterminate-item> Item 1 <br>
<input type="checkbox" name="c2" ng-model="data.c2" ng-true-value="'Y'" ng-false-value="'F'" indeterminate-item> Item 2 <br>
<input type="checkbox" name="c3" ng-model="data.c3" ng-true-value="'Y'" ng-false-value="'F'" indeterminate-item> Item 3 <br>
</fieldset>
Directive (main parts):
angular.module('app', [])
.directive('indeterminateGroup', function() {
function IndeterminateGroupController() {
this.items = [];
this.cueElement = null;
}
...
function setAllValues(value) {
if (this.inChangeEvent) return;
this.inChangeEvent = true;
try {
this.items.forEach(function(item) {
item.$setViewValue(value);
item.$render();
});
} finally {
this.inChangeEvent = false;
}
}
return {
restrict: "A",
controller: IndeterminateGroupController,
link: function(scope, element, attrs, ctrl) {
ctrl.inputChanged = function() {
var anyChecked = false;
var anyUnchecked = false;
this.items.forEach(function(item) {
var value = item.$viewValue;
if (value === true) {
anyChecked = true;
} else if (value === false) {
anyUnchecked = true;
}
});
if (this.cueElement) {
this.cueElement.prop('indeterminate', anyChecked && anyUnchecked);
this.cueElement.prop('checked', anyChecked && !anyUnchecked);
}
};
}
};
})
.directive('indeterminateCue', function() {
return {
restrict: "A",
require: '^^indeterminateGroup',
link: function(scope, element, attrs, indeterminateGroup) {
indeterminateGroup.addCueElement(element);
var inChangeEvent = false;
element.on('change', function(event) {
if (event.target.checked) {
indeterminateGroup.checkAll();
} else {
indeterminateGroup.uncheckAll();
}
});
}
};
})
.directive('indeterminateItem', function() {
return {
restrict: "A",
require: ['^^indeterminateGroup', 'ngModel'],
link: function(scope, element, attrs, ctrls) {
var indeterminateGroup = ctrls[0];
var ngModel = ctrls[1];
indeterminateGroup.addItem(ngModel);
ngModel.$viewChangeListeners.push(function() {
indeterminateGroup.inputChanged();
});
}
};
});
Model:
// Bring your own model
TODO:
- get rid of item.$render() inside main directive controller;
- give a better name to the directive;
- make easy to use this directive in more than one table column.
Plunker
"use strict";
var module = angular.module("myapp", []);
function Ctrl($scope) {
var element = $("#select_all");
$scope.$watch("$scope.isgreyed", $scope.fun = function() {
element.prop("indeterminate", $scope.isgreyed);
});
$scope.list = [{
isSelected: true,
desc: "Donkey"
}, {
isSelected: false,
desc: "Horse"
}]
$scope.isgreyed = true;
$scope.master = false;
$scope.onmasterclick = function() {
$scope.list.map(function(v) {
v.isSelected = $scope.master
})
}
$scope.oncheckboxclick = function() {
if ($('.select_one:checked').length === 0) {
$scope.isgreyed = false;
$scope.master = false;
} else if ($('.select_one:not(:checked)').length === 0) {
$scope.isgreyed = false;
$scope.master = true;
} else {
$scope.isgreyed = true;
}
$scope.fun();
}
}
HTML:
<div ng-controller="Ctrl">
<table>
<tr>
<td>
<input type="checkbox" id="select_all" ng-model="master" ng-click="onmasterclick()">
</td>
</tr>
<tr ng-repeat="elem in list">
<td>
<input ng-click="oncheckboxclick(elem)" class="select_one" type="checkbox" ng-model="elem.isSelected">
</td>
<td>{{elem.desc}}</td>
</tr>
</table>
</div>
Yes, it's ugly.
Rewritten using Plnker to a bit better code without resource-consuming ForEach's and some other complicated stuff:
var app = angular.module('angularjs-starter', []);
app.controller('MainCtrl', function($scope) {
$scope.listelements = [{
isSelected: true,
desc: "Donkey"
}, {
isSelected: false,
desc: "Horse"
}];
});
app.directive('triStateCheckbox', function() {
return {
replace: true,
restrict: 'E',
scope: {
checkboxes: '='
},
template: '<input type="checkbox" ng-model="master" ng-change="masterChange()">',
controller: function($scope, $element) {
$scope.masterChange = function() {
for(i=0;i<$scope.checkboxes.length; i++)
$scope.checkboxes[i].isSelected=$scope.master;
};
$scope.$watch('checkboxes', function() {
var set=0;
for (i=0;i<$scope.checkboxes.length;i++)
set += $scope.checkboxes[i].isSelected?1:0;
$element.prop('indeterminate', false);
$scope.master = (set === 0) ? false : true;
if (set > 0 && set < i) {
$scope.master = false;
$element.prop('indeterminate', true);
}
}, true);
}
};
});
i guess it can be solved by combining angular with javascript:
<div>
<input type="checkbox" id="select-all" name="selectAll" value="" ng-click="checkAll($event)" />
<div >
<input type="checkbox" name="childCheckbox" value="" />
<input type="checkbox" name="childCheckbox" value="" />
<input type="checkbox" name="childCheckbox" value="" />
<input type="checkbox" name="childCheckbox" value="" />
<input type="checkbox" name="childCheckbox" value="" />
<input type="checkbox" name="childCheckbox" value="" />
</div>
</div>
in checkAll() the following logic will do the job
$scope.checkAll = function (source) {
checkboxes = document.getElementsByName('childCheckbox');
for (var i = 0, n = checkboxes.length; i < n; i++) {
checkboxes[i].checked = source.originalEvent.srcElement.checked;
}
来源:https://stackoverflow.com/questions/12648466/how-can-i-get-angular-js-checkboxes-with-select-unselect-all-functionality-and-i