Knockout.js using `value:` binding in a `foreach` over a list of strings - does not update
Asked Answered
L

2

12

Here is a jsFiddle demonstrating the following problem:

Given a foreach binding over a list of (observable) strings, the observables do not seem to update from changes to input tags bound inside the foreach. One would expect them to. Here's the example from the jsFiddle:

HTML

<ul data-bind='foreach: list'>
    <li><input data-bind='value: $data'/></li>
</ul>

<ul data-bind='foreach: list'>
    <li><span data-bind='text: $data'></span></li>
</ul>

Javascript

​var vm = { list: [ko.observable('123'), ko.observable('456')] };
ko.applyBindings(vm);​

In the above example, one would expect that updating the input tags in the first list would cause the observables to update. Unfortunately they do not update as expected, as can be seen by the failure of the second list to reflect any changes to made to the first.

I verified that the list was not in fact being updated when the input elements are changed. Interestingly, changes made to the observables are reflected in both lists (as one would expect). Namely, vm.list[1]("444") will update the second element of both lists.

My recollection is that Knockout 2.0.0 did not have this issue, though I stand to be corrected. I did not find any documentation, Google or comments in the Knockout code that yielded any indication as to why this does not work or how to achieve the outcome expected.

Why does this not work as expected, and are there any workarounds that do not require changing the data structure?

Ludovick answered 12/11, 2012 at 19:48 Comment(0)
L
16

I worked around this by using value: $parent.list[$index()], as seen in this jsFiddle. The new bindings looks like this:

<ul data-bind='foreach: list'>
    <li>
        <input data-bind='value: $parent.list[$index()]' />
    </li>
</ul>

One could perhaps improve on this with a custom binding.

See also this related GitHub issue #708 for Knockout.js.

Update for Knockout 3.0:

Knockout now provides $rawData:

<ul data-bind='foreach: list'>
    <li><input data-bind='value: $rawData'/></li>
</ul>

creates a two-way binding as expected.

From the Binding Context documentation:

$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.

Ludovick answered 12/11, 2012 at 20:37 Comment(2)
This for some reason is causing my input field to lose focus on keyup when valueUpdate is set to 'keyup'.Organotherapy
@Organotherapy that can often mean that the list is being re-rendered. To be sure this isn't the case you should check that the observableArray list itself isn't being modified as the input changes the value.Ludovick
C
6

Every data object used in the default knockout bindings will always be unwrapped. So you are essentially binding to the value of the items in the list, not the observable as you are expecting.

Observables should be properties of an object, not a replacement of the object itself. Set the observables as a property of some object so this doesn't happen.

​var vm = {
    list: [
        { value: ko.observable('123') },
        { value: ko.observable('456') }
    ]
};
<ul data-bind='foreach: list'>
    <li><input data-bind='value: value'/></li>
</ul>

<ul data-bind='foreach: list'>
    <li><span data-bind='text: value'></span></li>
</ul>
Christianson answered 12/11, 2012 at 19:59 Comment(3)
Thanks Jeff. Alas, Knockout is not working as expected, your suggestion requires modifying the data to conform to unnecessary expectations, and the documentation is misleading: "$data is useful when you don’t want to reference a property on the viewmodel, but instead want to reference the viewmodel itself."Ludovick
$data is a reference to the value of the object. Since the object itself was an observable, it had to be unwrapped to get its underlying value. You'll understand why this has to be if you had directly bound to an array of strings (as it is in the example there). That line in the documentation was referring to the root context (i.e., you're not within a foreach or with block). Though you're right it is a bit misleading as it doesn't mention this subtle difference.Christianson
Seems this is related to a known issue. I'm glad I'm not the only one. :)Ludovick

© 2022 - 2024 — McMap. All rights reserved.