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.
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).
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.
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;
} });
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();
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.
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
.
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.el,
id
, attributes
, className
, and tagName
properties will control how your View's internal element is generated if you dont pass one upon instantiation.events
property is a declarative way of assigning DOM events based off event types, selectors, and method names.options
object described later on. We will see how this is not respected in all cases and should not always be relied upon.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.
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.
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.
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.
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.
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.
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() });
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.
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;
}
});
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 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.
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);
} });
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.
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);
}
});
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);
}
});
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);
}
});
Using child Views has some pros and some cons.
Pros:
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.
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.
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.
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.