How to render and append sub-views in Backbone.js

72,846

Solution 1

I have generally seen/used a couple of different solutions:

Solution 1

var OuterView = Backbone.View.extend({
    initialize: function() {
        this.inner = new InnerView();
    },

    render: function() {
        this.$el.html(template); // or this.$el.empty() if you have no template
        this.$el.append(this.inner.$el);
        this.inner.render();
    }
});

var InnerView = Backbone.View.extend({
    render: function() {
        this.$el.html(template);
        this.delegateEvents();
    }
});

This is similar to your first example, with a few changes:

  1. The order in which you append the sub elements matters
  2. The outer view does not contain the html elements to be set on the inner view(s) (meaning you can still specify tagName in the inner view)
  3. render() is called AFTER the inner view's element has been placed into the DOM, which is helpful if your inner view's render() method is placing/sizing itself on the page based on other elements' position/size (which is a common use case, in my experience)

Solution 2

var OuterView = Backbone.View.extend({
    initialize: function() {
        this.render();
    },

    render: function() {
        this.$el.html(template); // or this.$el.empty() if you have no template
        this.inner = new InnerView();
        this.$el.append(this.inner.$el);
    }
});

var InnerView = Backbone.View.extend({
    initialize: function() {
        this.render();
    },

    render: function() {
        this.$el.html(template);
    }
});

Solution 2 may look cleaner, but it has caused some strange things in my experience and has affected performance negatively.

I generally use Solution 1, for a couple of reasons:

  1. A lot of my views rely on already being in the DOM in their render() method
  2. When the outer view is re-rendered, views don't have to be re-initialized, which re-initialization can cause memory leaks and also cause freaky issues with existing bindings

Keep in mind that if you are initializing a new View() every time render() is called, that initialization is going to call delegateEvents() anyway. So that shouldn't necessarily be a "con", as you've expressed.

Solution 2

This is a perennial problem with Backbone and, in my experience, there's not really a satisfying answer to this question. I share your frustration, especially since there is so little guidance despite how common this use case is. That said, I usually go with something akin to your second example.

First of all, I would dismiss out of hand anything that requires you to re-delegate events. Backbone's event-driven view model is one of its most crucial components, and to lose that functionality simply because your application is non-trivial would leave a bad taste in any programmer's mouth. So scratch number one.

Regarding your third example, I think it's just an end-run around the conventional rendering practice and doesn't add much meaning. Perhaps if you're doing actual event triggering (i.e., not a contrived "onRender" event), it would be worth just binding those events to render itself. If you find render becoming unwieldy and complex, you have too few subviews.

Back to your second example, which is probably the lesser of the three evils. Here is example code lifted from Recipes With Backbone, found on page 42 of my PDF edition:

...
render: function() {
    $(this.el).html(this.template());
    this.addAll();
    return this;
},
  addAll: function() {
    this.collection.each(this.addOne);
},
  addOne: function(model) {
    view = new Views.Appointment({model: model});
    view.render();
    $(this.el).append(view.el);
    model.bind('remove', view.remove);
}

This is only a slightly more sophisticated setup than your second example: they specifiy a set of functions, addAll and addOne, that do the dirty work. I think this approach is workable (and I certainly use it); but it still leaves a bizarre aftertaste. (Pardon all these tongue metaphors.)

To your point on appending in the right order: if you're strictly appending, sure, that's a limitation. But make sure you consider all possible templating schemes. Perhaps you'd actually like a placeholder element (e.g., an empty div or ul) that you can then replaceWith a new (DOM) element that holds the appropriate subviews. Appending isn't the only solution, and you can certainly get around the ordering problem if you care about it that much, but I would imagine you have a design issue if it is tripping you up. Remember, subviews can have subviews, and they should if it's appropriate. That way, you have a rather tree-like structure, which is quite nice: each subview adds all its subviews, in order, before the parent view adds another, and so on.

Unfortunately, solution #2 is probably the best you can hope for using out-of-the-box Backbone. If you're interested in checking out third-party libraries, one that I have looked into (but haven't actually had any time to play with yet) is Backbone.LayoutManager, which seems to have a healthier method of adding subviews. However, even they have had recent debates on similar issues to these.

Solution 3

Surprised this hasn't been mentioned yet, but I'd seriously consider using Marionette.

It enforces a bit more structure to Backbone apps, including specific view types (ListView, ItemView, Region and Layout), adding proper Controllers and a lot more.

Here is the project on Github and a great guide by Addy Osmani in the book Backbone Fundamentals to get you started.

Solution 4

I have, what I believe to be, a quite comprehensive solution to this problem. It allows a model within a collection to change, and have only its view re-rendered (rather than the entire collection). It also handles removal of zombie views through the close() methods.

var SubView = Backbone.View.extend({
    // tagName: must be implemented
    // className: must be implemented
    // template: must be implemented

    initialize: function() {
        this.model.on("change", this.render, this);
        this.model.on("close", this.close, this);
    },

    render: function(options) {
        console.log("rendering subview for",this.model.get("name"));
        var defaultOptions = {};
        options = typeof options === "object" ? $.extend(true, defaultOptions, options) : defaultOptions;
        this.$el.html(this.template({model: this.model.toJSON(), options: options})).fadeIn("fast");
        return this;
    },

    close: function() {
        console.log("closing subview for",this.model.get("name"));
        this.model.off("change", this.render, this);
        this.model.off("close", this.close, this);
        this.remove();
    }
});
var ViewCollection = Backbone.View.extend({
    // el: must be implemented
    // subViewClass: must be implemented

    initialize: function() {
        var self = this;
        self.collection.on("add", self.addSubView, self);
        self.collection.on("remove", self.removeSubView, self);
        self.collection.on("reset", self.reset, self);
        self.collection.on("closeAll", self.closeAll, self);
        self.collection.reset = function(models, options) {
            self.closeAll();
            Backbone.Collection.prototype.reset.call(this, models, options);
        };
        self.reset();
    },

    reset: function() {
        this.$el.empty();
        this.render();
    },

    render: function() {
        console.log("rendering viewcollection for",this.collection.models);
        var self = this;
        self.collection.each(function(model) {
            self.addSubView(model);
        });
        return self;
    },

    addSubView: function(model) {
        var sv = new this.subViewClass({model: model});
        this.$el.append(sv.render().el);
    },

    removeSubView: function(model) {
        model.trigger("close");
    },

    closeAll: function() {
        this.collection.each(function(model) {
            model.trigger("close");
        });
    }
});

Usage:

var PartView = SubView.extend({
    tagName: "tr",
    className: "part",
    template: _.template($("#part-row-template").html())
});

var PartListView = ViewCollection.extend({
    el: $("table#parts"),
    subViewClass: PartView
});

Solution 5

Check out this mixin for creating and rendering subviews:

https://github.com/rotundasoftware/backbone.subviews

It is a minimalist solution that addresses a lot of the issues discussed in this thread, including rendering order, not having to re-delegate events, etc. Note that the case of a collection view (where each model in the collection is represented with one subview) is a different topic. Best general solution I am aware of to that case is the CollectionView in Marionette.

Share:
72,846
Ian Storm Taylor
Author by

Ian Storm Taylor

Hello.

Updated on March 12, 2020

Comments

  • Ian Storm Taylor
    Ian Storm Taylor about 4 years

    I have a nested-View setup which can get somewhat deep in my application. There are a bunch of ways I could think of initializing, rendering and appending the sub-views, but I'm wondering what common practice is.

    Here are a couple I've thought of:

    initialize : function () {
    
        this.subView1 = new Subview({options});
        this.subView2 = new Subview({options});
    },
    
    render : function () {
    
        this.$el.html(this.template());
    
        this.subView1.setElement('.some-el').render();
        this.subView2.setElement('.some-el').render();
    }
    

    Pros: You don't have to worry about maintaining the right DOM order with appending. The views are initialized early on, so there isn't as much to do all at once in the render function.

    Cons: You are forced to re-delegateEvents(), which might be costly? The parent view's render function is cluttered with all of the subview rendering that needs to happen? You don't have the ability to set the tagName of the elements, so the template needs to maintain the correct tagNames.

    Another way:

    initialize : function () {
    
    },
    
    render : function () {
    
        this.$el.empty();
    
        this.subView1 = new Subview({options});
        this.subView2 = new Subview({options});
    
        this.$el.append(this.subView1.render().el, this.subView2.render().el);
    }
    

    Pros: You don't have to re-delegate events. You don't need a template that just contains empty placeholders and your tagName's are back to being defined by the view.

    Cons: You now have to make sure to append things in the right order. The parent view's render is still cluttered by the subview rendering.

    With an onRender event:

    initialize : function () {
        this.on('render', this.onRender);
        this.subView1 = new Subview({options});
        this.subView2 = new Subview({options});
    },
    
    render : function () {
    
        this.$el.html(this.template);
    
        //other stuff
    
        return this.trigger('render');
    },
    
    onRender : function () {
    
        this.subView1.setElement('.some-el').render();
        this.subView2.setElement('.some-el').render();
    }
    

    Pros: The subview logic is now separated from the view's render() method.

    With an onRender event:

    initialize : function () {
        this.on('render', this.onRender);
    },
    
    render : function () {
    
        this.$el.html(this.template);
    
        //other stuff
    
        return this.trigger('render');
    },
    
    onRender : function () {
        this.subView1 = new Subview();
        this.subView2 = new Subview();
        this.subView1.setElement('.some-el').render();
        this.subView2.setElement('.some-el').render();
    }
    

    I've kind of mix and matched a bunch of different practices across all of these examples (so sorry about that) but what are the ones that you would keep or add? and what would you not do?

    Summary of practices:

    • Instantiate subviews in initialize or in render?
    • Perform all sub-view rendering logic in render or in onRender?
    • Use setElement or append/appendTo?
  • atp
    atp about 12 years
    The penultimate line -- model.bind('remove', view.remove); -- shouldn't you just do that in the Appointment's initialize function to keep them separate?
  • mor
    mor over 10 years
    What about when a view cannot be re-instantiated every time it's parent renders because it keeps a state?
  • Brave Dave
    Brave Dave over 10 years
    Stop all this craziness and just use the Backbone.subviews plugin!
  • Ceasar Bautista
    Ceasar Bautista over 9 years
    This does not answer the question.
  • Dana Woodman
    Dana Woodman over 9 years
    @CeasarBautista I don't go into how to use Marionette to accomplish this but Marionette does indeed solve the above problem
  • Dominic
    Dominic almost 9 years
    Neither of these solutions work up the sub view tree calling View.remove, which may be vital in doing custom cleanup in the view, which would otherwise prevent garbage collection