72

Using backbone.js:

I have a top level ModelA that contains 2 attributes and 2 nested models, ModelB and ModelC. ModelB and ModelC each have 2 attributes as follows:

ModelA
    attributeA1
    attributeA2
    ModelB
        attributeB1
        attributeB2
    ModelC
        attributeC1
        attributeC2

There is a ViewA for ModelA, and a ViewB for ModelB. ViewA's render function places a new div onto the body, whereas ViewB's render creates an h1. ViewA's initialization calls ViewB's render to insert that h1 into the new div. The rationale behind this separation is that the h1 may change and require re-rendering independent from ViewA.

ViewA
    initialise: 
        //call ViewA's own render function
        this.render() 

        //call ViewB's render function that further modifies the $("#new") div created earlier.
        $("#new").append(ViewB.render().el)

    //ViewA's own render function
    render: //place <div id="new"></div> onto 'body'

ViewB
    render: //create a <h1></h1>
    funcB1: //can this access it's parent ModelA's attributes and other objects?

Q1: ViewB has a function funcB1. Can this function access it's parent model's attributes? Attributes such as attributeA1, or even attributeC1 (which would be a sibling/cousin)?

Q2: As a further expansion to Q1, can funcB1 access the DOM elements associated with ViewA? (in this example, the #new div?)

Q3: In general, how do I define the associations between the Views and Models as described above so that everything ties together properly?

I realize this question is somewhat abstract but any appreciate any help or guidelines appreciated.

fortuneRice
  • 4,214
  • 11
  • 43
  • 58
  • 1
    Have you tried writing tests to see if those answers your questions? – Spoike Jun 15 '11 at 06:44
  • I tried to play around with some code for Q1 and Q2 but couldn't get them to work. I figured the best way to ask this question was just in pseudocode and allow the more js-versed guys out there to point me in right direction. – fortuneRice Jun 15 '11 at 06:49
  • I'd suggest you write seperate questions with the failing code so that js programmers can point out what's wrong. – Spoike Jun 15 '11 at 06:52
  • Sure, that's fair, I'll try to write some. In any case, if anyone still wants to provide any input still welcome! – fortuneRice Jun 15 '11 at 17:36
  • 1
    Here's a related (although more generic) question & long answer for further insight on this: http://stackoverflow.com/questions/10077185/backbone-js-should-nested-views-maintain-references-to-each-other – B Robster Aug 09 '12 at 19:31

5 Answers5

73

To be able to reach attributes on related models, the model must have some kind of knowledge about what models it is related to. Backbone.js does not implicitly deal with relations or nesting, which means you must yourself make sure that the models have knowledge of each other. To answer your questions, one way to go about it is to make sure each child model has a 'parent' attribute. This way you can traverse the nesting first up to the parent and then down to any siblings that you know of.

To be more specific with your questions. When initializing modelA, you are probably creating modelB and modelC, I would suggest setting a link to the parent model when doing this, like this:

ModelA = Backbone.Model.extend({

    initialize: function(){
        this.modelB = new modelB();
        this.modelB.parent = this;
        this.modelC = new modelC();
        this.modelC.parent = this;
    }
}

This way you can reach the parent model in any child model function by calling this.parent.

Regarding your views, when doing nested backbone views, I find it easier to let each view represent one HTML tag by using the tagName option of the view. I would write your views as this:

ViewA = Backbone.View.extend({

    tagName: "div",
    id: "new",

    initialize: function(){
       this.viewB = new ViewB();
       this.viewB.parentView = this;
       $(this.el).append(this.viewB.el);
    }
});

ViewB = Backbone.View.extend({

    tagName: "h1",

    render: function(){
        $(this.el).html("Header text"); // or use this.options.headerText or equivalent
    },

    funcB1: function(){
        this.model.parent.doSomethingOnParent();
        this.model.parent.modelC.doSomethingOnSibling();
        $(this.parentView.el).shakeViolently();
    }

});

Then in your application initialization code (eg in your controller), I would initiate ViewA and place its element inside the body element.

DarkLeafyGreen
  • 69,338
  • 131
  • 383
  • 601
Jens Alm
  • 3,027
  • 4
  • 22
  • 24
  • I have a question if I may. When creating modelB in the `initialize` method of ModelA at the start of your answer, should it be set by `this.set('modelB') = new ModelB();` or it ok to not use the setter method in this instance? – David Tuite Sep 12 '11 at 04:32
  • 1
    You can use the setter method if you want the relation to be in the model's attributes (e.g. to be able to bind to change events). The syntax for that would be this.set({modelB: new ModelB()}). However, if you want to do more complex relation-mappings, I highly recommend backbone-relational by Paul Uhitol or some other relation-mapping plugin for backbone.js. – Jens Alm Sep 12 '11 at 16:31
  • 8
    Are there any memory leaks due to the fact that this creates a circular reference? – Scott Greenfield Dec 05 '11 at 17:41
  • Good comment, makes me revisit this answer. Good GC's handle circular references pretty well, but I'm not so sure about how IE8 handles it (it is prone to memory leaks, but mostly in js<->DOM couplings, use a library such as jQuery for DOM-manipulation!).In my own code I use – Jens Alm Dec 06 '11 at 12:41
  • In my own code I use backbone-relational for a more comprehensive approach. To handle views bound to models, I usually bind a view.remove to the "destroy" event of the bound model and override the remove-function to do all unbinding there to avoid memory leaks from bound functions. I also remove all child-views in a cascade on remove. I don't think this leads to memory leaks other than possibly in IE8, but I haven't profiled it. Watch out for DOM-connections in the views though. – Jens Alm Dec 06 '11 at 12:49
  • A view that sticks due to DOM-references may keep the whole nest of models in memory and that would easily become very leaky! – Jens Alm Dec 06 '11 at 12:49
  • 1
    Please note that your appending of B.el to A.el in your initialize method would be clobbered if ViewA had a render method; your example only works because the parent is a render-less view. – Teflon Ted Jun 04 '13 at 18:20
  • 1
    True, this is a caveat. I use this for controller-like views only – Jens Alm Jun 05 '13 at 10:08
3

The general answer to the question "Can I" is always "yes, as long as you're willing to write the code." The point behind Backbone is to provide a strong separation of model and view. If B1 has a reference to A1, and A1 has a reference to C1, then you're fully capable of creating methods and setting the rules by which B1 can modify A1 and C1 and so forth.

The views should be set up to receive CRUD events from their respective models. If the user does something with B1view that modifies B1model, and B1model in turn modifies A1model, then A1model should generate an event that A1view receives and causes a re-render of A1view, and so forth. It should happen like magic. (In practice, it takes some time to get the magic right, but I've found Backbone to be really powerful. And BackboneRelational helps with things like what you're describing here.)

Elf Sternberg
  • 16,129
  • 6
  • 60
  • 68
  • 14
    thanks. I think a more accurate answer to that should be "yes, as long as you're willing **and able** to write the code"! – fortuneRice Jun 15 '11 at 22:50
3

The above solution is on the right track but has some problems.

initialize: function(){
  this.viewB = new ViewB();
  this.viewB.parentView = this;
  $(this.el).append(this.viewB.el);    
}

Mainly, the model's toJSON() now returns stale data. I've posted a solution to fix this problem in a backbone.js plugin. You're welcome to use it.

Nathan
  • 1,762
  • 1
  • 20
  • 30
geddesign
  • 31
  • 1
2

You can use some extensions, Backbone-Forms https://github.com/powmedia/backbone-forms for example. To follow your use case define a schema like:

var ModelB = Backbone.Model.extend({
    schema: {
        attributeB1: 'Text',
        attributeB2: 'Text'
    }
});

var ModelC = Backbone.Model.extend({
    schema: {
        attributeC: 'Text',
    }
});

var ModelA = Backbone.Model.extend({
    schema: {
        attributeA1: 'Text',
        attributeA2: 'Text',
        refToModelB: { type: 'NestedModel', model: ModelB, template: 'templateB' },
        refToModelC: { type: 'NestedModel', model: ModelC, template: 'templateC' }
    }
});

Look at https://github.com/powmedia/backbone-forms#customising-templates for partial templates.

Important parts here are type: 'NestedModel' and template: 'templateXXX'.

This plugin has some limitations but you can look at others at https://github.com/documentcloud/backbone/wiki/Extensions%2C-Plugins%2C-Resources.

Artem Oboturov
  • 4,344
  • 2
  • 30
  • 48
0

There is backbone plugin Backbone-relational.js which provides one-to-one, one-to-many and many-to-one relations between models for Backbone.

I think this js will fulfill your needs. Vist BackboneRelational for more documentation.

Gagan
  • 4,278
  • 7
  • 46
  • 71