English |  Español |  Français |  Italiano |  Português |  Русский |  Shqip

Developing a Backbone.js Edge

Chapter 4: Views

A View encapsulates a visual region of a web application and constrains all plugins, events, interactivity, and logic inside to this region. Views allow you to take advantage of reusable components and provide a more modular infrastructure.

Views are often managed within a Router or a parent View and can either be long lived like a persistant shopping cart in the header, for example, or short lived like the items in the cart as you expand it.

Purpose

When working with regions of a web application, it is often desirable to isolate and contextualize logic and rendering. Imagine a shopping cart web application that has an interface that positions navigation at the top, categories on the left, and the shopping cart on the right. The shopping cart is composed of details about each item.

You can think of breaking down this application into the following views:

Each view represents a unique or repeatable region in the application. By representing our application in this manner, we are able to adopt patterns and best practices that have existed in application development for decades. Since Views are visual representations of data, there is no querying the DOM to determine what the state is. The idea here is that you do logic and calculations (shopping cart totals, position, etc.) within the View and render that.

Views should not know anything about business logic, and instead focus solely on presentation. They should remain independent and not depend upon any other views to function. Views should also not leak any events outside of a specific region.

Backbone intentionally keeps views minimalistic, since there are many opinionated ways of using events, and developers have diverged to embracing different methods. The following section describes in detail how to create powerful views from scratch, but it also provides insight on refactoring using the LayoutManager plugin (as well as recommendations for other view management plugins).

Basic usage

Extending and initializing Views is surprisingly simple, but there are inconsistencies in views that are not prevalent in Models, Collections, or Routers that you will need to be aware of. These inconsistencies are most prevalent with assigning properties and figuring out how to access them. There are also properties that change from their original value to a new value. We'll talk about these as they come up in examples.

You will find that Views are not as fleshed out as the other classes, in terms of functionality and opinions. Views rely heavily on the DOM Library.

Extending a view

As with other Backbone classes, you are encouraged to extend the classes into your own custom implementations. This is a useful practice when you will create more than one instance of the same view.

We define our custom view by extending from Backbone.View:

var UserView = Backbone.View.extend(properties, [classProperties]);

This will create a unique class that is a derivation of a view. Neither the properties or classProperties object are required, but its as rare to see a view without properties as a view having classProperties.

Examine the Hubbub IssueHolderView as a good View example, and write the following:

var IssueHolderView = Backbone.View.extend({
className: 'issue-list', initialize: function(options) {
this.options.collection = new app.Issue.Collection();
this.options.collection.setFilter(app.board.issues,
options.testKey, options.testValue);
},
template: _.template($('#js-issue-list-template').html()),
render: function () {
this.$el.html(this.template({
title: this.options.title
}));
this.listView = new app.IssueListView(_.extend({}, this.options, {
modelView: app.ItemView,
el: this.$('ul')
}));
return this;
} });

Creating a view instance

A definition of a View is only a prototype that is later used to initialize an instance. The properties defined in the properties object will be available on every created instance.

For example:

// Create an instance of the previously extended View.
var issueHolder = new IssueHolderView();
// Render the view
issueHolder.render();

Examining the view instance

Once you've created an instance of a View, you can use your debugger in the browser to inspect the structure. At their essence, view instances are simply wrappers around a DOM Node/jQuery collection. Views must always have a root element to allow event delegation, provide a location to put template contents, and give you an end point to attach the View into the page Document.

To inspect, open the Elements/DOM tab of your Developer Tools (see Chapter 1: The Setup). Every View instance will have a cid property that uniquely identifies it on the client and is used for event delegation. This is automatically handled by Backbone.

Special properties

Backbone Views have a list of special properties that will be hijacked and purposed. Be aware that you should not try to assign arbitrary meaning to the following: model, collection, el, id, attributes, className, tagName, or events.

  • The model and collection properties are designed to have Backbone.Model and Backbone.Collection instances assigned to them for the View to render a visual representation of their value.
  • The el, id, attributes, className, and tagName properties will control how your View's internal element is generated if you dont pass one upon instantiation.
  • The events property is a declarative way of assigning DOM events based off event types, selectors, and method names.
  • Technically, any property that is not one of these special identifiers, will end up in the options object described later on. We will see how this is not respected in all cases and should not always be relied upon.

el

The el property is always an element after you create your instance. During the definition you can pass the el property a jQuery collection, a selector, or a DOM Node. Backbone will be smart enough to detect the type and normalize into a single DOM element.

The $el property will always be the jQuery sidekick of el, saving you numerous calls to $(this.el).
If el is not passed the element <tagName class="className" id="id" attributes> will be created for you.

options

Any key passed during instantiation that is not in the list of special keys will be put in the options object. This is inconsistent from how Models, Collections, and Routers handle options that are only available during constructor and initialize methods.

A good way to patch initialize to override your list of instance properties is by using some underscore methods to filter values from the options object like this:

var MyView = Backbone.View.extend({
template: _.template("Hello world!"),
initialize : function(options) {
_.extend(this, _.pick(options, ['template', 'whitelistedkey']));
} });

When you pass options to the View, they end up in options unless they are special, since all properties defined on a View are on the instance.

Templating

While not explicitly part of the Backbone.View API, templates are fundamental to a good abstraction of logic and presentation. The term can apply to many varied types and representations of the string contents.

Consider:
function() { return "Hello world!"; }
and:
Hello world!
They both represent "Hello world!", but in different ways.
Therefore when talking about templates, we mean anything
that is or can produce the HTML contents.

Inline

Typically the template property on a View is a function that accepts an object that is then interpolated into text and returned as a string.

In the following example, a template is assigned to the View inline. This means that the template exists within the JavaScript source code:

// Defining a new view that renders "Hello world!".
var IssueModalView = Backbone.View.extend({
// Compile a String template into a reusable Function.
template: _.template("<%= echo %>")
});

This approach may be seen as less than ideal, since you have: no syntax highlighting, very long lines, it blurs the lines of logic and presentation (potentially making it harder for designers), and it is not trivial to pre-compile these templates for production.

Script tag hack

A better way of handling templates is to place them all within your HTML markup, inside of <script></script> tags that have a non-JavaScript type attribute on them which will cause the browser to not execute the contents. This is a hack, which has gained mainstream endorsement, especially for example applications. This allows developers to quickly define templates outside of their JavaScript, but they can access them very easily.

To update the above example to use this new practice, we need to break out the template and then reference it within the View definition.

First write the markup:

<head>
<script type="template" class="my-template">
<%= echo %>
</script>
</head>

And then modify the View:

var MyView = Backbone.View.extend({
// Fetch the text contents of the template from the script tag.
template: _.template($(".my-template").html())
});

This approach isn't ideal since it's a lot of repetitive code. Ideally we can just provide the selector and deal with the actual compilation of the template function inside the render function when we need it.

This is a DRY-er (Don't Repeat Yourself) way of representing the same code above.

var MyView = Backbone.View.extend({
// Assign the selector to fetch the template contents from.
template: ".my-template",
// This method can be used by the render method to fetch the template,
// before using it. It's only job is to return a compiled template.
fetchTemplate: function() {
return _.template($(this.template).html());
}
});

For the example application that accompanies this book we chose this implementation of template organization, since it is the easiest way to consume and modify templates. It is great for getting started and does not require any dependencies to run.

This is still not a perfect solution. There is missing syntax highlighting, your HTML is bloated, and it can be confusing since it is a browser hack (storing templates in script tags meant for JavaScript), and it's not easy to pre-compile these templates.

How to pre-compile templates is explained in Chapter 9: Modules, Build Tools, & Preparing for Production. For the purposes of this chapter, pre-compiling templates can be understood as a method of compiling the string to a function before the application runs to avoid extraneous processing. This is problematic for the above two solutions since the raw templates exist in the markup and source.

Working with data

Once your template has been set up to accept data and return string markup, you will need to come up with a way to provide data to the template. This will almost always be trivial.

Instances or primitives

Depending on your template engine, you may wish to work with the JavaScript object instances themselves. You may want flat valid JSON objects instead, for engines like Mustache and Handlebars.

Here is an example of instances being used in underscore templates:

<% searchResults.each(function(result) { %> 
<li>
<a class='js-result' data-id='<%= result.id %>'>
<%- result.get('title') %></a>
</li>
<% }); %>
// Passing the data this.template({ searchResults:
this.collection });

You can see how you are writing JavaScript directly into the template.  The searchResults will often be data from a Backbone.Collection, and you may want to use the toJSON method which will flatten all its Backbone.Model instances down to simple JavaScript objects:

_.template({ searchResults: this.collection.toJSON() });

Creating a reusable function

A common implementation to keep your view methods small is to have a serializing method to format a View's Model or Collection into native a JavaScript object or array.

An implementation may look something like this:

var MyView = Backbone.View.extend({
// Use a method, so you can get at instance properties.
data: function() {
return { active: this.isActive };
}
});
// Create a new instance.
var myViewInstance = new MyView();
// Mark it as active.
myViewInstance.isActive = true;
// Check and ensure the data is correct.
console.log(myViewInstance.data()); // => { active: true }

This function can now return any kind of data that will then be passed to the template. This is discussed in the next section on rendering.

Rendering

Once you've fetched your template and have it as a function that will return markup and the data that will be passed to the function, you can put the two together and inject into the View's element.

This is typically done with a render function. Backbone actually provides a no-op render method by default. All it does is return this, it's implied that you bring your own render logic and that you maintain returning this for chainability:

Backbone.View.extend({
render: function() {
// Using the previously defined fetch method.
var template = this.fetch();
// Using the previously defined data method.
var data = this.data();
// Generate the markup.
var markup = template(data);
// Insert into this element.
this.$el.html(markup);
// Allow for chaining.
return this;
}
});

Events

The main value proposition of Backbone.View is helping you with events. Both DOM events react to clicks and user interaction, as well as data changes coming from the Model need to be rendered to the user.

DOM Events

DOM events are declarative events that are scoped to the View's #el property. They are set in the events property. This can either be an object or a function returning an object of events. Events are described by "eventType selector" : callback.

Let's examine the two ways of declaring views, taken from Hubbub modal views. Start by writing the following:

var ModalView = Backbone.View.extend({
events: {
// you can use any jQuery selectors here
'click .modal-mask': 'close',
'click .modal' : 'stopPropagation',
// note that an <input> element needs focus for this to work
'keydown': 'keydown'
} }); var WelcomeModalView = ModalView.extend({ events: function(){
return _.extend({}, ModalView.prototype.events,{
'click .exit': 'close',
'change .js-toggle-show-welcome': 'toggleShowWelcome'
});
} });

The context of all callbacks will be the View, which makes it easy to call upon other View methods.

Data driven Views

You can easily set up a View to automatically re-render whenever relevant data changes with events. These are normally set up in the initialize method.

Consider our ItemView that will re-render upon a change to its models attribute isActive:

var ItemView = Backbone.View.extend({
tagName: "li",
template: _.template($('#js-issue-item-template').html()),
initialize: function () {
this.listenTo(this.model.repo, 'change:isActive', this.toggle);
} });

Clean up considerations

If you create and tear down a lot of views, there is a chance you need to manually ensure proper cleanup once a view is removed. JavaScript garbage collection frees up variables as they aren't referenced from anywhere, and ideally when you tear down a view, you need to ensure that the view does not leave any references to external objects that in time will make that object into a ghost object.

Backbone.View provides a remove method you need to call on your view to safely tear it down, and if you have used listenTo/listenToOnce in order to be data driven, then those callbacks to your model/collection will be safely removed. If; however, you use on or your event bindings to external objects, you need to override the remove method and ensure cleanup is done. As a rule of thumb, you should stick to using listenTo unless you know what you are doing.

External considerations

It may be a good idea to add in a callable cleanup function. That way you can remove references to non-Backbone related objects:

Backbone.View.extend({ remove: function() { this.stopListening();
// Remove from some external dependency.
removeFromSomethingElse(this);
// This will call up the prototype chain and trigger the `__super__`
// `remove` method.
return Backbone.View.prototype.remove.apply(this, arguments);
}
});

Nesting views

When working with layouts, or Views that render collections, you often wish to maintain a relationship between the child Views. This can make it easier to maintain them and render out dynamic lists.

It's useful to create a views object or array that contains all of the nested Views. The following examples will layer on a ListView that can be extended and reused. This is used in the Hubbub application under views/main.js.

var ListView = Backbone.View.extend({ initialize: function() {
this.views = {}; } });

The power of managing views like this is that propagating downwards becomes really easy. Consider this very useful remove override:

Backbone.View.extend({
remove : function() {
_.invoke(this.views, 'remove');
Backbone.View.prototype.remove.call(this);
}
});

Appending into parent

You can use the jQuery append method to insert rendered sub Views elements. In the ListView abstraction a method is added called addModel and this creates an instance of a passed View named modelView, which assigns that to the internal Views object. It is then rendered and appended into the parent list.

Keep in mind that DOM operations are probably the most expensive part of your code, so you need to make sure you don't have repeating DOM updates. That means either inserting DOM content or injecting CSS styles causing repaints.


One of the first optimizations you want to make is to avoid the working, but naive, implementation below when it comes to View's rendering lists with many items. If only two to three items are rendered at the maximum, then this approach is okay: 

var ListView = Backbone.View.extend({
initialize: function() {
this.views = {};
this.listenTo(this.collection, {
add: this.addModel
});
},


addModel: function(model) {
this.views[model.cid] = new this.options.modelView({
collection: this.collection,
model: model
});
this.$el.append(this.views[model.cid].render().el);
}
});

When to create child Views

Using child Views has some pros and some cons.

Pros:

  • Can pass a distinct model and make re-render easy
  • Easier to keep all state out of the DOM
  • More modular code
  • Easier to make interchangeable modules
Cons:
  • N extra objects has higher memory and a CPU footprint
  • Event listeners are expensive, if each view handles its own event delegation things can get slow
  • It's hard to write an efficient render method for the parent View

For small lists (up to 5 items at a time) a full blown view is faster to develop and easier to maintain and for big lists (like a data grid with 10000 rows) you need to forget about creating a RowView. For list sizes a hybrid approach can be considered. You can create light weight views but handle the events in the parent view in order to not create extra DOM event listeners, then have your parent view call on the appropriate child view.

Plugins

You may find that you tire having to maintain your own View logic for common tasks like insert nested Views, View cleanup, and template loading.

This is a good time to investigate View plugins, which are designed to make your time working with Views more efficient.

Available options

There are several options available. The popular plugins at the time of writing this book are: Marionette, Chaplin, and LayoutManager. These are more like frameworks built on top of Backbone really. You can look into the complete list of plugins and the status of them at: https://github.com/documentcloud/backbone/wiki/Extensions%2C-Plugins%2C-Resources.

Conclusion

Now that you have your feet wet with Views, let's go on to the next chapter and learn how to use Models in Backbone.

There has been error in communication with Booktype server. Not sure right now where is the problem.

You should refresh this page.