JavaScript Architecture: Backbone.js Views

01.08.2012

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:

<code>this.$el.append(row.render().$el);</code>

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.

<< JavaScript Architecture: Backbone.js CollectionsJavaScript Architecture: Backbone.js Routers >>

Tags: , , , , , , , , , , , , ,


Comments

01.15.2012 / Trouble said:

Precious like the Other Posts! i really appreciate your Tutorials!
What are you thinking about the “war” between backbone.js/underscore.js and require.js/AMD??

Are u planning to write something about AMD + Documentcloud?

From Germany

01.16.2012 / nross83 said:

This is a very tasty series and your good examples make it really easy to follow. I’m pretty impressed with Backbone and how they have tried to come up with nice conventions to shorten your code (like in your Event Delegation section). Just pure juiciness.

Can’t wait for the next addition to your series.

01.16.2012 / Aaron Hardy said:

@Trouble,

Thanks for stopping by. I hope your real name is Trouble ’cause that’d be aaawesooome. For those who don’t know what you’re talking about, I think you’re referring to this:

https://github.com/documentcloud/backbone/pull/710

I’ve chimed in to give my support for supporting AMD in their libraries. I think the other folks on the list have done a good job at representing the pros and cons. It’s a bummer they’ve decided to remove AMD support but I’d rather have both backbone and underscore not support it than only underscore (which is how it currently is).

Overall, I think AMD is a great standard and needs to happen.

@nros83 Thanks. It is quite juicy!

02.21.2012 / Brian said:

Thanks for this great series of articles, especially to a wep app beginner like me.

The complete sample you give in the Rendering section didn’t work for me. I had to change the this.el.append(row.el); line to this.el.appendChild(row.el);.

02.21.2012 / Aaron Hardy said:

Thanks for stopping by. this.el.append() will only work if this.el is jquery-wrapped. So if you did this before: this.el = $(this.el); then the example should work. However, since the writing of this post, Backbone has released a new version that does the jquery-wrapping for you. You should now just be able to go: this.$el.append(). Hope that helps!

03.20.2012 / Hyrum said:

Hola Aaron.

I know I could find this elsewhere but I figure it might be useful to people reading your blog:

I’ve seen you use the _ in a couple places.
e.g. _template: _.template($(‘#tweet-row-template’).html())
What is that? Is _ some special object in Backbone?

Thanks.

PS Hope all is well with the kids, both new and old(er).

03.20.2012 / Aaron Hardy said:

The _.template() (underscore + period) is just accessing a method of Underscore, the library. The _ is to Underscore as $ is to jQuery. See JavaScript Architecture: Underscore.js for more info on Underscore.

When I do something like _template or _someOtherName, I’m just prefixing the variable name to denote that it should be respected as a private variable. See the Naming Conventions section of JavaScript Architecture: Organization and Quality for more info on that.

Hope that helps. The kiddos and fam are doing a-okay. Thanks. Likewise I hope you’re doing well with the fam, Rain, and such. I oft reminisce of you banging on the desk in a heated Call of Duty battle and “I shot you!” ringing through the air.

03.21.2012 / Dreyfus said:

Hi, i have to thank you for a very detailed post. But i would just like to ask if on the model side instead of adding it in script. can i create a separate html on it and just call it?

03.21.2012 / Dreyfus said:

Opps. sorry on the views side it what i mean. :)

03.22.2012 / Aaron Hardy said:

Sorry Dreyfus, I don’t quite understand what you’re asking. Can you clarify?

03.22.2012 / Dreyfus said:

i wanted to create a separate file for the templates. like listview.tpl.html is it possible? also when i run the application firebug displays this.
str is null
str.replace(/\\/g, ‘\\\\’)
i’m using template : _.template($(“tpl-item-list”).html()), on my views. am i doing it right?

03.23.2012 / Aaron Hardy said:

Thanks for the clarification. You can definitely create separate files for the templates but you’ll need something to load those files in appropriately. I use RequireJS. I show how you can do this in JavaScript Architecture: RequireJS Dependency Management.

I believe the error you’re getting is because you need a # in your jquery selector like this: $(“#tpl-item-list”). The # says to look for and element with an id of tpl-item-list.

Hope that helps!

03.28.2012 / Trouble said:

@Aaron Hardy

:D yeah im the same Trouble(stackoverflow)! awesome that u made an Article about AMD love it!! i will citate u into my B.of.Sc that im writing now :-)!! Your Tutorials really helped me out!

Cheers from Germany

06.06.2012 / dan said:

great series of articles, really made me finally understand backbone and how to architect my javascript apps more correctly.

just a small thing I noticed, maybe you need to update the Backbone.View constructor code because now backbone calls delegateEvents after the call to initialize (it makes sense because if you call render in the initialize method then the events you declare in the events property can bind to elements that have been rendered already)

06.06.2012 / Aaron Hardy said:

Yeah, quite a few things have changed since the time of this writing. I hope to go through and makes updates like the one you mentioned. Also, note that this.setElement() should now be used rather than setting this.el directly. Part of what setElement() does is re-bind the events hash.

Thanks for the comment and I’m glad I could help!

06.29.2012 / Jordan Arseno said:

Hands down, excellent. The first few paragraphs explaining view philosophy and granularity was very informative, and somewhat relieving. All the best AH, look forward to reading more from you.

07.04.2012 / Julianne Thoms said:

I think this is a real great blog.Much thanks again. Cool.

08.29.2012 / Isaac Zepeda said:

When you say a parent View can have children views, how you defined? You just extend the Parent view?

Thanks this blog has been very helpful

Regards,

09.01.2012 / le big mug said:

[quote]Yeah, quite a few things have changed since the time of this writing. I hope to go through and makes updates like the one you mentioned…[quote]

[quote] 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. :) [/quote]

so Aaron does it mean that you already used your reserved right and moved to another camp.:) i read a long time back your statement and followed same way of calling render within initialize. was i pretending to be purist ain’t sure, sort of your statement made a sense..

is calling render within initialize not anymore a valid statement?

09.04.2012 / lebigmug said:

hi Aaron,

i noticed you had removed my previous comment.
aint sure what was the reason behind?

however, i still ‘d like hear your opinion on calling render within initialize method. i do myself read long time back here and was gone in a way as you did putting render within initialize method.

that time it made a sense that render should be responsibility of view itself.

thanks.

09.04.2012 / Aaron Hardy said:

@Isaac, What I mean by “a parent can have child views” is this: Let’s say you have a page that has a header, a left-hand navigation bar, and a content area. In this case, I might have a view that represents the full page. I might name this my shell view. Its html template might have one div for the header, one div for the left-hand nav bar, and one div for the content area. In the JavaScript, the shell view can instantiate a header Backbone view and pass the header div to that view. Something like:

new HeaderView({el: headerDiv})

Likewise, it can instantiate a left-hand nav bar Backbone view and pass the appropriate div to that view. And so on. Doing this allows larger views to be broken down into “child” (smaller) views so that each view has a simple, single responsibility rather than a single view having many responsibilities.

@lebigmug Sorry, I didn’t remove your comment; I just took a while to get around to approving it. Thanks for asking the question! I maintain my position about calling render from initialize, but I’m not a big stickler on the principle. I see very intelligent devs calling it from parent views. There may come a time where I run across a scenario where calling render from the parent makes more sense. If it hasn’t been a problem for you yet, I’d say stick with it. That said, I’ve been using the Marionette extension (https://github.com/derickbailey/backbone.marionette) for my projects which handles the initial call to the render function for me (and it does so from the parent). I highly recommend checking out Marionette.

09.04.2012 / Isaac Zepeda said:

Thanks Aaron!

10.22.2012 / Martijn said:

Hi Aaron,

first of all thank you very much for all your articles i really enjoyed reading them as i’ve been starting to learn the conecpt of backbone by your articles.

I am beginning to understand the concept of multiple views listening to the same collection, however i’m trying to build an app with backbone in combination with requirejs and i can’t seem to find a solution on the internet for the multiple views that listen to 1 collection working with requirejs.

Could you try to send me in the right direction to get this working?

Thanks

12.01.2012 / Re-Learning Backbone.js – Views Basics | BarDev said:

[...] Here are a couple of quotes about views that help me understand views better: “At a technical level, a Backbone view is simply a JavaScript object that manages a specific DOM element and its descendants.” http://aaronhardy.com/javascript/javascript-architecture-backbone-js-views/ [...]

12.01.2012 / Re-Learning Backbone.js – View Render | BarDev said:

[...] Here are a couple of quotes about Backbone.js Views: “Backbone views are almost more convention than they are code — they don’t determine anything about your HTML or CSS for you, and can be used with any JavaScript templating library. The general idea is to organize your interface into logical views, backed by models, each of which can be updated independently when the model changes, without having to redraw the page. Instead of digging into a JSON object, looking up an element in the DOM, and updating the HTML by hand, you can bind your view’s render function to the model’s “change” event — and now everywhere that model data is displayed in the UI, it is always immediately up to date.” http://backbonejs.org/#View “At a technical level, a Backbone view is simply a JavaScript object that manages a specific DOM element and its descendants.” http://aaronhardy.com/javascript/javascript-architecture-backbone-js-views/ [...]


Leave a Comment

Your email address is required but will not be published.




Comment