Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

making fields observable after ajax retrieval in knockout.js

I am wondering how I can make certain fields observables in knockout.js that I get from an ajax call without having to define the whole object in my viewmodel. Is this possible? Here is what I have so far:

var viewModel = {
    lines: new ko.observableArray([])
};
function refreshList(ionum) {
    var data = {};
    data['IONum'] = ionum;
    $.ajax({
    url: 'handlers/getlines.ashx',
    data: data,
    cache: false,
    dataType: 'json',
    success: function(msg) {

        viewModel.lines(msg);
        //here is where I am attempting to make the email address field observable
        /*for (var i = 0; i < msg.length; i++) {
            viewModel.lines()[i].emailAddress = new ko.observable(msg[i].emailAddress);

        }*/

        //alert(viewModel.lines[0].emailAddress);
        //ko.applyBindings(viewModel);


    }

});
}
like image 792
Chris Westbrook Avatar asked Dec 01 '10 17:12

Chris Westbrook


3 Answers

With the mapping plugin for Knockout you can define your view model from a plain Javascript object, as returned from an Ajax call:

var viewModel = ko.mapping.fromJS(data);
// Every time data is received from the server:
ko.mapping.updateFromJS(viewModel, data);

I had an almost similar situation, where my objects are instances of Javascript classes. The classes are defined without Knockout in mind, and I wanted to modify them to use observables instead.

I created a small helper to convert regular objects to observable objects. You can either specify the observable fields, or set them in the object (prototype). (I thought of doing this automatically, but I could not determine which field was safe to convert and which one was not.)

(function() {
    ko.observableObject = function(object, ko_fields) {
        ko_fields = ko_fields || object._ko_fields;
        if (!ko_fields) {
            return object;
        }
        for (var f_idx = 0; f_idx < ko_fields.length; f_idx++) {
            var field_name = ko_fields[f_idx];
            if (object[field_name] && object[field_name].__ko_proto__ !== undefined) {
                continue;
            }
            if (object[field_name] instanceof Array) {
                var field_array = object[field_name];
                for (var a_idx = 0; a_idx < field_array.length; a_idx++) {
                    field_array[a_idx] = ko.observableObject(field_array[a_idx]);
                }
                object[field_name] = ko.observableArray(field_array);
            } else {
                object[field_name] = ko.observable(object[field_name]);
            }
        }

        return object;
    };
})();

You can use it with classes or with plain objects.

// With classes. We define the classes without Knockout-observables
// User.subscriptions is an array of Subscription objects
User = function(id, name) {
    this.id = id;
    this.name = name;
    this.subscriptions = [];
};

Subscription = function(type, comment) {
    this.type = type;
    this.comment = comment;
});

// Create some objects
var jan = new User(74619, "Jan Fabry");
jan.subscriptions.push(new Subscription("Stack Overflow", "The start"));
jan.subscriptions.push(new Subscription("Wordpress Stack Exchange", "Blog knowledge"));
var chris = new User(16891, "Chris Westbrook");

// We would like to convert fields in our objects to observables
// Either define the fields directly:
ko.observableObject(jan, ['id', 'name', 'subscriptions']);
ko.observableObject(chris, ['id', 'name', 'subscriptions']);
// This will only convert the User objects, not the embedded subscriptions
// (since there is no mapping)

// If you define it in the class prototype, it will work for embedded objects too
User.prototype._ko_fields = ['id', 'name', 'subscriptions'];
Subscription.prototype._ko_fields = ['type', 'comment'];
ko.observableObject(jan);
ko.observableObject(chris);

// It also works with objects that are not created from a class, like your Ajax example
var observable = ko.observableObject({'id': 74619, 'name':'Jan'}, ['id', 'name']);
like image 89
Jan Fabry Avatar answered Nov 12 '22 05:11

Jan Fabry


I fixed this issue, found out it was better to declare the observable fields on my object before setting it to my view, like

for (var i=0;i<msg.lenth;i++){
msg[i].emailAddress=ko.observable(msg[i].emailAddress);
}
viewModel.lines(msg);
like image 3
Chris Westbrook Avatar answered Nov 12 '22 05:11

Chris Westbrook


Here is my CoffeeScript RequireJS module for converting what I call "view data" (server-derived JSON values) to KO ViewModels:

define "infrastructure/viewModels", [], (viewModels) ->
    exports = {}

    isDate = (x) ->
        typeof x is "string" and
        x.startsWith "/Date("

    deserializeDate = (dateString) ->
        new Date(parseInt(dateString.substr(6)))

    isScalar = (x) ->
        x is null or
        typeof x is "string" or
        typeof x is "number" or
        typeof x is "boolean"

    exports.fromViewData = (viewData) ->
        if isDate viewData
            return ko.observable deserializeDate viewData
        if isScalar viewData
            return ko.observable viewData

        viewModel = {}
        for own key, value of viewData
            if key is "id" then continue

            viewModel[key] = 
                if Array.isArray value
                    ko.observableArray (exports.fromViewData el for el in value)
                else
                    exports.fromViewData value

        return viewModel

    return exports

Sample usage:

require ["infrastructure/viewModels"], (viewModels) ->
    $.getJSON "/url/to/data", (viewData) ->
        viewModel = viewModels.fromViewData viewData
        ko.applyBindings viewModel

Of course, if you don't do CoffeeScript, you can translate to JavaScript by clicking "Try CoffeeScript" on the linked site. And if you don't use RequireJS, then just take the relevant functions from my module without wrapping them in define.

like image 1
Domenic Avatar answered Nov 12 '22 07:11

Domenic