Knockout is very convenient library to use on html forms, and if we want to add a dropdown with multiselect checkboxes, then we have several options available:
- There is a plugin which enables binding a multiselect checkbox list to knockout objects, however it also needs a custom binding. More information about the plugin can be found at http://stackoverflow.com/questions/37797907/multiselect-dropdown-with-checkboxes-knockout-js. And the custom binding is explained at https://github.com/davidstutz/bootstrap-multiselect/issues/532.
- Another interesting alternative is to use custom binding handler explained at http://jsfiddle.net/bkzb272j/1/.
These two look like promising solutions, but I was looking for something more lightweight.
So I resorted to use only the basic functions in the first binding and create a lightweight multiselect checkbox which can show grouped options. I will try to explain this approach here.
The group model also contains a computed observable IsSelected (which should be correctly named AreAllChildOptionsSelected). This observable returns true if all the children options have been selected.
Note that the drop down view model uses a computed observable named FlattenedOptions to make it easier to select/deselect options or groups. It also has another computed observable named SelectedOptions which sets/returns all the options (not groups) which currently have IsSelected set to true.
The knockout object model and templates are all explained at https://jsfiddle.net/pranavnk/3ocwhyho/. The example uses jquery, bootstrap and knockout libraries.
So I resorted to use only the basic functions in the first binding and create a lightweight multiselect checkbox which can show grouped options. I will try to explain this approach here.
The Knockout Model
First I created a knockout view model for each option as:
var FilterOptionModel = function (text, value) {
var self = this;
value = value ? value : "";
self.Value = ko.observable(value);
text = text ? text : "";
self.Text = ko.observable(text);
self.IsSelected = ko.observable(false);
};
This model contains a property IsSelected which is used to set the checkbox at each option level.
Next I added a view model for option groups like so:
var FilterGroupModel = function (label, options) {
var self = this;
label = label ? label : "";
self.Label = ko.observable(label);
options = options ? options : [];
self.Options = ko.observableArray(options);
self.IsSelected = ko.pureComputed({
read: function () {
var nonSelected = false;
for (var i = 0; i < self.Options().length; i++) {
if (!self.Options()[i].IsSelected())
nonSelected = true;
}
return !nonSelected;
},
write: function (selected) {
for (var i = 0; i < self.Options().length; i++) {
!self.Options()[i].IsSelected(selected);
}
}
});
};
And finally the view model for dropdown:
var FilterDropDownModel = function () {
var self = this;
self.Title = ko.observable('');
self.Options = ko.observableArray([]);
self.FlattenedOptions = ko.pureComputed(function () {
var allOptions = self.Options();
if (self.IsGroupedOptions()) {
allOptions = Array.prototype.concat.apply([], self.Options().map(function (category) { return category['Options'](); }));
}
return allOptions;
}, self);
self.IsGroupedOptions = ko.pureComputed({
read: function () {
var result = false;
var allOptions = self.Options();
if (allOptions && allOptions.length && allOptions.length > 0 && allOptions[0].Options) {
result = true;
}
return result;
}
});
self.SelectedOptions = ko.pureComputed({
read: function () {
var result = self.FlattenedOptions().filter(function (option) { return option.IsSelected(); });
return result;
},
write: function (selectedOptions) {
if (selectedOptions && selectedOptions.length) {
for (var i = 0; i < self.Options().length; i++) {
if (self.IsGroupedOptions()) {
for (var j = 0; j < self.Options()[i].Options().length; j++) {
var selectedOption = $.grep(selectedOptions, function (item) { return item.Value() === self.Options()[i].Options()[j].Value(); });
if (selectedOption) {
self.Options()[i].Options()[j].IsSelected(true);
}
}
}
else {
var selectedOption = $.grep(selectedOptions, function (item) { return item.Value() === self.Options()[i].Value(); });
if (selectedOption) {
self.Options()[i].IsSelected(true);
}
}
}
}
else {
for (var i = 0; i < self.Options().length; i++) {
self.Options()[i].IsSelected(false);
}
}
}
});
self.IsAllOptionsSelected = ko.pureComputed({
read: function () {
return self.FlattenedOptions().length === self.SelectedOptions().length;
},
write: function (selected) {
if (selected) {
self.SelectedOptions(self.FlattenedOptions());
}
else {
self.SelectedOptions([]);
}
}
});
self.OptionsBusy = ko.observable(false);
self.SelectedText = ko.pureComputed({
read: function () {
if (self.SelectedOptions().length === 0) {
return 'None selected';
}
else if (self.FlattenedOptions().length === self.SelectedOptions().length) {
return 'All';
}
else if (self.SelectedOptions().length > 3) {
return self.SelectedOptions().length + ' selected';
}
else {
var selected = '';
var delimiter = ', ';
for (var i = 0; i < self.SelectedOptions().length; i++) {
var option = self.SelectedOptions()[i];
var label = option.Text();
selected += label + delimiter;
}
return selected.substr(0, selected.length - delimiter.length);
}
}
});
};
HTML
HTML page has knockout bindings which display all options and groups. It also has checkboxes which indecate if a group or an options is currently selected. To make it easier to reuse this component, I have separated the HTML knockout binding in a knockout template like so:
<div class="rounded-box" id="locationsSelect-container" data-bind="with: students">
<span>There are </span><span data-bind="text:
FlattenedOptions().length"></span><span> students in our class.</span>
<br/>
<span>There are </span><span data-bind="text:
Options().length"></span><span> student groups in our class.</span>
<!-- ko template: { name: 'dropdown-template' } -->
<!-- /ko -->
</div>
<script type="text/html" id="dropdown-template">
<div id="busyContainer" class="loading-div-mask" data-bind="css: {
'visible': OptionsBusy() }">
<img class="loading-image" src="https://i0.wp.com/cdnjs.cloudflare.com/ajax/libs/galleriffic/2.0.1/css/loader.gif" alt="Loading..." />
</div>
<div class="multiselect-native-select">
<div class="btn-group">
<button type="button" class="multiselect dropdown-toggle btn btn-default" data-toggle="dropdown" title="All" aria-expanded="true"><span class="multiselect-selected-text" data-bind="text:
SelectedText()"></span> <b class="caret"></b></button>
<ul class="multiselect-container dropdown-menu">
<li class="multiselect-item multiselect-all"><a tabindex="0" class="multiselect-all"><label class="checkbox"><input type="checkbox" data-bind="checked: IsAllOptionsSelected"> Select all</label></a></li>
<!-- ko foreach: Options -->
<!-- ko if: typeof Options ==
'undefined' -->
<li>
<a tabindex="0"><label class="checkbox">
<input type="checkbox" data-bind="checkedValue: $data, checked:
IsSelected"><span data-bind="text: Text"></span>
</label></a>
</li>
<!-- /ko -->
<!-- ko if: typeof Options !=
'undefined' -->
<li class="multiselect-item multiselect-group">
<a tabindex="0"><label>
<input type="checkbox" data-bind="checked: IsSelected"><span data-bind="text:
Label"></span>
</label></a>
<!-- ko foreach:
Options -->
<li>
<a tabindex="0"><label class="checkbox">
<input type="checkbox" data-bind="checkedValue: $data, checked: IsSelected"><span data-bind="text: Text"></span>
</label></a>
</li>
<!-- /ko -->
</li>
<!-- /ko -->
<!-- /ko -->
</ul>
</div>
</div>
</script>
The knockout object model and templates are all explained at https://jsfiddle.net/pranavnk/3ocwhyho/. The example uses jquery, bootstrap and knockout libraries.