Memet Olsen
Memet Olsen

Reputation: 4608

Troubles with parsing nested knockout viewmodels to JSON

The problem

As I was working and exploring knockoutjs, I got stuck at some point. I want to serialize a viewmodel (and underlying viewmodels) to JSON. This will end up in an infinite loop because the child viewmodels have a property which references the parent viewmodel. What is the best practice for solving this issue?

The code

var Partner = function (parent) {
    var self = this;
    self.parent = parent;
    self.name = ko.observable('');
}

var ProjectViewModel = function () {
    var self = this;
    self.nr = ko.observable(0);
    self.tite = ko.observable('');
    self.partners = ko.observableArray();            

    self.addPartner = function () { self.partners.push(new Partner(self)) };
    self.removePartner = function (c) { self.partners.remove(c) };
};
var vm = new ProjectViewModel();
ko.applyBindings(vm);


$("#button").click(function () {
    alert(ko.toJSON(vm));
}

What I tried so far

I tried adding the following method in the Partner viewmodel:

Partner.prototype.toJSON = function () {
            var copy = ko.toJS(self);
            delete copy.parent;
            return copy;
        }

This works with only one Partner, if the ProjectViewModel has multipe partners, every partner will have the same value as the last partner. This happens only when I want to serialize it to JSON.

Upvotes: 2

Views: 560

Answers (1)

RP Niemeyer
RP Niemeyer

Reputation: 114792

There are several ways to handle this situation. The ko.toJS part of KO handles this properly. It is ultimately JSON.stringify (called after ko.toJS in ko.toJSON that causes the error.

Your toJSON method on your prototype is pretty close, except that you want to be dealing with this rather than self.

So, it would look like:

Partner.prototype.toJSON = function() {
  var copy = ko.toJS(this);
  delete copy.parent;
  return copy; 
};

Other ways to handle it:

1- don't actually store your parent on the child object and just reference it in any handlers directly based on the argument passed to the constructor.

var Partner = function (parent) {
    var self = this;
    self.name = ko.observable(name);

    self.doSomething = function() {
        //can use "parent" directly here without storing it anywhere
    };
};

2- "hide" your parent reference

var Partner = function (parent) {
    var self = this;
    self.meta = function() {};
    self.meta.parent = parent;
    self.name = ko.observable(name);    
};

You can place your parent value behind a function (could be as a sub-property of an observable). When your structure is turned into a plain JS object by ko.toJS, then any sub-properties of functions are lost.

3- change the structure of your application, depending on what you want to do. Suppose that the parent wants to react whenever a child has it's name changed. You could pass in a callback, setup the subscription, and execute it whenever it changes

var Partner = function (nameChangedCallback) {
    var self = this;
    self.name = ko.observable(name);    

    if (typeof nameChangedCallback == "function") {
        self.name.subscribe(function() {
           nameChangedCallback.call(self, self);
        });
    }
};

Upvotes: 2

Related Questions