Search code examples
knockout.jsknockout-mapping-pluginknockout-2.0

Bind viewmodel to ui after ajax call succeeds Knockout


I have a selectall toggle checkbox and checkbox in each row of data .

Now the data which is returned from the server doesnt have isSelected Observable. I have added 'isSelected' observable for each row . But the isSelected observable doesnt get bound to the checkboxes in each row .

Here is the viewmodel :

var folderViewModel = function () {
    var self = this;
    self.Folders = ['Inbox', 'Archive', 'Sent', 'Spam'];
    self.SelectedFolder = ko.observable();
    self.Mails = ko.observableArray([]);
    self.SelectedMail = ko.observable();
    self.SelectAll = ko.observable(false);

    self.navigate = function (folder) {
        self.SelectedFolder(folder);
        //$.get('/Api/MailBox', { folder: folder }, self.Mails);

        $.ajax({
            url: "/Api/Mailbox",
            data: { folder: folder },
            success: function (data) {
                ko.mapping.fromJS(data, {}, self.Mails);
                ko.utils.arrayForEach(self.Mails(), function (mail) {
                    mail.isSelected = ko.observable(true);
                    mail.isSelected.subscribe(function (myvalue) {
                        console.log(myvalue);
                    });
                });
                console.log(ko.toJSON(self.Mails()));
            },
            statusCode: {
                404: function () {
                    alert("No Mail");
                }
            }
        });

        //ko.mapping.fromJS(data, {}, self.Mails);
        //console.log(ko.toJSON(self.Mails));
    };

    self.SelectAll.subscribe(function (newValue) {
        ko.utils.arrayForEach(self.Mails(), function (mail) {
            console.log(mail.isSelected());
            mail.isSelected(newValue);

        });
        console.log(newValue);
    }, self);

    this.navigate("Inbox");
};
ko.applyBindings(new folderViewModel());

And here is the binding .

<table class="table table-bordered table-striped table-condensed table-hover">
<thead>
    <tr>
        <th>
            <input type="checkbox" data-bind="checked: SelectAll"/>
            @*<input type="checkbox" />*@
        </th>
        <th>
            From
        </th>
        <th>
            To
        </th>
        <th>
            Subject
        </th>
        <th>
            Date
        </th>
    </tr>
</thead>
<tbody data-bind="foreach:Mails">
    <tr data-bind="click:$root.navigateToMail">
        <td style="width: 15px">
            <input type="checkbox" data-bind="checked: $root.isSelected">
            @*<input type="checkbox">*@
        </td>
        <td data-bind="text: From">
        </td>
        <td data-bind="text: To">
        </td>
        <td data-bind="text: Subject">
        </td>
        <td data-bind="text: MailDate">
        </td>
    </tr>
</tbody>

The checkbox <input type="checkbox" data-bind="checked: $root.isSelected"> is not getting bound to the ajax data in mails.isSelected=ko.obsevable(true). What might be the problem ?


Solution

  • First, kuddos for using the learn.knockoutjs.com example, amazing resource.

    Your error is a common pitfall in KnockoutJS: you are modifying your model without updating the observables that are binded to it. See the following lines -

    ko.mapping.fromJS(data, {}, self.Mails);
    ko.utils.arrayForEach(self.Mails(), function (mail) { ... });
    

    If you see, you are first creating your model for the mails and THEN adding up an extra observable. To which context this observable should be attached to? Without updating the values of each mail, your mail.isSelected is never added to the mail object.

    There are two ways to solve this. The first one, you update the mail model in the forEach array:

    ko.utils.arrayForEach(self.Mails(), function (mail, index) { 
        // Add up the isSelected observable
        self.Mails()[index] = mail; 
    });
    

    This has a performance gotcha on observable arrays that you can read here. Basically, you want to create a temp array and update the entire observable array than call the Mails() each time.

    The other way is quite simple: swap the order of how are you creating the mail model:

    ko.utils.arrayForEach(self.Mails(), function (mail) { ... });
    ko.mapping.fromJS(data, {}, self.Mails);
    

    FIRST, you are attaching to a data object your observable and THEN you are telling to your viewmodel "Hey! We got a new model to save it has this properties from data PLUS an observable isSelected I just added, care to bind it up?"

    That being said, the property in the HTML is checked: isSelected and not checked: $root.isSelected, which was the reason why it was sort of working on the SelectAll (sort of), as the isSelected was being binded to your ViewModel instead of your mail model. You could had figure it out debugging with this handy statement:

    // In any row inside your data
    <td data-bind="text: ko.toJSON($data)">DEBUG DATA</td>
    

    That should be it, you can see your code working here as well as all its solution: http://jsfiddle.net/jjperezaguinaga/VTuHA/. I added some sample data to play around (with the /echo/json/ feature from JsFiddle) and removed some stuff, as well as an extra column that I used to debug with the last code I put. In this last column you can see the value isSelected being updated every time you click on a checked box.