In the last article, we covered the basics of Backbone Views and how to enable jQuery to seamlessly manipulate the same DOM that Backbone manipulates.
In this article, we will explore how we can make Backbone play nicely with d3.js. The concepts in this article should apply to situations where you intend to use Backbone with most other libraries that also manipulate the DOM.
Backbone + d3.js Working on the Same DOM
d3.js, written by Mike Bostock, is another widely used library that manipulates the Document Object Model, primarily to visualize data. In fact, you can get really creative with the way data can be visualized.
At the lowest level, d3.js interacts with HTML and/or SVG elements and manipulates their attributes by binding data to the Document Object Model.
Here is a quick example of how d3 operates:
var numericalData = [1,2,3,4,5,6]; d3.select("body").selectAll("p") .data(numericalData) .enter() .append("p") .text(function(d) { return "I’m number " + d + "!"; });
The above code does the following:
- Selects and creates a reference to the
body
element - Within that selection, selects all of the
p
elements currently in the DOM - Appends each element in
numericalData
to a selectedp
element - For each
p
element that has yet to exist (i.e., some elements innumericalData
that still need to be appended), creates ap
element and adds it the DOM - Sets the text node in each newly created
p
element to contain some text (including the relevant number innumericalData
)
A First Attempt to Make Backbone and d3.js Play Nicely
Leveraging what we learned in the previous article, here is one implementation of a shared DOM manipulation.
var CoolView = Backbone.View.extend({ render: function() { var html = "<p>I am not a number!</p>"; this.$el.html(html); return this; }, renderVisualization: function(arrayOfData) { d3.select("body") .selectAll("p") .data(arrayOfData) .enter() .append("p") .text(function(d) { return "I’m number " + d + "!"; }); } }); var coolView = new CoolView(); coolView.render(); var myData = [10, 9, 4, 2, 1]; coolView.renderWithD3(myData);
Houston, We’ve Got a Problem!
Assuming that our goal is to preserve the existing p
element and append the other p
elements to DOM, when we execute the above code, we quickly run into a major problem.
The .render()
inserts a p
element with the text “I am not a number” into the DOM. But .renderVisualization()
selects all existing p
elements in the DOM and inserts content into those elements. This overwrites the text in the original p
element, despite our appending to the DOM using d3.append()
.
Solutions to Getting d3.js and Backbone to Play Nicely Together
In this simple example, at least two simple solutions exist.
- Use a more specific CSS selector
- Use a different tag altogether
Cordoning Off a Portion of the DOM
For more complicated examples, we may need alternative strategies. One potential solution is to cordon off a portion of the DOM that will be operated on by one of the libraries. For example:
var CoolView = Backbone.View.extend({ ... renderVisualization: function(arrayOfData) { d3.select("#visualization") .selectAll("p") .data(arrayOfData) .enter() .append("p") .text(function(d) { return "I’m number " + d + "!"; }); } }); var coolView = new CoolView(); var myData = [10, 9, 4, 2, 1]; coolView.renderVisualization(myData);
In the above case, Backbone continues to manage the to-be-created view. However, let’s say that there is an element somewhere in the DOM (inside or outside of the DOM managed by the Backbone view) whose id is “visualization”. We can capitalize on this fact by scoping all of our d3-related DOM manipulations to this element. As a result, all d3 chained methods, including .selectAll("p")
and .append("p")
, are executed in the context of #visualization
.
Encapsulate Differentiated DOM Manipulation With Sub-Views
Finally, another approach to managing third-party view functionality is to use subviews. Here is how that might look.
var CoolView = Backbone.View.extend({ render: function() { var subViewA = new SubViewA(); var subViewB = new SubViewB(); // set the html content for coolview this.$el.html(subViewA.render().el); // append more html content to coolview this.$el.append(subViewB.render().el); return this; } }); // Elsewhere, perhaps in your router... var coolView = new CoolView(); $('.app').html(coolView.render().el);
In this scenario, subViewA
might contain non-visualization content, and subViewB
might contain visualization content.
Another Shared-DOM Manipulation Approach for Backbone + d3.js
There are times when you need to make sure that your d3 DOM manipulation occurs in the same context as the Backbone view itself. For example, simply cordoning off a portion of the DOM with #visualization
provides no guarantee that the element exists within or outside of the part of the DOM represented by the Backbone view.
One alternative is to ensure that your starting d3 selection references the same element as the one pointed to by this.$el
. However, if you tend not to set el
or any of its attributes directly, it would be difficult to sufficiently target the current view using CSS.
Luckily, in the d3.js library, there exists a method that enables direct node selection. Let me introduce you to d3.select(node)
. According to the documentation, this method:
Selects the specified node. This is useful if you already have a reference to a node, such as d3.select(this) within an event listener, or a global such as document.body. This function does not traverse the DOM.
This method allows us to write the following code and ensure that d3 manipulations occur within the context of the Backbone view.
var CoolView = Backbone.View.extend({ // note that "el" is not set directly render: function() { var html = "<p>I am not a number!</p>"; this.$el.html(html); return this; }, renderVisualization: function(arrayOfData) { d3.select(this); // will not work, "this" refers to a Backbone object d3.select(this.el) //pass in a node reference .selectAll("p") .data(arrayOfData) .enter() .append("p") .text(function(d) { return "I’m number " + d + "!"; }); } });
Conclusion
In summary, there are a number of considerations when making Backbone play nicely with other DOM manipulators.
- Be sure to select the right element to manipulate. Using specific CSS classes and ids can make life much easier.
- Clever uses of the various DOM manipulation methods in either library can go a long way. Be sure to clarify the DOM action (append, prepend, insert, remove) you really need.
- Looping operations in the DOM can be tricky. Ensure that the correct elements are impacted.
There are also a number of alternatives to consider, depending on the outcome you seek:
- Separating out a portion of the DOM
- Scoping the other library’s DOM manipulation to the Backbone context
- Using sub-views to encapsulate the impact of other DOM manipulators
Comments