107

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?

grc
  • 22,885
  • 5
  • 42
  • 63
fortuneRice
  • 4,214
  • 11
  • 43
  • 58
  • 3
    While it isn't an answer to the question: Whenever specifying an object (anything passed by reference) in `defaults` (obj1 in this case), that _same_ object will be shared across all instances of the model. The current practice is to define `defaults` as a function that returns an object to be used as defaults. http://backbonejs.org/#Model-defaults (see the italicized note) – freethejazz Feb 19 '14 at 20:54
  • 1
    @JonathanF Comments aren't meant for *answers* so you never needed the declaration :) – T J Nov 26 '14 at 18:05

9 Answers9

145

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/

Domenic
  • 110,262
  • 41
  • 219
  • 271
  • 24
    To this answer, I would add the advisement that this solution teeters on widespread Law of Demeter violations. I would consider adding convenience methods that hide the navigation to the nested object. Basically, your callers don't need to know the model's internal structure; after all, it may change and the callers should be none the wiser. – Bill Eisenhauer Jun 15 '11 at 00:54
  • 7
    Can't get this to work for me. Throws error: `Uncaught TypeError: Object # has no method 'set'` – wilsonpage Oct 14 '11 at 15:15
  • Yeah, this doesn't work for me either... I get the same error as pagewil. I was doing: `window.Helper = Backbone.Model.extend({ defaults: { "type": null, "notes": [] } }); window.Events = Backbone.Model.extend({ defaults: { "title": null, "data": new Helper() } } });` (didnt work with `model.get("data").get("type")`) – Benno Jun 04 '12 at 00:21
  • get("attr") returns a plain object, not a Backbone model, so get("attr").set({any: "thing"}) doesn't work. – Christian Nunciato Jun 25 '12 at 23:33
  • 1
    @ChristianNunciato, pagewil, Benno: You seem to have missed the point of the post, which is to nest Backbone models inside Backbone models. Don't nest plain objects inside Backbone models. Working example here: http://jsfiddle.net/g3U7j/ – Domenic Jun 26 '12 at 13:23
  • 1
    I didn't inspect backbone.js code, but from my test, if you have a nested custom Model and change a property of it with set(), its parent model will not fire a 'change' event itself; I had to fire the event myself. I really should just inspect the code, but is this your understanding too? – tom Jan 01 '13 at 20:53
  • 2
    @tom that is correct. Backbone doesn't special-case for when properties of models are instances of `Backbone.Model`, and then start doing magical event bubbling. – Domenic Jan 02 '13 at 04:38
  • @Domenic: I have tried it, and the view doesn't change (render is not called) when model of a model is changed. How to connect them to View's render? – maximus Jun 03 '13 at 10:37
  • @Domenic: For example, do I have to call this.listenTo(this.model, "change", this.render); then to call: this.listenTo(this.model.get("obj1"), "change", this.render); etc? – maximus Jun 03 '13 at 11:19
  • @Domenic If we model our data like you describe above, we have to customize model's parse function to set values to obj1. Am i correct? – user10 Aug 27 '13 at 07:34
  • @Domenic while doing something similar this.model.get("eventListener").set({"id":2}) Following exception was thrown: "Uncaught TypeError: this.model.get(...).set is not a function" Same goes for using nested get() . – ambar Jun 16 '15 at 14:02
  • I'm confused. I'm simply trying to access attributes of a model, but seeing `get(...).get is not a function`. From the model, I have to `get('attributes')` then `get('attribute')` (For context, this is in a #groupBy loop called on a Backbone collection, in which the context passed is a Backbone model of the collection). – allthesignals Oct 06 '15 at 20:05
  • How would you go about and listen to change events of e.g. myAttribute1? – Setup Jul 17 '17 at 13:19
75

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
evilcelery
  • 15,941
  • 8
  • 42
  • 54
  • 1
    Yes it does, if you look at the [API](https://github.com/powmedia/backbone-deep-model) there is an example like `//You can use index notation to fetch from arrays console.log(model.get('otherSpies.0.name')) //'Lana'` – tawheed Nov 13 '13 at 13:30
  • Works great! But does line 2 in your example require a colon instead of a comma? – mariachi Feb 10 '14 at 23:56
16

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.

Community
  • 1
  • 1
Rusty
  • 186
  • 1
  • 4
5

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.

  • 1
    Are you sure about this? this does not work for me. `meta2= m.get('x'); meta2.id=110; m.set('x', meta2)`. This does not trigger any change event for me :( – HungryCoder Jun 12 '13 at 09:25
  • 1
    I see it works when I clone the attribute like `_.clone(m.get('x'))`. thanks – HungryCoder Jun 12 '13 at 09:26
  • Thanks @HungryCoder it worked for me too when cloned. Backbone must compare the object you are `setting` with the object you are `getting` at set time. So if you don't clone then the two objects, then the two objects being compared are exactly the same at set time. – Derek Dahmer Oct 24 '13 at 22:33
  • Remember that objects are passed by reference and are mutable, unlike string and number primitives. Backbone's set and constructor methods attempt a shallow clone of any object reference passed as an argument. Any references to other objects in properties of that object aren't cloned. When you set it and retrieve it the reference is the same, which means you can mutate the model without triggering a change. – niall.campbell May 01 '18 at 03:33
3

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
bicycle
  • 8,315
  • 9
  • 52
  • 72
2

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.

Cory Gagliardi
  • 770
  • 1
  • 8
  • 13
2

Solution proposed by Domenic has some drawbacks. Say you want to listen to 'change' event. In that case 'initialize' method will not be fired and your custom value for attribute will be replaced with json object from server. In my project I faced with this problem. My solution to override 'set' method of Model:

set: function(key, val, options) {
    if (typeof key === 'object') {
        var attrs = key;
        attrs.content = new module.BaseItem(attrs.content || {});
        attrs.children = new module.MenuItems(attrs.children || []);
    }

    return Backbone.Model.prototype.set.call(this, key, val, options);
}, 
yuliskov
  • 1,379
  • 15
  • 16
1

While in some cases using Backbone models instead of nested Object attributes makes sense as Domenic mentioned, in simpler cases you could create a setter function in the model:

var MyModel = Backbone.Model.extend({
    defaults: {
        obj1 : {
            "myAttribute1" : false,
            "myAttribute2" : true,
        }
    },
    setObj1Attribute: function(name, value) {
        var obj1 = this.get('obj1');
        obj1[name] = value;
        this.set('obj1', obj1);
    }
})
Ibrahim Muhammad
  • 2,808
  • 4
  • 29
  • 39
0

If you interact with backend, which requires object with nesting structure. But with backbone more easy to work with linear structure.

backbone.linear can help you.

darky
  • 163
  • 1
  • 7