KnockoutJS binding on Select elements in a ForEach loop

In an application that I was working on recently, I had the need to bind a select element (i.e. a drop-down list) with KnockoutJS.  This is not an unusual thing to need to do with Knockout, however, this particular select element was ultimately rendered by Knockout itself as it was part of a collection of data objects, and so was within a Knockout foreach binding.

I had a collection of "rates" to choose from in one part of my Knockout View Model and needed to populate another part of the same Knockout View Model with the user's selected rate.  In this case, I wasn't simply selecting the numerical index value or some singular intrinsic data type as the value of the select element, but rather, I needed to whole underlying object.

So, our view model looks something like this:

{
	"MyViewModel" : {
		"Rates" : [{
				"Name" : "Initial Rate",
				"Percentage" : "80",
				"Categories" : [{
						"Id" : "b1fae11b-ce8d-4c72-ad4f-71dc515d0f42",
						"Name" : "Mechanical"
					}, {
						"Id" : "10e01f58-e7f5-4f2c-8d35-e2b87fc43a77",
						"Name" : "Performance"
					}
				]
			}, {
				"Name" : "Extended Rate",
				"Percentage" : "60",
				"Categories" : [{
						"Id" : "10e01f58-e7f5-4f2c-8d35-e2b87fc43a77",
						"Name" : "Performance"
					}, {
						"Id" : "d79a225b-4b76-4371-910d-47a9f3f58665",
						"Name" : "Sync"
					}
				]
			}
		],
		"OverrideRates" : []
	}
}

You can see that we're going to select one of the "Rates" from the array of Rate objects, and that each Rate object doesn't have it's own obvious unique identifier.  Once the user has selected a rate from the rates array, we want to copy the entire object into an OverrideRate object and store that in the OverrideRates array.  Our OverrideRate object will eventually look like this:

{
	"SelectedRate" : {
		"Name" : "Initial Rate",
		"Percentage" : "80",
		"Categories" : [{
				"Id" : "b1fae11b-ce8d-4c72-ad4f-71dc515d0f42",
				"Name" : "Mechanical"
			}, {
				"Id" : "10e01f58-e7f5-4f2c-8d35-e2b87fc43a77",
				"Name" : "Performance"
			}
		]
	},
	"OverridePercentage" : "90",
	"DateFrom" : "2017-01-01T00:00:00.000Z",
	"DateTo" : "2017-12-31T00:00:00.000Z"
}

So you can see here that the entire selected Rate object has been copied from the Rates array into the SelectedRate property.

In order to achieve this, I had markup similar to the following:

<table>
	<tbody data-bind="foreach: MyViewModel.OverrideRates">
		<tr>
			<td>
				<div>
					<select data-bind="options: $root.Rates,
							optionsCaption: 'Please select a rate...',
							optionsText: function(item) {
								return item.Name() + ' (' +
								item.Categories().map(function(elem){ return elem.Name() }).join(', ') + ')'},
							value: $data.SelectedRate">
					</select>
				</div>
			</td>
		</tr>
	</tbody>
</table>

The user was presented with a select element like this:

The Knockout binding for the select element shows that we're telling Knockout to get the options available for selection from the $root.Rates property, which is the collection of available rates (the $root prefix is a special binding context telling Knockout to access the rates object from the root level of the View Model), and further we're telling Knockout to use a special user-defined inline function to take certain string properties of our Rate object and use them to build up the actual text that the user will see inside the select element (i.e. "Initial Rate (Mechanical, Performance)").  Finally, we tell knockout that the "value" of the selected option from the select element needs to be bound to the $data.SelectedRate property.  However, there was a problem.

Two way data binding and data contexts

Knockout's data binding works by having your view model's properties actually be observable functions rather than the data that you actually want to bind.  This means that your MyViewModel.Name property, which can be two-way data-bound to a input box for editing the name string, is actually a function.  The function, when invoked with no parameters (i.e. MyViewModel.Name() ) will return the underlying data - in this case the name string, whilst invoking the function with a parameter (i.e. MyViewModel.Name("Jimmy") ) will set the underlying data to the parameter value passed in.

Whenever Knockout is binding page elements to underlying view model properties inside a loop, you use the special $data binding context prefix in order to access the current object that is currently being processed as part of the the loop.  On all other elements (for example, an input element) this works just fine, since the data that Knockout has to bind is a single value - in the case of a textbox, it's a simple string:

<tbody data-bind="foreach: MyViewModel.Rates">
	<tr>
		<td data-bind="text: $data.Name"></td>
		
	</tr>    
</tbody>

$data gives us a reference to the current rate object within the loop, allowing us to access the Name property of the correct object.  Note that although the Name property of the object is actually a Knockout observable function, we don't need to add the parentheses at the end (i.e. $data.Name() isn't required) since Knockout's binding is clever enough to deal with that for us.

This is great for single, granular pieces of data, however, when attempting to use this binding context as part of my select element that needed not just a simple, singular value, but a whole object to be bound to it, things did work so well.

As the user was selecting a "rate" from the select element drop-down list, I wanted knockout to binding the entire Rate object to the SelectedRate property of the View Model.  This part of the data-bind attribute was intended to achieve that:

value: $data.SelectedRate

However, when examining the View Model with Google Chrome's debugging tools, I noticed that the view model's SelectedRate property was not being updated when the user changed the selection.  Bizarrely, I did notice that when Knockout performed other data binding, for example, when the user changed the value in a textbox that was related to the selected rate, the SelectedRate property was suddenly bound with the correct object!

This was quite strange behaviour, and actually turned out to be a bit of a red herring which was leading my debugging efforts awry.  After much Googling and trial and error, it turned out the issue was all down to observable functions and how they work with the data binding context.

When your $data context isn't

Turns out that when you use the $data binding context in a binding expression, $data is bound not to the Knockout observable function but to the returned value from invoking the function.  This means that $data.SelectedRate was simply returning the underlying values of the observables rather than the observable function's themselves (SelectedRate is also the underlying value since the call to $data is only returning a value, meaning that properties "hanging off" that are also not observables).  This fix is to use a different Knockout binding altogether, which is the $rawData binding context.

Knockout's own documentation describes this:

$rawData

This is the raw view model value in the current context. Usually this will be the same as $data, but if the view model provided to Knockout is wrapped in an observable, $data will be the unwrapped view model, and $rawData will be the observable itself.

 

(Emphasis above is mine).

So, we needed to use the $rawData binding context to ensure that data bound page elements, that are expected to be bound to a complex object, are correctly bound.  And that this is not required when data binding to a simple, intrinsic type (i.e. a string, number etc.)

Interestingly, a Stack Overflow answer to a similar question talks about the $data binding context only referring to the underlying value rather than the observable function, but offers a different solution, one which didn't seem to work correctly for me.

You might think that, since usage of the $rawData binding context is fully explained in the Knockout documentation, why did I not discover and resolve this issue earlier?  Well, it turns out that Knockout has a somewhat chequered history regarding its $data binding context.

Knockout broke my data binding!

Another Stack Overflow question gives us a clue to the history of Knockout's $data binding context.

It would appear that in versions of Knockout prior to Version 3.0, the $data binding context used to function the same as the current version's $rawData binding context works - that is to say that $data used to be bound to the observable function rather than the underlying value. This was entirely due to the fact that the $rawData binding context did not exist in Knockout prior to Version 3.0 and so was considered missing functionality.

With Knockout Version 3.0, the developers introduced the $rawData binding context, however, it was somewhat buggy.  In Knockout Version 3.1, they finally fixed it so that $rawData now correctly refers to the observable function (if one exists) rather than the underlying value, whilst $data continues to "unwrap" the observable and refer to the underlying data value.

Add comment