Knockout.js Make every nested object an Observable
Asked Answered
S

6

35

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.

Shot answered 11/5, 2012 at 16:21 Comment(1)
Not a direct answer, but can.Observe in CanJS builds a nested observable exactly as you describe.Vaughnvaught
C
14

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]);
    }
};
Cosmopolis answered 12/5, 2012 at 6:47 Comment(5)
I need this so I can change the color on a frame containing both name and surname. If any of the two changes, I set it to yellow. This is just an example, but that's what I have in mind. Knockout.js triggers a change for the parent if any of the children was changed, so yes, it would work. And is there a way to pass the object by reference, not by value? What if I use some kind of extension function (jQuery.extend maybe?) that does not change the original pointer but the value it references?Shot
By using computed observables, it works. As soon as you change either the firstName or lastName, fullName gets triggered. This way you can track changes in any of elements in the structure. The function @jason-goemaat posted works (thank you!), but there's the need to know if it has already been applied. I have setup a function that sets the JS and makes everything an observable by itself. :DShot
How that works is that knockout runs the computed and traces what is accessed, adding a dependency for each. If you're using the individual observables in your computed anyway, there's no point in making the parent observable. I think what you need to do to make this automatic would be to create a function on your roar (or each object) and subscribe all children to it...Cosmopolis
This was just an example, what I'm going to do in the computed is some sort of change tracking, the computed will evaluate the ko.mapping.toJSON of the parent object to see if there are any changes in the inner structure, so that I can undo any nested object.Shot
As promised, this is the result of what I asked here. It's an extension to knockout.js that provides REST methods to entities and a nice undo.Shot
D
23

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.

Dey answered 11/5, 2012 at 18:54 Comment(3)
Yes, I had already thought about this solution, the thing is I need to do that for many structures much bigger than the on in this example, and I'm looking for a way to do it automatically.Shot
This is propably the only and the best way of doing it that I know of unfortunately. It means that you need to model your data ahead of coding.Art
I agree, it perhaps is cumbersome work, because it means you need to have a model pre-defined for every type of an object that you want to map / have observables in. But think... there must be a way to say to Knockout how to map properties. It's not magic you know. :) I think in long term modelling is good for maintainability reasons. Also imagine you use TypeScript. In TypeScript the objects must be strongly typed thus pre-defined ahead. So modeling is a must on the client-side as well.Art
C
14

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]);
    }
};
Cosmopolis answered 12/5, 2012 at 6:47 Comment(5)
I need this so I can change the color on a frame containing both name and surname. If any of the two changes, I set it to yellow. This is just an example, but that's what I have in mind. Knockout.js triggers a change for the parent if any of the children was changed, so yes, it would work. And is there a way to pass the object by reference, not by value? What if I use some kind of extension function (jQuery.extend maybe?) that does not change the original pointer but the value it references?Shot
By using computed observables, it works. As soon as you change either the firstName or lastName, fullName gets triggered. This way you can track changes in any of elements in the structure. The function @jason-goemaat posted works (thank you!), but there's the need to know if it has already been applied. I have setup a function that sets the JS and makes everything an observable by itself. :DShot
How that works is that knockout runs the computed and traces what is accessed, adding a dependency for each. If you're using the individual observables in your computed anyway, there's no point in making the parent observable. I think what you need to do to make this automatic would be to create a function on your roar (or each object) and subscribe all children to it...Cosmopolis
This was just an example, what I'm going to do in the computed is some sort of change tracking, the computed will evaluate the ko.mapping.toJSON of the parent object to see if there are any changes in the inner structure, so that I can undo any nested object.Shot
As promised, this is the result of what I asked here. It's an extension to knockout.js that provides REST methods to entities and a nice undo.Shot
M
3

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?

Meraree answered 27/2, 2014 at 3:23 Comment(0)
P
3

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/

Pedology answered 10/7, 2014 at 7:30 Comment(0)
A
2

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>
Art answered 4/4, 2015 at 13:59 Comment(1)
"The details property itself is an object and as such CAN'T be an observable." What do you mean? Of course observables can contain objects.Adulteration
M
1

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

Meraree answered 28/2, 2014 at 3:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.