A Smarter `checked` Binding for KnockoutJS
Update: In Knockout 3+, this issue is solved with the new checkedValue
binding. Read the documentation for details. You can still use my workaround for KO 2.x and below.
I love KnockoutJS. I've been using it for awhile now and I really enjoy it. That said, there are a few oddities with it, one such being the checked
binding.
Let's say you have this scenario (as I do now):
You want to foreach
a list of items, each one having a checkbox. You want to store the checked items in a list, for use throughout the view model (bulk updates, select all, deselect all, etc.). How do you go about doing this?
The "recommended" approach would be to add an observable property, isSelected
, to each of your items. Then, in your markup, you can bind to that property:
<ul data-bind="foreach: tasks">
<li><input type="checkbox" data-bind="checked: isSelected"></li>
</ul>
That is OK. It's OK because what if you need to use that item (assuming you're using a "class" or other reusable object) somewhere else where you don't need that property. You could just attach it where you need it. It also makes it a bit tricky to manage the list of selected items (you'd probably end up creating a computed
array), as well as making it difficult to dynamically bind a checkbox list to a computed array.
What you may decide to do (like me) is to try and bind the checked
to your own observable array, and assign the value
of the checkbox to a model property like this:
<input type="checkbox" data-bind="checked: $root.selectedTaskIds, value: id">
However, if you do this, you'll soon discover a few oddities:
- When you check the checkbox, the string value of your model property will be placed into the array.
- Your model property will change to this new string value essentially changing your original model.
There are reasons for these two crazy happenings, which I discovered:
- The
checked
binding bases its value on the element's value, which is stored as a string. - The
value
binding keeps the property you are bound to and the element's value in-sync, which is why your model's ID becomes a string.
It turns out, we're suffering from some mental model breakdown. If you look at the KO documentation for checked
, it's easy to believe the above syntax should work, after all, it says:
Special consideration is given if your parameter resolves to an array. In this case, KO will set the element to be checked if the value matches an item in the array, and unchecked if it is not contained in the array.
If the value matches... "Cool!" so I can just bind the value:
to my model property? NOPE. You'll run into the aforementioned issues if you try.
Making a smarter checked
binding
Since I didn't want to add isSelected
properties everywhere, I decided to "monkey patch" the checked
and value
bindings to play nice together.
Essentially, all my patch does is two things:
- In the
checked
binding, if it finds avalue
binding, it binds to that value rather than the element's value. - In the
value
binding, it completely ignores any changes if thechecked
binding is present, essentially nullifying the binding.
Here are the new binding definitions:
And you can see it in action here:
As you can see, now it works as you'd expect! You can bind the id
property to the list. You can also bind $data
and you'll have the full object in your selected list.