Overview
Backbone views provide a useful convention and abstraction for user interfaces. However, to include UI functionality in your app that Backbone, on its own, was not designed to support, you’ll need to consider how to effectively integrate custom or third-party functionality into your Backbone app. As a result, developers must navigate challenges and avoid tricky conflicts between the external libraries and Backbone.
Intro to Backbone.js
Backbone is a fantastic way to organize your client-side code. With abstractions like models, views, and collections, Backbone helps serious developers write well-organized, scalable applications.
While there are many alternatives to Backbone, including Angular and Ember, Backbone provides developers with incredible freedom to write and organize their code in natural and comfortable ways without being too opinionated about what the Document Object Model (DOM) looks like.
The Skinny on Backbone Views
Views are one of the most powerful and flexible components in Backbone. According to the authors of Backbone:
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.
They are used to manipulate what users see in their browser, and they facilitate communication with models. As a result, in the paradigm of Model-View-Controller, it is useful to think about Backbone Views as both view and controller.
This has serious implications when developing applications with significant user interaction. In fact, there are many situations where you might want to use some other library to manipulate the DOM. Data visualization and web-based gaming are two examples where you might prefer to have another library handle some of your user-facing view rendering. As a result, you might consider using jQuery, d3.js, crossfilter, or three.js for some of your DOM manipulation needs.
Fortunately, there are ways to make Backbone play nicely with these other DOM manipulators.
Manipulating the Document Object Model in Backbone
Before we get into it, let’s review DOM manipulation in Backbone. Let’s start with a basic view object.
var SomeView = Backbone.View.extend({ // Some definition stuff here }); var aView = new SomeView();
Great. Now, let’s tell the view how to render itself by defining a .render()
method.
var SomeView = Backbone.View.extend({ // define how to render something render: function() { // get some HTML var htmlContent = "<p>This some HTML that will be inserted into the DOM</p>"; // insert the html this.$el.html(htmlContent); // return an instance of the object for chaining return this; } });
There are several things going on here, so let’s take it step by step.
Defining a .render()
Method
First, we define a .render()
method that encapsulates the logic necessary to render HTML. Note that Backbone comes with a .render()
method out of the box. However, it doesn’t do anything. It was designed to be overwritten with custom logic!
Getting HTML Content
The above example assumes that you get HTML somewhere. You can use underscores _.template()
.
Alternatively, we can use other templating libraries, like Handlebars
(my personal favorite). All that really matters is that, somehow, we get
some HTML content.
What the Hell is el
?
We need a place to put the HTML content; that’s what el
is for. Like .render()
, el
is an attribute that comes with Backbone Views out of the box. It
references the HTML element (and all its children) contained in this
view. In the above example, we did not specify el
. By default, el
is a div
. However, we could have easily set the parent element like so:
var SomeView = Backbone.View.extend({ el: "article", ... }); var aView = new SomeView(); console.log(aView.el); // an empty "article" HTML element
There is also $el
, which is just el
wrapped in jQuery. We'll see later on that $el
plays a powerful role in mastering Backbone views.
Returning this
…
Finally, we return a reference to the object itself to allow for chaining. While not strictly required, returning this
is a convention. Without return this
, we would need some way to access the element’s HTML content. The following code illustrates an alternative solution.
/** * If render() returns nothing, we are really * accessing the `el` property of undefined, which does not * exist! */ aView.render().el; // Should throw an error // Try accessing the HTML console.log(aView.el); // Should be empty (but defined!) // add HTML to the DOM of 'aView' aView.render(); // Try accessing the HTML again console.log(aView.el) // Should contain the HTML
Uhh, Nothing Is on the Screen!
Good point. Even though we called .render()
, there isn’t anything on the screen—what gives?
That is because we haven’t interacted with the DOM yet. All we did
was generate some HTML and represent it in a JavaScript object called aView
. Since we now have access to the generated HTML, all we have to do is append or insert the HTML in your web app’s DOM.
To move things along, we’ll also set up a mini-app so that when the page loads, the view appears. Below are what your HTML and JavaScript should look like.
Basic HTML Setup
<html> <head> <meta charset="utf-8"> <title>My Awesome Backbone App</title> <!-- Include your CSS Here --> <link rel="stylesheet" type="text/css" href="/css/styles.css" /> <!-- Include JS dependencies --> <!-- Backbone depends on underscore and, in this example, depends on jQuery. Please note the order of the dependencies --> <script src="/js/lib/jquery.js"></script> <script src="/js/lib/underscore.js"></script> <script src="/js/lib/backbone.js"></script> </head> <body> <div class="app"></div> <!-- Include your custom Backbone code in the below script --> <script src="/js/app.js"></script> </body> </html>
Here’s What Is Going On in App.js
// Create a view var SomeView = Backbone.View.extend({ initialize: function() {}, render: function() { var someHTML = "<p>This is some HTML</p>"; this.$el.html(someHTML); return this; } }); // Create a router var Router = Backbone.Router.extend({ // define your routes routes: { "": "home" }, home: function() { var aView = new SomeView(); $('.app').html(aView.render().el); } }); // Instantiate your router new Router(); // Start tracking history Backbone.history.start();
Go to your local server/browser, load up the page, and your application should be running!
Using Backbone and jQuery Simultaneously
Backbone’s flexibility allows our use of third-party libraries to manipulate the DOM. One scenario is when you want to use jQuery and Backbone simultaneously to manipulate your views. Below is an updated example.
var SomeView = Backbone.View.extend({ // Manipulate DOM indirectly by creating HTML content in a // Backbone View render: function() { var someHTML = "<p>Some HTML</p><p class='empty'><p>"; this.$el.html(someHTML); return this; }, // Manipulate DOM directly from within the Backbone View renderWithJQuery: function() { var otherHTML = "<p>Other HTML</p>"; $('.app').append(otherHTML); // may not make sense to return 'this' }, // another render method, to keep things interesting specialRender: function() { this.$('.empty').append("<span>No longer empty!</span>"); return this; } }); // Later in your app... // create the view var aView = new SomeView(); // change the DOM to reflect the newly created view $('.app').html(aView.render().el); // append more content directly to the DOM using jQuery within // a Backbone view object aView.renderWithJQuery();
The code above will result in two paragraphs on the page. The first paragraph contains “Some HTML”. The second paragraph contains “Other HTML”.
To test your understanding of this, reverse the method calls like so:
// SomeView is already defined var aView = new SomeView(); aView.renderWithJQuery(); $('.app').html(aView.render().el);
The code above will result in one paragraph: “Some HTML”. In both cases, there is also a <p>
element with nothing in it. We’ll discuss this in a moment.
Manipulating the DOM in Backbone Views Efficiently
Understanding the magic of efficient DOM manipulation (and traversal) requires an understanding of this.$el
and this.$()
. By using this.$el
, we are scoping DOM manipulation to the content contained in the view. By using this.$()
, we are scoping DOM traversal to the DOM tree within the view.
As a result, in the Backbone context, some uses of $()
(instead of this.$()
) could
be inefficient. For example, let’s say we wanted to traverse the DOM to
find some element. We might use any of the common DOM traversal
methods, including .find()
, .children()
, .closest()
, .first()
, and so on.
If we know, a priori, that the element we seek lies somewhere within the view’s DOM, then we should use this.$()
to avoid searching a larger DOM tree unnecessarily. If the element we seek lies outside of the view’s DOM, then we’ll need to use $()
.
For example, the .specialRender()
method uses localized DOM traversal to ensure that we search for elements with class empty
within the context of the view. If found, it sets the HTML content of those elements to include a span
and the text "No longer empty".
Conclusion
In this article we reviewed Backbone views, discussed how to render Backbone views in the DOM, and explored how to make Backbone play nicely with other libraries you might want to use to manipulate the DOM. We also learned about localized DOM traversal and identified methods to efficiently and inefficiently traverse the DOM.
The next part of this article will delve deeper into more
complicated examples of getting multiple libraries to work together in
manipulating the DOM.
Comments