Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Knockout.js: Binding to complex object

Tags:

knockout.js

I'm new to knockout.js and am trying to bind to the following object representing a user:

{
    "$id": "1",
    "$values": [
        {
            "$id": "2",
            "Locations": {
                "$id": "3",
                "$values": []
            },
            "Photos": {
                "$id": "4",
                "$values": []
            },
            "UserId": 1,
            "Name": "Test User"
        }
    ]
}

A user can zero or more locations, and zero or more photos.

The view model:

function UsersViewModel() {
    var self = this;
    self.users = ko.observableArray();

    var baseUri = 'http://localhost:46241/api/users';

    $.getJSON(baseUri, function (data) {
        self.users = data;
    });
}

$(document).ready(function () {
    ko.applyBindings(new UsersViewModel());
})

The HTML contains the following binding:

<ul id="update-users" data-bind="foreach: users"> 
     <li> 
            <div><div class="item">User ID</div>
                <input type="text" data-bind="value: $data.UserId" />
            </div>                 
            <div><div class="item">Name</div>
                <input type="text" data-bind="value: $data.Name" />
            </div>                  
     </li> 
</ul>

Am I flat out doing this incorrectly? Or are user's object references to Locations and Photos possibly messing up the binding?

like image 911
verbose Avatar asked Nov 18 '12 09:11

verbose


1 Answers

Am I flat out doing this incorrectly?

Yes. You are overwriting your observableArray in the JSON callback, thus destroying it:

$.getJSON(baseUri, function (data) {
    self.users = data;
});

In general, knockout observables are assigned to like this:

$.getJSON(baseUri, function (data) {
    self.users(data.$values);
});

Note that in your case, data.$values seems to contain the actual array, not data itself.

This would work for your case, but it's not using knockout to its full potential.

The mapping plugin has been created for exactly this case. It recursively traverses a complex object and turns all its properties into observables and all contained arrays into observable arrays. This gives you fine-grained control over the state of the object, allowing you to monitor every change.

$.getJSON(baseUri, function (data) {
    ko.mapping.fromJS(data.$values, {}, self.users);
});

It also allows partial updates: Assume you retrieve a list of users from the server and your view model already has half of them. The mapping plugin is capable of adding only those to your view model that are missing and making appropriate changes to the existing ones. Read the "Advanced usage" section in the docs if you want to know how.


Assuming you get an array of user objects from the server and use the mapping plugin, then your binding would look like this:

<ul id="update-users" data-bind="foreach: users"> 
     <li> 
            <div><div class="item">User ID</div>
                <input type="text" data-bind="value: UserId" />
            </div>                 
            <div><div class="item">Name</div>
                <input type="text" data-bind="value: Name" />
            </div>
            <ul class="photos" data-bind="foreach: Photos.$values">
              <!-- ... -->
            <ul>
     </li> 
</ul>

On a general note, @Greg Smith's comment is correct. Try to lose the $ in the object keys, they might clash with special variables in knockout at some point. You could safely replace them with underscores, for example.

On a more general note, I'd try to strip out the whole $id/$values notion in your JSON if I were you, it does not seem to serve any purpose:

[
    {
        "Locations": [],
        "Photos": [],
        "UserId": 1,
        "Name": "Test User"
    }
]
like image 143
Tomalak Avatar answered Sep 20 '22 20:09

Tomalak