I have a computed
observableArray
called selectedToppings
which returns filtered data from another observableArray
called toppings
. selectedToppings
should return all toppings that have their observable
property of selected
set to true
. In some situations, only one topping can be selected, such as if a customer is ordering the child-sized ice cream. Here is my code.
<label>Multiple Toppings:
<input type="checkbox" data-bind="checked: multiselectable" />
<em data-bind="if: !multiselectable()">pick only one topping</em>
</label>
<h2>Available</h2>
<ul data-bind="foreach: toppings">
<li data-bind="text: label, click: selected.bind($data, true)"></li>
</ul>
<h2>Selected</h2>
<ul data-bind="foreach: selectedToppings">
<li data-bind="text: label, click: selected.bind($data, false)">
</li>
</ul>
<script>
function Topping(options) {
this.label = ko.observable(options.label);
this.selected = ko.observable(false);
}
var iceCream = {
toppings: ko.observableArray([
new Topping({label: 'Sprinkles'}),
new Topping({label: 'Marshmallows'}),
new Topping({label: 'Nuts'})
]),
multiselectable: ko.observable(false)
};
iceCream.selectedToppings = ko.computed(function () {
return ko.utils.arrayFilter(iceCream.toppings(), function(item){
return item.selected();
});
});
var selectedSub = iceCream.selectedToppings.subscribe(function (toppings) {
if (!this.multiselectable()) {
if (toppings.length > 1) {
var item = toppings.shift();
item.selected(false);
}
}
}, iceCream);
ko.applyBindings(iceCream);
</script>
Also on jsfiddle.
I am using a subscription on availableToppings
to enforce the one-topping rule if it is supposed to be followed. This implementation is not working, and I am not sure how to fix it.
What is not working:
It's usually difficult to manage data violations after it has already changed. In your example, it's easier and more reliable to prevent the user from making invalid selections in the first place, rather than trying to "fix it" after a bad entry is detected. A business rule should be applied later in the process to validate that the multiselection is honored, and throw a warning or exception if it's not, but the user interface can prevent most of these mistakes before they happen.
Here's one way to prevent the invalid selection using the code you have shown: See the fiddle:
This is in the view for the available toppings:
<h2>Available</h2>
<ul data-bind="foreach: toppings">
<li data-bind="text: label, click: selected.bind($data, ($parent.multiselectable() || $parent.selectedToppings().length === 0) && true)"></li>
</ul>
With that in mind, I would consider a slightly different approach
Take a look at this: JS Fiddle
Think about separating out the list of available toppings from the ice cream itself. This will allow you to expand your example to include many ice cream instances without each instance needing to maintain a list of all the possible toppings and whether each one is selected or not.
Instead, consider one list of toppings and let each ice cream object instance contains an array of only the toppings that have been added to that ice cream instance.
function Topping(options) { this.label = ko.observable(options.label); }
function IceCream(){
this.toppings = ko.observableArray([]);
};
var toppings = ko.observableArray([
new Topping({label: 'Sprinkles'}),
new Topping({label: 'Marshmallows'}),
new Topping({label: 'Nuts'})
]);
var iceCream = new IceCream();
var multiselectable = ko.observable(false);
var viewModel = {
multiselectable: multiselectable,
iceCream: iceCream,
toppings: toppings,
addTopping: function(topping){
if( multiselectable() || iceCream.toppings().length < 1)
if( iceCream.toppings.indexOf(topping) == -1 )
iceCream.toppings.push(topping);
},
removeTopping: function(topping){
iceCream.toppings.remove(topping);
}
};
ko.applyBindings(viewModel);