Knockout.js Make every nested object an Observable

I am using Knockout.js as a MVVM library to bind my data to some pages. I'm currently building a library to make REST calls to a web service. My RESTful web service returns a simple structure:

{
    id : 1,
    details: {
        name: "Johnny",
        surname: "Boy"
    }
}

I have an observable main parent, myObject. When I do

myObject(ko.mapping.fromJS(data))

the observables in myObject are:

  • id
  • name
  • surname

How can I make details (and theoretically any object in the structure an observable)? I need this behavior so that i can set a computed observable on details and get noticed as soon as any of the internal data changes.

I have set up a basic recursive function which should do the trick. It doesn't, of course, myObject.details doesn't become an observable.

// Makes every object in the tree an observable.
var makeAllObservables = function () {
    makeChildrenObservables(myObject);
};
var makeChildrenObservables = function (object) {
    // Make the parent an observable if it's not already
    if (!ko.isObservable(object)) {
        if ($.isArray(object))
            object = ko.observableArray(object);
        else
            object = ko.observable(object);
    }
    // Loop through its children
    for (var child in object()) {
        makeChildrenObservables(object()[child]);
    }
};

I'm pretty sure it's something about incorrect references, but how can I solve this? Thank you.

Answers:

Answer

I don't think knockout has a built-in way to observe changes to child elements. If I understand your question, when someone changes the name you want a change to details as an entity to be noticed. Can you give a concrete example of how you would use this? Would you use a subscription to the details observable to perform some action?

The reason your code doesn't make details an observable is because javascript is pass by value, so changing the value of the 'object' argument in your function doesn't change the actual value you passed, only the value of the argument inside your function.

Edit

If changes will automatically propagate to the parents, this should make all children observable I think, but your root that you pass the first time should already be an observable.

// object should already be observable
var makeChildrenObservables = function (object) {
    if(!ko.isObservable(object)) return;

    // Loop through its children
    for (var child in object()) {
        if (!ko.isObservable(object()[child])) {
            object()[child] = ko.observable(object()[child]);
        }
        makeChildrenObservables(object()[child]);
    }
};
Answer

I would use the knockout mapping plugin.

var jsonData = {
    id : 1,
    details: {
        name: "Johnny",
        surname: "Boy"
    }
}

var yourMapping = {
    'details': {
        create: function(options) {
            return Details(options.data);
        }
    }
}

function Details(data) {
    ko.mapping.fromJS(data, {}, this);
}

function YourObjectName() {
    ko.mapping.fromJS(jsonData, yourMapping, this);
}

This will create your object hierarchy with all of the children as observables.

Answer

From what I have experienced, ko.mapping.fromJS does not make an observable out of an object.

Let's say you have this ViewModel constructor:

var VM = function(payload) {
  ko.mapping.fromJS(payload, {}, this);
}

and this data object:

var data1 = {
  name: 'Bob',
  class: {
    name: 'CompSci 101',
    room: 112
  }

}

and you use data1 to create VM1:

var VM1 = new VM(data1);

Then VM1.class will not be a ko.observable, it is a plain javascript Object.

If you then create another viewmodel using a data object with a null class member, ie:

var data2 = {
  name: 'Bob',
  class: null
}
var VM2 = new VM(data2);

then VM2.class is a ko.observable.

If you then execute:

ko.mapping(data1, {}, VM2)

then VM2.class remains a ko.observable.

So, if you create a ViewModel from a seed data object where object members are null, and then popuplate them with a populated data object, you will have observable class members.

This leads to problems, because sometimes the object members are observables, and sometimes they are not. Form bindings will work with VM1 and not work with VM2. It would be nice if ko.mapping.fromJS always made everything a ko.observable so it was consistent?

Answer

By using Knockout-Plugin we can make child elements observable .We have lot of options to manage how we want our data to make observable.

Here is a sample code :

var data = {
    people: [
        {
            id: 1,
            age: 25,
            child : [
                {id : 1,childname : "Alice"},
                {id : 2,childname : "Wonderland"}
            ]
        }, 
        {id: 2, age: 35}
    ],
    Address:[
        {
            AddressID : 1,
            City : "NewYork",
            cities : [
                {
                    cityId : 1,
                    cityName : "NewYork"
                },
                {
                    cityId :2,
                    cityName : "California"
                }
            ]
        },
        {
            AddressID : 2,
            City : "California",
            cities : [
                {
                    cityId :1,
                    cityName : "NewYork"
                },
                {
                    cityId :2,
                    cityName : "California"
                }
            ]
        }
    ],
    isSelected : true,
    dataID : 6
};
var mappingOptions = {
    people: {
        create: function(options) {
            console.log(options);
            return ko.mapping.fromJS(options.data, childmappingOptions);
        }
    },
    Address: {
        create: function(options) {
            console.log(options);
            return ko.mapping.fromJS(options.data, childmappingOptions);
        }
    }
};
var childmappingOptions = {
    child: {
        create: function(options) {
            return ko.mapping.fromJS(options.data, { observe: ["id","childname"]});
        }
    },
    cities :{
        create: function(options) {
            return ko.mapping.fromJS(options.data, { observe: ["cityId","cityName"]});
        }
    }
};
var r = ko.mapping.fromJS(data, mappingOptions);

I have attached a working fiddle: http://jsfiddle.net/wmqTx/5/

Answer

I will extend Paolo del Mundo's answer (which I think it might easily be the best and the only solution at this moment) with my example solution.

Consider frapontillo's original object:

{
    id : 1,
    details: {
        name: "Johnny",
        surname: "Boy"
    }
}

The details property itself is an object and as such CAN'T be an observable. The same goes for the User property in the example below, which is also an object. Those two objects cannot be observables but their LEAF properties can be.

Every leaf property of your data tree / model CAN BE AN OBSERVABLE. The easiest way to achieve that is that you properly define the mapping model before passing it to the mapping plugin as parameter.

See my example below.

EXAMPLE:

Let's say we need to show an html page / view where we have a list of users on a grid. Beside the Users grid a form for editing a selected user from the grid is shown.

STEP 1: DEFINING THE MODELS

function UsersEdit() {
    this.User = new User();                    // model for the selected user      
    this.ShowUsersGrid = ko.observable(false); // defines the grid's visibility (false by default)
    this.ShowEditForm = ko.observable(false);  // defines the selected user form's visibility (false by default)      
    this.AllGroups = [];                       // NOT AN OBSERVABLE - when editing a user in the user's form beside the grid a multiselect of all available GROUPS is shown (to place the user in one or multiple groups)
    this.AllRoles = [];                        // NOT AN OBSERVABLE - when editing a user in the user's form beside the grid a multiselect of all available ROLES is shown (to assign the user one or multiple roles)
}

function User() {
    this.Id = ko.observable();
    this.Name = ko.observable();
    this.Surname = ko.observable();
    this.Username = ko.observable();
    this.GroupIds = ko.observableArray(); // the ids of the GROUPS that this user belongs to
    this.RoleIds = ko.observableArray();  // the ids of the ROLES that this user has
}

STEP 2: MAPPING (TO GET NESTED OBSERVABLES)

Let's say this is your raw JSON model with data that you want to map and get a KO model with nested observables.

var model = {
    User: {
        Id: 1,
        Name: "Johnny",            
        Surname = "Boy",
        Username = "JohhnyBoy",
        GroupIds = [1, 3, 4],
        RoleIds = [1, 2, 5]
    }
};

Now that all of this is defined, you can map:

var observableUserEditModel = ko.mapping.fromJS(model, new UsersEdit());

AND YOU'RE DONE! :)

The observableUserEditModel will hold all of your observables, even nested ones. Now the only thing you need to take care of in order to test this is to bind the observableUserEditModel object with your HTML. Hint: use the with binding and test the observable observableUserEditModel data structure inserting this in your HTML view:

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

Maybe in a future version there could be a configuration option that causes ko.mapping.fromJS to always create observables. It could be enabled for new projects or after updating bindings of an existing project.

What I do to prevent this problem is ensure that model seeds always have Objects member properties populated, at every level. In this way, all object properties are mapped as POJOs (Plain Old Javascript Objects), so the ViewModel doesn't initialize them as ko.observables. It avoids the "sometimes observables, sometimes not" issue.

Best regards, Mike

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.