JavaScript Architecture: Backbone.js Views

Updated Aug 11, 2012 to reflect current library versions.

Tech-agnostic concepts

At this point of the series I really want to emphasize that the core concepts I’ve explained and will explain are not unique to Backbone; they’re unique to apps with state and dynamic views. I’m merely using Backbone as an illustration of a concrete tool that can be used to solve problems common to this type of app in general. The concept of “views” is no different.

What is a view?

If you’re coming from a different language or even a different library, you may be familiar with words like component, widget, or control. You can ask 10 engineers what they think those terms mean and you’ll likely get 10 different answers…or 30 if they think the terms are different from each other. The term view is just another one to throw on the pile and is equally ambiguous. It’s not all that unfortunate. Indeed, its usage can be quite flexible and its granularity disparate.

In the traditional web of requesting a new page for each section of a website, we may consider each page a view. Indeed, it is. In modern apps, it’s more common to have a single page and, as the user interacts with the page, portions of the page change. Those dynamic portions could likewise be called views. Within a dynamic portion of the page, there may be a toolbar that affects a list of customers. The toolbar could be considered a view. The list of customers could be another view. Each customer row inside the list of customers may be its own view. The row may contain a toggle button which is yet another view. The point is, in the Backbone world, the term view doesn’t necessary mean “a section of your website”. It can be, and oftentimes should be, much more granular than that.

At a technical level, a Backbone view is simply a JavaScript object that manages a specific DOM element and its descendants. If view A manages DOM element A and its descendants, no other view should touch DOM element A or its descendants. That’s view A’s domain. If you want to change the DOM elements of view A, go through view A’s API.

That said, a view can have sub views. In the example above, I mentioned a list of customers being a view and each customer row being a view. The customer row owns its DOM elements. The parent list view should not touch DOM elements within the customer row view. Is there a model the row should render? Pass the model to the customer row view. At that point, it’s the row view’s job. Throw it over the wall and forget about it.

With jQuery you’ll be heavily tempted to just change elements anywhere on the page willy-nilly because it’s so easy. It’s the fastest way to accomplish your task before the demo with your boss that starts in five minutes. The unfortunate truth is that structure, robustness, and scalability are not the easiest path in the short-term. In the long-term, though, they are absolutely critical to the success and future of the product and ultimately will establish the easiest path.

View granularity

So how granular should a view be? You’ll be hard-pressed to find a definite answer but here are a few guidelines. Is your view getting bloated with hundreds of lines of code? Try to break it up into smaller views. Is a single view accomplishing multiple, unrelated tasks (termed as having low cohesion)? Break it up into smaller views with distinct tasks. Do you find yourself wanting to copy-paste a portion of a view into other views? That portion should likely be its own view. Does your view manage a single DOM element that has no DOM descendants and is very simple like a single html link? It can likely be safely merged up with the parent view. When in doubt, choose many small views over a few large ones.

For you visual learners, take my current Twitter feed as an example. Look around this page and think about how you might divide it up into views and why.

I’m not going to tell you there’s a right answer, but here’s an idea of how I might divide it up:

View isolation and synchronization

We’ve talked about eventful models and collections and how they can be used to synchronize and drive multiple views without the views having to be aware of each other. The idea of view isolation and black-boxing is a very, very critical concept. So important we’re going to cover it again.

Let’s take a closer look at my Twitter feed. Let’s say I put my cursor in the “What’s happening?” textarea, type my tweet, then hit the tweet button. What should happen at that moment on the page?

  1. My number of tweets on the right side of the page should be incremented.
  2. My tweet should be added to my timeline.
  3. The “What’s happening?” textarea should be cleared.

While coding the event handler for the tweet button, it’d be really easy to just say, “Well, I know the id of the tweet count DOM element. I’ll just grab the element using jQuery and increment the number inside it.” What’s wrong with this? We just reached outside our view and manipulated the DOM element of another view. Like I’ve said before, this will lead to a giant web of dag-nasty in the long run. Avoid it. In fact, avoid forcing the two views to know about each other at all.

Instead, let’s create a model that contains the number of tweets. Both the “What’s happening?” view and the tweet count view share the model. When the user clicks the tweet button, we increment the number on the model. The tweet count view will be notified by the model that the number of tweets has changed. The tweet count view will then update its DOM element. The views can carry on blissfully unaware of each other.

Likewise, we can have a collection containing the tweets that should show up in our timeline. We add a tweet model to the collection from the “What’s happening?” view and the collection notifies the timeline view of the addition. The timeline view creates a single tweet view, shoves the tweet model into the tweet view, then adds the tweet view’s DOM element as the first child of the timeline’s DOM element. This last part will hopefully make more sense later, but notice the timeline does not know how the tweet view is showing the tweet or what its DOM element contains. It doesn’t care. It just throws the model over the wall.

View properties

Okay, let’s get to the meat. Backbone views have several special properties you should know about:

  • el - Each view has a DOM element it is managing. el is a reference to this DOM element. It can be set from the parent or it can be created and set from within the view itself.
  • $el - A jQuery-wrapped version of el.
  • id - If the view automatically creates a DOM element because el wasn't set through the constructor, the automatically-created DOM element will receive an id as specified by the id property.
  • model - Each view will likely be dealing with a model specified using the model property.
  • collection - Or the view will be dealing with a collection specified using the collection property.
  • tagName - During instantiation, the view will determine if el was set through the constructor. If it wasn't, the view will automatically create an element and will set it as the value for the el property. The type of element the view will automatically create is determined by tagName. By default, tagName is set to div, meaning the view will create a div element and set it as the el property if el wasn't set through the constructor.
  • className - If the view automatically creates a DOM element because el wasn't set through the constructor, the automatically-created DOM element will receive a CSS class name as specified by the className property.

What’s so special about these properties? Let’s take a look:

var TweetRow = Backbone.View.extend({
initialize: function(options) {
alert('I can access this.model directly! ' +
'The tweet text is ' + this.model.get('text'));

alert('I can only access my notSoSpecialProp through options: ' + options.notSoSpecialProp);

alert('I didn\'t even pass in el but the view made a ' +
this.el.tagName + ' for me!');
}
});

var tweet = new Backbone.Model({
avatar: 'aaronius.jpg',
alias: 'Aaronius',
text: 'Honey roasted peanuts rock my sox.'
});

var row = new TweetRow({
model: tweet,
notSoSpecialProp: 42
});

Here I passed in an options object into my tweet row view. The options object is passed into the initialize() method which is automatically called by Backbone.View. Behind the scenes, though, the view first takes the special properties in the options object and merges them as first-class properties of the view. Now we can access them directly (e.g., this.model, this.el). On the other hand, anything that's not considered special by the view is only accessible through the options object itself; they aren't merged to the view as direct properties.

Rendering

Now we have a model or collection we’re dealing with (this.model or this.collection) and also a DOM element we're managing (this.el). Now it's time to render something. In JavaScript Architecture: Underscore.js we talked about how to use templates to render pieces of UI. Let's take a look at how we would use our model and template to render a tweet row. In this example, I'm going to include the full HTML file I'm working with. This is far from a fully baked app, but I want you to get your hands on something that will actually run in a browser:

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>Backbone Example</title>
</head>
<body>
<script src="libs/jquery.js"></script>
<script src="libs/underscore.js"></script>
<script src="libs/backbone.js"></script>
<script type="text/javascript">
$(document).ready(function() {
var TweetRow = Backbone.View.extend({
_template: _.template($('#tweet-row-template').html()),

initialize: function() {
this.render();
},

render: function() {
this.$el.html(this._template(this.model.toJSON()));
return this;
}
});

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

render: function() {
var tweet = new Backbone.Model({
avatar: 'avatar.jpg',
username: 'Aaronius',
text: 'Honey roasted peanuts rock my sox.'
});

var row = new TweetRow({
model: tweet
});

this.$el.append(row.$el);
return this;
}
});

var app = new App({el: $('body')});
});
</script>

<script type="text/template" id="tweet-row-template">
<div style="float: left"><img src="<%= avatar %>"/></div>
<div style="margin-left: 60px">
<p><%= username %></p>
<p><%= text %></p>
</div>
</script>
</body>
</html>

You’ll see in TweetRow we’ve created a render() method which populates the inner HTML of the view's DOM element (in this case, a div that was automatically created by Backbone.View). By default, Backbone.View already has a render() method but it doesn't perform any operation. Nothing in Backbone even calls it either. Truthfully, the render() method is more of a convention than anything, but I highly suggest you follow it. The render() method should do any work needed to update the view's DOM element. Keep in mind that optimally you should be able to call render() multiple times if needed and your view should still render and function properly. Also, it's recommended you return this; at the end to allow for method chaining.

There are some differences in the Backbone community regarding how render() should be called. You'll often see examples that show the parent view calling the render() method of a child view. In our case, that would mean rather than calling render() from our initialize() method within the view we would call it later outside of the view before we append the element to a parent DOM element:

this.$el.append(row.render().$el);

I prefer to only call render() from within the view itself--never from another view--even a parent view. It's my opinion that views should not be concerned with when other views need to be rendered or re-rendered. Others may disagree and I reserve the right to join them someday. :)

Model-View knowledge

It may be tempting to store a view or two on a model. Steer clear of this. Views should know about models and be able to bind to their events but not the other way around. This is standard MVC protocol and for good reason. Logic flow and interaction among application actors can easily become unwieldy when introducing views into models.

Event delegation

A helpful nugget Backbone views offer is event delegation. Normally when dealing with DOM element interactions you would have to query for elements using jQuery and then bind to their events. It’s not difficult, but Backbone provides a shortcut for doing so:

var DocumentView = Backbone.View.extend({
events: {
"dblclick" : "open",
"click .icon.doc" : "select",
"mouseover .title .date" : "showTooltip"
},

render: function() {
...
},

open: function() {
...
},

select: function() {
...
},

showTooltip: function() {
...
},
});

As you can see, an events property is set. Each entry could be read as follows:

  • When this.el is double-clicked, call the open() method.
  • When descendants of this.el matching the .icon.doc CSS selector are clicked, call the select() method.
  • When descendants of this.el matching the .title .date CSS selector are moused over, call the showTooltip() method.

Another bonus about this shortcut is that referencing the this context from within the callback methods will still refer to the view--not the DOM element that triggered the event. If you've used JavaScript for much time, you'll know that handling the this context can be a pain.

This event delegation generally is handled for you transparently. However, if you ever replace this.el with a different DOM element, be sure to call this.setElement(newEl) instead of just this.el = newEl. The setElement() function will properly remove the event delegation from the old element and add it to the new element. It will also set up the this.$el variable for you. Hopefully that will save you from pulling your hair out sometime.

Other functionality

jQuery allows us to search for elements only within this.el:

var paragraphs = $('p', this.el);

As a convenience, Backbone provides a shortcut to do the same thing:

var paragraphs = this.$('p');

With the appropriate usage of views we can build more maintainable, scalable apps. I invite you to read more in the Backbone.View documentation.