I have an observable array of Services that might be provided by a company (i.e. Sales, Service, Consulting). Each of these services may be performed by the company in zero or more US States, and for each state there is an optional certification number. Thus, the Service has an observable array of Locations, and a Location is a combination of State and Cert Number.
function Service(name) {
var self = this;
self.Name = ko.observable(name);
self.Locations = ko.observableArray();
}
function Location(state) {
var self = this;
self.State = ko.observable(state);
self.CertNum = ko.observable();
}
In the user interface, I want to display a single list of all the US States. The user would tick the checkbox next to each state the in which the company operates, making a "master list" of states. This master list, in turn, drives the choices that appear under each of the possible services.
For example, the user ticks "Alabama" and "Arizona" in the master list. The UI then renders a separate Location for each Service as a combination of a checkbox and textbox. Here's the master view model:
function viewModel() {
var self = this;
self.AllStates = [
{ name: "Alabama", abbrev: "AL" },
{ name: "Alaska", abbrev: "AK" },
{ name: "Arizona", abbrev: "AZ" },
{ name: "Arkansas", abbrev: "AR" }
];
self.BusinessStates = ko.observableArray();
self.AllSelectedStates = ko.computed( function() {
return ko.utils.arrayFilter( self.AllStates, function(item) {
return self.BusinessStates.indexOf(item.abbrev) >= 0;
});
});
self.Services = ko.observableArray([
new Service("Sales"),
new Service("Service"),
new Service("Consulting")
]);
}
A simple view would look like:
<h2>Master List</h2>
<ul data-bind="foreach: AllStates">
<li>
<label><input type="checkbox" data-bind="value: abbrev, checked: $root.BusinessStates"/> <span data-bind="text: name"></span></label></li>
</ul>
<h2>Services</h2>
<div class="service" data-bind="foreach: Services">
<h3 data-bind="text: Name"></h3>
<ul data-bind="foreach: $root.AllSelectedStates">
<li>
<label>
<input type="checkbox" data-bind="value: abbrev, checked: $parent.Locations" />
<span data-bind="text: name"></span>
</label>
<input type="text" placeholder="Cert. Number" data-bind="value: ??" />
</li>
</ul>
</div>
That view at least gets me a state entered into the Service's Locations array, but I don't know how to bind the CertNum property.
Let's say the user has selected Alabama and Arizona in the master list. Now, for the "Sales" service, the user ticks "Arizona" and enters a certification number of "98765". Under the "Consulting" service, the user ticks "Alabama" and does not enter a cert number. Ideally I'd like the resulting JSON snippet to look like:
"Services": [
{
"Name": "Sales",
"Locations": [
{
"State": { "name": "Arizona", "abbrev": "AZ" },
"CertNum": "98765"
}
]
},
{
"Name": "Service",
"Locations": []
},
{
"Name": "Consulting",
"Locations": [
{
"State": { "name": "Alabama", "abbrev": "AL" },
"CertNum": null
}
]
}
]
I can render and bind the "master list" of US States. I can iterate over the services and render a UI that provides controls for each available service location (i.e. checkbox for the state, textbox for the cert. number). What I haven't been able to is bind the service location controls to control membership of the location in the appropriate Locations array, AND affect the value of the certification number at the same time.
The closest attempt I've made is pretty complicated and hacky, where I'm using the knockout-postBox plugin to link the viewModel.AllSelectedStates property to a similar property on each Service model, and then push new Locations into each Service.Locations array. It almost works, except it wipes out changes to the Services whenever a state is checked or unchecked in the master list. Also, I have to push every available location into the Locations array for each Service, and rely on an IsSelected boolean flag on the location to determine whether it's been selected (I'd rather put just the locations in the array that were selected).
You can see the attempt in this JSFiddle: http://jsfiddle.net/cbono/PU6Sq/23/
The nice thing about it is it demonstrates how the UI should behave. But I think this is possible without the postBox plugin, I'm just not looking at the problem the right way to work out the answer. I've spent several days working through this and can't come up with a solution that works 100%.