Backbone.js get and set nested object attribute

Backbone.js get and set nested object attribute

I have a simple question about Backbone.js’ get and set functions.
1) With the code below, how can I ‘get’ or ‘set’ obj1.myAttribute1 directly?
Another question:
2) In the Model, aside from the defaults object, where can/should I declare my model’s other attributes, such that they can be accessed via Backbone’s get and set methods?
var MyModel = Backbone.Model.extend({
defaults: {
obj1 : {
“myAttribute1” : false,
“myAttribute2” : true,
}
}
})

var MyView = Backbone.View.extend({
myFunc: function(){
console.log(this.model.get(“obj1”));
//returns the obj1 object
//but how do I get obj1.myAttribute1 directly so that it returns false?
}
});

I know I can do:
this.model.get(“obj1”).myAttribute1;

but is that good practice?

Solutions/Answers:

Solution 1:

While this.model.get("obj1").myAttribute1 is fine, it’s a bit problematic because then you might be tempted to do the same type of thing for set, i.e.

this.model.get("obj1").myAttribute1 = true;

But if you do this, you won’t get the benefits of Backbone models for myAttribute1, like change events or validation.

A better solution would be to never nest POJSOs (“plain old JavaScript objects”) in your models, and instead nest custom model classes. So it would look something like this:

var Obj = Backbone.Model.extend({
    defaults: {
        myAttribute1: false,
        myAttribute2: true
    }
});

var MyModel = Backbone.Model.extend({
    initialize: function () {
        this.set("obj1", new Obj());
    }
});

Then the accessing code would be

var x = this.model.get("obj1").get("myAttribute1");

but more importantly the setting code would be

this.model.get("obj1").set({ myAttribute1: true });

which will fire appropriate change events and the like. Working example here: http://jsfiddle.net/g3U7j/

Solution 2:

I created backbone-deep-model for this – just extend Backbone.DeepModel instead of Backbone.Model and you can then use paths to get/set nested model attributes. It maintains change events too.

model.bind('change:user.name.first', function(){...});
model.set({'user.name.first': 'Eric'});
model.get('user.name.first'); //Eric

Solution 3:

Domenic’s solution will work however each new MyModel will point to the same instance of Obj.
To avoid this, MyModel should look like:

var MyModel = Backbone.Model.extend({
  initialize: function() {
     myDefaults = {
       obj1: new Obj()
     } 
     this.set(myDefaults);
  }
});

See c3rin’s answer @ https://stackoverflow.com/a/6364480/1072653 for a full explanation.

Solution 4:

I use this approach.

If you have a Backbone model like this:

var nestedAttrModel = new Backbone.Model({
    a: {b: 1, c: 2}
});

You can set the attribute “a.b” with:

var _a = _.omit(nestedAttrModel.get('a')); // from underscore.js
_a.b = 3;
nestedAttrModel.set('a', _a);

Now your model will have attributes like:

{a: {b: 3, c: 2}}

with the “change” event fired.

Solution 5:

There is one solution nobody thought of yet which is lots to use. You indeed can’t set nested attributes directly, unless you use a third party library which you probably don’t want. However what you can do is make a clone of the original dictionary, set the nested property there and than set that whole dictionary. Piece of cake.

//How model.obj1 looks like
obj1: {
    myAttribute1: false,
    myAttribute2: true,
    anotherNestedDict: {
        myAttribute3: false
    }
}

//Make a clone of it
var cloneOfObject1 = JSON.parse(JSON.stringify(this.model.get('obj1')));

//Let's day we want to change myAttribute1 to false and myAttribute3 to true
cloneOfObject1.myAttribute2 = false;
cloneOfObject1.anotherNestedDict.myAttribute3 = true;

//And now we set the whole dictionary
this.model.set('obj1', cloneOfObject1);

//Job done, happy birthday

Solution 6:

I had the same problem @pagewil and @Benno had with @Domenic’s solution. My answer was to instead write a simple sub-class of Backbone.Model that fixes the problem.

// Special model implementation that allows you to easily nest Backbone models as properties.
Backbone.NestedModel = Backbone.Model.extend({
    // Define Backbone models that are present in properties
    // Expected Format:
    // [{key: 'courses', model: Course}]
    models: [],

    set: function(key, value, options) {
        var attrs, attr, val;

        if (_.isObject(key) || key == null) {
            attrs = key;
            options = value;
        } else {
            attrs = {};
            attrs[key] = value;
        }

        _.each(this.models, function(item){
            if (_.isObject(attrs[item.key])) {
                attrs[item.key] = new item.model(attrs[item.key]);
            }
        },this);

        return Backbone.Model.prototype.set.call(this, attrs, options);
    }
});

var Obj = Backbone.Model.extend({
    defaults: {
        myAttribute1: false,
        myAttribute2: true
    }
});

var MyModel = Backbone.NestedModel.extend({
    defaults: {
        obj1: new Obj()
    },

    models: [{key: 'obj1', model: Obj}]
});

What NestedModel does for you is allow these to work (which is what happens when myModel gets set via JSON data):

var myModel = new MyModel();
myModel.set({ obj1: { myAttribute1: 'abc', myAttribute2: 'xyz' } });
myModel.set('obj1', { myAttribute1: 123, myAttribute2: 456 });

It would be easy to generate the models list automatically in initialize, but this solution was good enough for me.