Marc
Marc

Reputation: 6771

Change js object to knockout js view model with nested observable arrays

I get a specific JSON from a server and want to be able to add/edit/delete items in nested arrays (variant lists, variants and columns) but I can't figure out how to do that with knockout.js.

I know that I need to change the properties in that JSON object to observables and I do it with the mapping plugin like shown under "Binding" and all values has been bind correctly - but if I change a value in an input field, the model/view is not updated automatically.

Why? Am I missing something?

So are nested arrays supported native by knockout.js without the need to write own code? How can I get this JSON to a full working knockout.js view model?

I use the current available versions (knockout: v2.1.0, mapping plugin: v2.3.2).

JSON

{
    "VariantList": [
        {
            "ColumnCount": 1,
            "Variants": [
                {
                    "Title": "One column 100%",
                    "Columns": [
                        "100 %"
                    ]
                }
            ]
        },
        {
            "ColumnCount": 2,
            "Variants": [
                {
                    "Title": "Two columns 50%/50%",
                    "Columns": [
                        "50%",
                        "50%"
                    ]
                },
                {
                    "Title": "Two columns 75%/25%",
                    "Columns": [
                        "75%",
                        "25%"
                    ]
                }
            ]
        }
    ]
}

HTML

<div data-bind="foreach: VariantList">
    <h2 data-bind="text: ColumnCount"></h2>
    <div data-bind="foreach: Variants">
        <h3 data-bind="text: Title"></h3>
        <table style="width:500px">
            <tr>
                <!-- ko foreach: Columns -->
                <th><input data-bind="value: $data"/></th>
                <!-- /ko -->
            </tr>
            <tr>
                <!-- ko foreach: Columns -->
                <td data-bind="style: {width:$data}, text:$data"></td>
                <!-- /ko -->
            </tr>
        </table>
   </div>
</div>

Binding

var viewModel;
$(function(){
   viewModel = ko.mapping.fromJS(myJson);
   ko.applyBindings(viewModel);
});

Upvotes: 0

Views: 2104

Answers (1)

RP Niemeyer
RP Niemeyer

Reputation: 114792

The issue is that the mapping plugin does not turn primitive values in an array into observables, by default. Even it it did, by the time that you bind $data against your input, you would have the unwrapped value of it rather than the observable.

The easiest way to make this work is to structure your data something like:

var data = {
    "VariantList": [
        {
            "ColumnCount": 1,
            "Variants": [
                {
                    "Title": "One column 100%",
                    "Columns": [
                        { value: "100 %" }
                    ]
                }
            ]
        },
        {
            "ColumnCount": 2,
            "Variants": [
                {
                    "Title": "Two columns 50%/50%",
                    "Columns": [
                        { value: "50%" },
                        { value: "50%" }
                    ]
                },
                {
                    "Title": "Two columns 75%/25%",
                    "Columns": [
                        { value: "75%" },
                        { value: "25%" }
                    ]
                }
            ]
        }
    ]
};

Then you would bind against value in your loop on Columns. Here is a sample: http://jsfiddle.net/rniemeyer/MCnMX/

If you are not able to pull your data in this structure, then you can consider using the mapping options, to turn it into a structure like this. Here is a sample: http://jsfiddle.net/rniemeyer/sH3r2/

var mappingOptions = {
    Columns: {
        create: function(options) {
            return { value: ko.observable(options.data) };  
        }
    }        
};

var viewModel = ko.mapping.fromJS(data, mappingOptions);

If you need to send your JSON back to the server in the same format that you received it, then there are a few options. I kind of like to do it like this: http://www.knockmeout.net/2011/04/controlling-how-object-is-converted-to.html. Here is a sample with your data: http://jsfiddle.net/rniemeyer/Eed2R/

var Value = function(val) {
    this.value = ko.observable(val);  
};

Value.prototype.toJSON = function() {
    return ko.utils.unwrapObservable(this.value);  
};

var mappingOptions = {
    Columns: {
        create: function(options) {
            return new Value(options.data);
        }
    }        
};

var viewModel = ko.mapping.fromJS(data, mappingOptions);

Upvotes: 2

Related Questions