Backbone.js (Sub)View Rendering Trick

Backbone.js Subview Rendering TrickIn Backbone.js, rendering views is really simple, yet not so much. It's simple because Backbone doesn't force you into doing it any specific way, so you have freedom to just use a bit of jQuery and dump the HTML into an element. Then again, since it doesn't implement anything on its own, we're stuck writing our own implementations, making it more difficult than it could otherwise be. In the case of rendering subviews, things can definitely get a little more difficult.

I know I said I was going to put off doing any more Backbone.js stuff for a while, but I had to do it. First, I read about this trick on Ian Storm Taylor's blog and decided it was worthy of spreading. Secondly, I'm not prepared for writing about the topic I planned on doing next, so it needs to wait a bit.

The Requirements

In Ian Taylor's original post about the trick, he opened up by listing some requirements that should be met for subview rendering implementation. First, though, you have to fully understand what's going on when we talk about subviews.

If you look back at the Wine Cellar application that we created in my Backbone.js screencast series you'll see that the sidebar is built from a single view that creates additional views for each list item. This is not what we're talking about. If you take the entire body section from that application, create a view that manages all three view areas (header, sidebar, and main), then those three areas would be the type of subviews that we're talking about. The main view would contain a reference to each of the three views and render them in their correct places. So rather than using the Router to set the views, like we did in the Wine Cellar app, we would use a super view to set up the other views.

Now that we're on the same page, let's take a look at the requirements that Ian had:

  1. render should be able to be called multiple time without side effects. Often, the "current way" of doing things will break event listeners on subviews. Anything breaking like that is a side effect.
  2.  The order of the DOM should be declared in templates, not JavaScript. So, rather than defining the order of the subviews within the render  function, we just assign the subviews to different areas in the DOM structure of the template.
  3. Calling render again should maintain the state the view was in. If the state hasn't changed, then calling render on any view (super or sub) should not cause any changes to what is already rendered.
  4. Rendering twice shouldn't trash views just to reconstruct them again. This one is pretty self-explanatory. Don't remake a perfectly fine subview. If it's in the state that you want it to be in, just leave it be.

Implementations

First let's take a look at how someone might normally do this:

render: function() {
    this.$el.html(this.template(options));
    this.$('.subview').html(this.subview.render());
    this.$('.othersubview').html(this.othersubview.render());
    return this.el;
}

Note that this code assumes that the render method of the subviews always returns the views el, just like this outer render function does. I prefer my code to do this. I've seen a lot of people just return this. That makes sense if you want to make things chainable, but 95% of the time you just end up writing this:

view.render().el

This is ugly (in my opinion) because you reference a property after a function. If you already have a reference to the view and if 95% of the time you're just going to ask for el right away anyway, why don't we just simplify it a bit and return this.el from the render function?

Anyway, back to the first code snippet. You may not realize it, but this has a serious flaw in it. When you call jQuery's html function, jQuery will first call empty on the current contents, removing all the bound event handlers on the elements. When you call render on your subviews, those events won't be re-bound, so you're stuck with static HTML and no event listeners.

One way you could fix this is to call delegateEvents() within every render function of the subviews, but that's just one more step you have to remember to include in every subview. Some people will instead just recreate the subviews, which causes too much overhead and useless computation.

A Better Way

Mr. Taylor notes that using setElement on the subviews works really well. When you call setElement, the argument passed in will become the new element for the subview (replaces this.el within the subview). It also causes delegateEvents to be called again in the subview so the event listeners are reassigned. So our render function would now look like this:

render: function() {
    this.$el.html(this.template(options));
    this.subview.setElement(this.$('.subview')).render();
    this.othersubview.setElement(this.$('.othersubview')).render();
    return this.el;
}

This becomes a bit annoying to manage, though, so he created a function that he adds to the outer view that he calls assign, which would look like this:

assign: function(view, selector) {
    view.setElement(this.$(selector)).render();
}

Then he just uses assign within his render function:

render: function() {
    this.$el.html(this.template(options));
    this.assign(this.subview, '.subview');
    this.assign(this.othersubview, '.othersubview');
    return this.el;
}

This then takes care of all the requirements he has, but he wasn't satisfied with this. He later wrote a second post on the subject where he states that he took a look at Layout Manager and saw that it was using the same concept. But it showed Ian a way to improve his assign function a bit, which would change the last code snippet we wrote into this:

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

    this.assign({
        '.subview': this.subview,
        '.othersubview': this.othersubview
    });

    return this.el;
}

It's a minor improvement that makes it less repetitive by requiring you to only need to call assign once. Here's what the new assign method looks like:

assign: function (selector, view) {
    var selectors;

    if (_.isObject(selector)) {
        selectors = selector;
    }
    else {
        selectors = {};
        selectors[selector] = view;
    }

    if (!selectors) return;

    _.each(selectors, function (view, selector) {
        view.setElement(this.$(selector)).render();
    }, this);
}

Conclusion

Thanks a bunch Ian Storm Taylor for your insights! I'll definitely be using this in my smaller applications, but when I get into slightly larger apps, I think I'll take a deeper look at Backbone.LayoutManager. Do you have any really cool "tricks" that you use in your Backbone.js applications? Share them in the comments below! God bless and happy coding!

About the Author

Author: Joe Zim

Joe Zim

Joe Zimmerman has been doing web development ever since he found an HTML book on his dad's shelf when he was 12. Since then, JavaScript has grown in popularity and he has become passionate about it. He also loves to teach others though his blog and other popular blogs. When he's not writing code, he's spending time with his wife and children and leading them in God's Word.


  • David Beck

    Awesome summary of requirements! Here is a pre-packaged light weight solution that also takes care all four, unless I am mistaken. Can be mixed into existing view classes:

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

    • http://www.joezimjs.com Joe Zimmerman

      I didn’t get a great look at it, but it looks promising. Largely, though, I’ve been using Backbone.Marionette and it handles a lot of the heavy lifting for you with Regions, CollectionView, and CompositeView.