2

There have been a lot of questions posted about using checkboxes in a foreach loop in Knockout, but I haven't seen an answer to why initalizing the checked binding works in some situations and not others.

With this simple viewmodel:

viewModel = function (obj) {
    this.AllPossibleThings = ko.observableArray(
    [
        { ID: "1", Value: 'Thing1' }, 
        { ID: "2", Value: 'Thing2' }, 
        { ID: "3", Value: 'Thing3' }, 
        { ID: "4", Value: 'Thing4' }
    ]);
    this.selectedThings = ko.observableArray(["2","3"]);
};
$(ko.applyBindings(new viewModel()));

Putting these in a foreach template works as you'd expect. It shows 4 checkboxes, with 2 and 3 pre-checked:

<table>
    <tbody data-bind="template: { name: 'ThingTmpl', foreach: AllPossibleThings }">
    </tbody>
</table>
<script id="ThingTmpl" type="text/html">
        <tr>
            <td><input type="checkbox" 
                      data-bind="attr: {value: ID}, checked: $root.selectedThings" /></td>
            <td><span data-bind="text: Value"></span></td>
        </tr>
</script>

Doing what should be the equivalent without a template doesn't work the same:

<table>
    <tbody data-bind="foreach: AllPossibleThings">
        <tr>
            <td><input type="checkbox" 
                      data-bind="checked: $root.selectedThings, 
                                 attr: {value: ID}" /></td>
            <td><span data-bind="text: Value"></span></td>
        </tr>
    </tbody>
</table>

The non-template one shows 4 checkboxes, but with none pre-checked. BUT, when you click, say, the 1st checkbox then 2 and 3 get checked. It's like it got bound before the "selectedThings" array got created and the change was never observed.

Fiddle to demonstrate with both: http://jsfiddle.net/jturnage/j763w/

Anybody know why the template foreach works here, but the regular foreach binding does not?

Nick
  • 1,417
  • 1
  • 14
  • 21
Jason Turnage
  • 59
  • 1
  • 5

1 Answers1

2

The problem is the order of your bindings and your browser.

It is my understanding that the order in which you iterate over the property names of an object is not well-defined and cannot be relied on. It may or may not be iterated in the order you have declared them. Knockout internally turns this binding to an object and iterates over the properties of the object. In this case, it happens to be in the order it was declared.

Your binding on the checkbox in the template is like this:

data-bind="attr: {value: ID}, checked: $root.selectedThings"

While the binding not in your template is like this:

data-bind="checked: $root.selectedThings, attr: {value: ID}"

The reason why the binding isn't working for you is because the attr binding hasn't been applied yet. So the checkboxes don't have their values set to the corresponding ID. When the checked binding updates, it is searching the selectedThings for non-existant values. You'll see it will work if you change the order of the binding so attr is applied first.


A better and safer way to deal with this IMO would test yourself whether it should be checked or not, rather than depending on this unstable behavior. Add a function which tests if a particular item is selected. Then it won't matter what order these bindings are fired.

viewModel = function (obj) {
    // ...

    this.isSelected = function (thing) {
        return this.selectedThings.indexOf(thing.ID) !== -1;
    };
};
<td><input type="checkbox" 
           data-bind="checked: $root.isSelected($data), attr: {value: ID}" />
</td>

Of course, you'll have to make adjustments if you want to keep the selectedThings synchronized. You can get that with some clever use of a computed observable.

this.isSelected = function (thing) {
    return ko.computed({
        read: function () {
            return this.selectedThings.indexOf(thing.ID) !== -1;
        },
        write: function (newValue) {
            var index = this.selectedThings.indexOf(thing.ID);
            if (newValue) {
                // checked
                if (index === -1)
                    this.selectedThings.push(thing.ID);
            } else {
                // unchecked
                if (index !== -1)
                    this.selectedThings.remove(thing.ID);
            }
        }
    }, this);
};

Updated fiddle

Community
  • 1
  • 1
Jeff Mercado
  • 129,526
  • 32
  • 251
  • 272
  • Ultimately it would be better if you made each thing have a `selected` property and make `selectedThings` a computed observable to filter `AllPossibleThings` by which are selected. Then you won't need to use these hacks. – Jeff Mercado Oct 19 '12 at 07:09
  • Wow I didn't catch that. I swapped the attr binding around and sure enough that worked. I would have never guessed that, though I have read something from Ryan N related to it about understanding why bindings fire all at the same time and order can be important, just didn't realize that was applicable here. – Jason Turnage Oct 19 '12 at 13:28
  • And actually both the bindings swap and the computed observable are great answers, thanks. The computed() was my original way but I changed it to the simpler oA for one reason or another (i think because i have dozens of them in my object and took up more code than i wanted). Thanks @JeffMercado. – Jason Turnage Oct 19 '12 at 13:32
  • Thx a lot, I actually spent quite a long time wondering why ma radios' checked properties weren't binding ;) – Pierre Murasso Aug 26 '13 at 13:16