Search code examples
javascriptbackbone.jsnestedbackbone.js-collections

How to serialize nested Backbone's collections and models?


In my project, I use Backbone collection to arrange the application's data and I also need to sync my data to localstorage. My data is a two level deep nested collections and models, and there is where the problem happens.

After the inner collection synchronizes to localstorage, it becomes a raw array of objects. So the collection method (like add) can not be used.

After debugging and googling, I found the reason:

When localStorage serializes the model, it calls model.toJSON(), and it simply clones the model's attributes, excluding the nested collection.

// Return a copy of the model's `attributes` object.
toJSON: function(options) {
  return _.clone(this.attributes);
},

so it uses Underscore's clone function, and the doc says that it:

Create a shallow-copied clone of the provided plain object. Any nested objects or arrays will be copied by reference, not duplicated.

So I am searching a deep copy method to override the default model's .toJSON. But I can't figure out the correct way.

For example, I tried the following:

Backbone.Model.prototype.toJSON = function() {
    var json = $.extend(true, {}, this.attributes);
    return json;
};

Edit, based on the Emile's suggestion, My real Model goes as following:

app.RecordItem = Backbone.Model.extend({

    defaults: {
        target: 1,
        date: '',
        day: '',
        //foodlist: new TrackList(),
        currentvalue: 0,
        isSetup: false,
    },
    initialize: function() {

        var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
        var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
        var d = new Date();
        this.set("date", d.getDate() + "." + months[d.getMonth()]);
        this.set("day", days[d.getDay()]);
        //
        var foodlist = this.getFoodlist();
        if (!(foodlist instanceof TrackList)) {
            this.set('foodlist', new TrackList(foodlist));
        }
    },
    getFoodlist: function() {
        if (!this.foodlist) this.foodlist = new TrackList(this.get('foodlist'));
        return this.get('foodlist');
    },

    toJSON: function(options) {
        // this gets the default behavior
        var attrs = this.constructor.__super__.toJSON.apply(this, arguments);
        var foodlist = attrs.foodlist;
        if (foodlist) {
            // then replace the collection reference with the actual serialized data
            attrs.foodlist = foodlist.toJSON(options);
        }
        return attrs;
    },
});

After override the toJSON method. The error message is

"foodlist.toJSON is not a function(…)"

Solution

  • While jQuery's extend offers deep copy, it's not what you need, and here's why:

    The localStorage stores strings so it's needed to serialize to JSON. Functions won't be serialized as they're not valid in JSON1.

    So it's not a good idea to try to serialize a whole Backbone collection or model, and instead, serialize only the data and to instantiate the nested structure back when deserializing the data

    Backbone's toJSON

    This can be used for persistence, serialization, or for augmentation before being sent to the server. The name of this method is a bit confusing, as it doesn't actually return a JSON string — but I'm afraid that it's the way that the JavaScript API for JSON.stringify works.

    The default toJSON behavior is to make a shallow copy of the model's attributes. Since you're nesting models and collections, you need to change the serialization to take the nesting into account.

    A simple way to accomplish this is to override toJSON to call the toJSON function of every nested collections and models within the attributes hash.

    var Daymodel = Backbone.Model.extend({
        defaults: { day: 1, },
        initialize: function(attrs, options) {
            var agenda = this.getAgenda();
            if (!(agenda instanceof Todocollection)) {
                // you probably don't want a 'change' event here, so silent it is.
                return this.set('agenda', new Todocollection(agenda), { silent: true });
            }
        },
        /**
         * Parse can overwrite attributes, so you must ensure it's a collection
         * here as well.
         */
        parse: function(response) {
            if (_.has(response, 'agenda')) {
                response.agenda = new Todocollection(response.agenda);
            }
            return response;
        },
        toJSON: function(options) {
            var attrs = Daymodel.__super__.toJSON.apply(this, arguments),
                agenda = attrs.agenda;
            if (agenda) {
                attrs.agenda = agenda.toJSON(options);
            }
            return attrs;
        },
        getAgenda: function() {
            return this.get('agenda');
        },
        setAgenda: function(models, options) {
            return this.getAgenda().set(models, options);
        },
    });
    

    Additional information:


    1 While not impossible to serialize a function to a string and to deserialize it with eval, it's not a good idea and completely unnecessary here.