Igor Timoshenko
Igor Timoshenko

Reputation: 1011

How to render nested views in Backbone.js?

I have a tree of categories and I would like to do rending using by Backbone.js instead of using jQuery or do rendering on the server-side. I have the following breakout that I described as template:

<li>
    <select class="categories">
        <option value="">Select</option>
    </select>
    <input class="edit" type="button" value="Edit">
    <input class="add" type="button" value="Add">
</li>

The tag select I render separately as inner view. When I change select I need get nested categories from the server and append them to the tag li using the template above wrapped in the tag ul. In outer view I create inner view and listen events on click at editing and adding. I have a trouble with the last events when the level of nesting is more than one because they are fires equal times to the level of nesting. What am I doing wrong?

The below code snippet shows how I work with outer and inner views:

    var CategoriesInnerView = Backbone.View.extend({
        tagName:       'select',
        initialize:    function(){
            _.bindAll(this,'addOne','addAll');
            this.collection.bind('reset',this.addAll);
        },
        addOne:        function(category){
            this.$el.append(new CategoryView({model:category}).render().el);
        },
        addAll:        function(){
            this.collection.each(this.addOne);
        },
        events:        {
            'change':'changeSelected'
        },
        changeSelected:function(){
            var children = new Categories();
            children.url = 'categories/' + this.$el.val();

            var childrenView = new CategoriesOuterView({collection:children});
            this.$el.parent().find('ul').remove();
            this.$el.parent().append(childrenView.render().el);

            children.fetch();
        }
    });

    var CategoriesOuterView = Backbone.View.extend({
        tagName:   'ul',
        template:  _.template($('#categories-template').html()),
        initialize:function(){
            this.inner = new CategoriesInnerView({collection:this.collection});
        },
        render:    function(){
            this.$el.html(this.template);

            this.inner.setElement(this.$('select')).render();

            return this;
        },
        events:    {
            'click .edit':'edit',
            'click .add': 'add'
        },
        edit:    function(){
            this.renderForm(this.collection.get(this.inner.$el.val()));
        },
        add:    function(){
            this.renderForm(new Category());
        },
        renderForm:function(category){
            // some code to render the form
        }
    });

Upvotes: 3

Views: 2468

Answers (2)

nikoshr
nikoshr

Reputation: 33364

When you set up nested views, you have to take into account the fact that events will bubble up the DOM tree and that Backbone handles DOM events at the view.el level. This means that in your scenario, nested nodes will also trigger events in the parent nodes if you let the event go up the hierarchy.

See http://jsfiddle.net/PX2PL/ for a demo

A simple solution would be to stop the event propagation in your callbacks

var CategoriesOuterView = Backbone.View.extend({
    events:    {
        'click .edit':'edit',
        'click .add': 'add'
    },
    edit: function(e) {
        e.stopPropagation();
        this.renderForm(this.collection.get(this.inner.$el.val()));
    },
    add: function(e) {
        e.stopPropagation();
        this.renderForm(new Category());
    }
}

And an updated demo http://jsfiddle.net/PX2PL/1/

Upvotes: 3

machineghost
machineghost

Reputation: 35725

We really need to seem some of your code to answer this properly, but it sounds like one of two things is happening:

1) you are instantiating your view on the same element multiple times

2) your event selectors are too broad

But without actually seeing (the relevant parts of) your view, it's hard to say more than that.


Tangentially-Related Side Note

BTW, when you have something like this, there are two basic approaches you can take:

1) you can have your parent view create sub views, and put the event handling on the sub-views

2) you can have your parent view create sub views, or not (it could create all the HTML itself), and put the event handling on it.

The advantage of #1 is simplicity: your event handlers can just refer to this to refer to the relevant view. However, there's a problem with #1 if you have to scale it up too much: millions of views, each with their own event handlers, will hurt performance.

So, if performance is/will be important, #2 is better because you're only doing a single set of event hookups. However, you're event handlers will have to be smarter, because they'll have to figure out what element their working on based on the provided event (ie. e.target, vs. this in the #1 approach).

Upvotes: 2

Related Questions