6
votes

This is a follow-up to How can I bind a ko.observableArray of strings?

How can I bind an editable observable array of observable strings to a set of input boxes? I don't want to bind to an array of objects, as my underlying JSON sent from the server is an array of strings.

The following example doesn't work (try it at http://jsfiddle.net/LDNeA/). Binding an array of objects with observable strings is OK, but binding the array of observable strings directly doesn't work, and the model is not updated.

The important thing is that the entries in the textboxes are mapped back into the model.

JS:

var ViewModel = function() {
    this.value = ko.observable("hi")
    this.array1 = ko.observableArray([ko.observable("hi"), ko.observable("there")]);
    this.array2 = ko.observableArray([{ data: ko.observable("hi") }, { data: ko.observable("there") }]);
};

ko.applyBindings(new ViewModel());

HTML:

<div class='liveExample'>   
    <p><input data-bind='value: value' /></p> 
    <div data-bind="foreach: array1">
        <p><input data-bind='value: $data' /></p> 
    </div>
    <div data-bind="foreach: array2">
        <p><input data-bind='value: data' /></p> 
    </div>
</div>

<pre data-bind="text: ko.toJSON($data)"></pre>
4
This is definitely an issue, but if the sending back to server JSON as a simple array is the only requirement, would you accapt an object that serialized itself as a single value so that it looked like just an array? - Kyeotic
@Tyrsius - I'd accept that answer if the answer is that "this is currently a bug in knockout.js", or "this is why this could never work". :) - slipheed
Apparently it is a logged issue, I updated my answer - Kyeotic

4 Answers

14
votes

As noted by links posted by @Tyrsius, this is a bug (?) in Knockout.

The easiest workaround is to use $parent.items()[$index()], as seen in this fiddle: http://jsfiddle.net/r8fSg/. Note that $parent.items() is the observableArray of items that is used in the foreach.

<div data-bind="foreach: items">
    <p><input data-bind='value: $parent.items()[$index()]' /></p> 
</div>

<pre data-bind="text: ko.toJSON($data)"></pre>

And the model:

var ViewModel = function() {
    this.items = ko.observableArray([ko.observable("hi"), ko.observable("hi")]);
};

ko.applyBindings(new ViewModel());
3
votes

The issue is that Knockout doesn't get a reference to the source when it isn't the property of something. Since you are just passing it a function, the two-way binding fails. This has been noted before, is logged as an issue here, and the behavior is expalined in this issue.

This is not an ideal solution, but you can control object serialization with toJSON methods. This will allow you to produce what appears to be an array of strings, while still being observable in your app.

var Item = function(name){
    this.name = ko.observable(name);
};

Item.prototype.toJSON = function(){
    //special note: knockout already unwraps the observable for you
    return this.name;
};

Here is the fiddle


Update

See slipheeds answer for a solution that just uses a binding, which I prefer to this method. Leaving this answer in case others prefer it

2
votes

In Knockout 3.0 and above using $rawData instead of $data solves this problem. It is also mentioned as solution @ https://github.com/knockout/knockout/issues/708#issuecomment-27630842

1
votes

Another solution is to use the Repeat binding (https://github.com/mbest/knockout-repeat), which does provide this functionality. Here's your example updated to use Repeat: http://jsfiddle.net/LDNeA/1/

<div data-bind="repeat: array1">
    <p><input data-bind='value: $item' /></p> 
</div>