When I first started using Backbone, one of my biggest unsolved problems was finding a good pattern for rendering views. It should be easy, but there are lots of pitfalls that crop up in larger apps, so I’ll show you what we’ve settled on at Segment.
The first problem with learning how to render views is that Backbone tutorials rarely go deep enough to run into any of the problems that larger apps have. Most projects aren’t as simple as the typical todo-list examples. So before we start evaluating different methods, here are my requirements for rendering a view:
Render should be able to be called multiple times without side effects.
The order of the DOM should be declared in templates, not Javascript.
Calling render again should maintain the state the view was in.
Rendering twice shouldn’t trash views just to re-construct them again.
With those requirements in mind, here are some ways I’ve seen people rendering their views that don’t fit our needs:
Unbinding DOM events by accident.
You’ll often see people rendering a template using jQuery’s html()
method:
render : function () {
this.$el.html(this.template(options));
return this;
}
That makes sense, and works perfectly if you never deal with subviews. But as soon as you need subviews—which in any real app seems to happen fairly early on—you’ll realize that jQuery’s .html()
calls .empty()
first, which unbinds all jQuery events on all child nodes. So the first time you render()
everything works fine, but as soon as you call it again all of the events defined in this.events
in your subviews will be unbound. Not great.
Not using templates!
To get around it, another thing you see in a lot of Backbone examples is this:
render : function () {
this.$el.append(this.subview.render().$el);
this.$el.append(this.anotherSubview.render().$el);
return this;
}
This way you never need to call .html()
so you aren’t accidentally unbinding events, but this has bigger problems. Now instead of having a template define the order of your markup, the order is precariously defined in the view’s render
method. Which isn’t only harder to maintain, it also makes it harder to keep track of the structure of the DOM.
And even then, it’s not that simple! Because you’ll often want to have a template, with things like captions, images, buttons, etc. as well as subviews. So now you’re really screwed, because you have to call .html()
which means render
is back to being a once-only method.
Way too much overhead.
So to solve all of that, people resort to re-initializing their subviews each time render is called:
render : function () {
this.$el.$html(this.template());
this.subview = new Subview();
this.anotherSubview = new AnotherSubview();
this.$el.append(this.subview.render().$el);
this.$el.append(this.anotherSubview.render().$el);
return this;
}
But now you’re doing a lot of extra work each time render is called. You’re completely re-initializing every subview underneath the current view (and how deep do your subivews go?) even though the only thing you actually needed to rebind was your DOM events.
So what’s the solution?
Actually, the solution isn’t even that complicated. You just need to make sure that delegateEvents
is called to rebind the events on your subviews any time .html()
runs. And since Backbone’s setElement
calls delegateEvents
already, a quick solution could look like this:
render : function () {
this.$el.$html(this.template());
this.subview.setElement(this.$('.subview')).render();
this.anotherSubview.setElement(this.$('.another-subview')).render();
return this;
}
But that’s pretty verbose and hard to parse, so I use a helper method called assign
which I’ve defined in our BaseView
and our typical render
method looks something like this:
render : function () {
this.$el.html(this.template());
this.assign(this.subview, '.subview');
this.assign(this.anotherSubview, '.another-subview');
return this;
}
assign
is basically just setElement
—which calls delegateEvents
for you—but with a nicer API and an automatic call to render
:
assign : function (view, selector) {
view.setElement(this.$(selector)).render();
}
With assign
, those lines in render
are actually pretty expressive. And it solves all of the requirements we set in the beginning for a render
method:
Render should be able to be called as many times as I want without any side effects: Since
setElement
re-delegates DOM events you never have to worry about jQuery’s.html()
wiping out your submodule’s event handlers.The order of the DOM shouldn’t be declared in Javascript, it should be declared in templates: Check!
Calling render again should maintain the state the view was in: Yup. Since the view isn’t completely re-initialized each time
render
is called, nothing is wiped clean.Rendering twice shouldn’t trash views just to re-construct them again: Check, none of your subview’s initialization code is called again needlessly for every call to
render
.
The assign
method has been working really well for us so far, while building Segment. If you have an improvement, found another way to wrangle Backbone rendering, or think it’s complete junk I’d love to hear about it on twitter.