In the last chapter, we discussed CollectionView
. This chapter is about CompositeView
, which inherits most of its functionality from CollectionView
, so most of what I’ve mentioned in chapter 5 is also true for CompositeView
. There are differences, obviously — otherwise we wouldn’t need both view classes — and instead of going through everything about CompositeView
, this chapter will only explain what’s different between CollectionView
and CompositeView
.
CollectionView
cannot render templates (it simply appends the child views to its el
), but CompositeView
can render a template, so rendering — and the properties that affect it — is probably the biggest difference between the two view types. There are also some other aspects that affect the differences in how the two views render, so let’s take a closer look.
CompositeView
renders a template in much the same way ItemView
does. The first and most obvious thing you need to do is provide a template
property. Go back and look at ItemView
in chapter 4 for more information about how that works.
An example of a CollectionView
that would need a template is building a table with headers. Each child view is a row in the table, but the first row of the table — the headers — isn’t built from data, so it should be in a template. Having a template would also allow you to add in the thead
and tbody
elements for more semantic goodness.
Now, since we’ll be rendering child views, we need to inform the view where child views will be shown within our template. By default, if you don’t specify where the child views should be added, they will be appended to the view’s root el
, right after the template. This can be what you want sometimes, but generally you will need to place them somewhere inside your template. To do this, we use the itemViewContainer
property to specify a selector for the element we want the child elements placed into.
There is something about the way CompositeView
renders the template that is slightly different from ItemView
. ItemView
sends either the model or the collection to the template for rendering, depending on what exists (or an empty object if neither exists). CompositeView
will only send the model (or an empty object if there is no model) to the template. It will then render the collection (if there is one) as child views.
This means we need both a model and a collection in order to make the CompositeView
useful (though usually we only need to provide the model, as the collection will be extracted from it, as you’ll see later on). Also, if your template doesn’t require any data (such as when it’s just building the shell of a table or list), you can do without the model as well. It renders the data from the model within the template and then iterates through the collection, creating a child view for each model in that collection. If all you have is a model, it’ll pretty much act just like an ItemView
. If all you have is a collection, then I hope your template doesn’t require any data.
Another interesting and unique thing about CompositeView
is that although you can, you don’t need to supply an itemView
property. If an itemView
is provided, it’ll act like a CollectionView
and render each child view as the type of view specified. But if itemView
isn’t specified, it will default to recursively using its own class for each child view.
Recursion is often difficult for people to wrap their heads around, even if they are familiar with the concept and have seen it in practice. This is more than a simple function calling itself, so I’ll try to give some concrete examples, which I’ll steal from a post by Derick Bailey1, since he has a simple example that does a good job at focusing on the concept. However, as some people pointed out in the comments of that article, there are some flaws in the way he implemented the example, so I’ll actually use the code sample2 that one of those commenters offered as a replacement.
Before I show how CompositeView
recursively creates the views, though, I need to show you how recursive data works. Without recursive data, there is no reason to make the views recursive, since views are just visual representations of the data. When I talk about recursive data, I’m really talking about a tree of data in which a single node can have child nodes. There are plenty of great places to learn about composite data structures online3, and it’s a bit outside the scope of this book to go into it too much. If you don’t already know much about it, then I suggest you learn more about it online before continuing. I’ll still show examples of this sort of data structure so you know how it’s implemented in Backbone.
We’ll start with a collection of models. The models will simply be called “nodes” so I don’t need to contrive some strange reason for having a tree.
var Node = Backbone.Model.extend({
initialize: function(){
var nodes = this.get("nodes");
// Covert nodes to a NodeCollection
this.set('nodes', new NodeCollection(nodes));
},
toJSON: function() {
// Call parent's toJSON method
var data = Backbone.Model.prototype.toJSON.call(this);
if (data.nodes && data.nodes.toJSON) {
// If nodes is a collection, convert it to JSON
data.nodes = data.nodes.toJSON();
}
return data;
}
});
NodeCollection = Backbone.Collection.extend({
model: Node
});
As you can see, when a model is created, it will convert the nodes
property’s data to a NodeCollection
from that data and assign it to the nodes
property. Now you have a node that contains a collection of nodes. The nodes in that collection could also have their own collections, but not necessarily (no one likes infinite recursion). We also customize the toJSON
method to also call toJSON
on the child NodeCollection
.
Here’s some sample data that we could use plus the code to set it up:
var nodeData = [
{
nodeName: "1",
nodes: [
{
nodeName: "1.1",
nodes: [
{ nodeName: "1.1.1" },
{ nodeName: "1.1.2" },
{ nodeName: "1.1.3" }
]
},
{
nodeName: "1.2",
nodes: [
{ nodeName: "1.2.1" },
{
nodeName: "1.2.2",
nodes: [
{ nodeName: "1.2.2.1" },
{ nodeName: "1.2.2.2" },
{ nodeName: "1.2.2.3" }
]
},
{ nodeName: "1.2.3" }
]
}
]
},
{
nodeName: "2",
nodes: [
{
nodeName: "2.1",
nodes: [
{ nodeName: "2.1.1" },
{ nodeName: "2.1.2" },
{ nodeName: "2.1.3" }
]
},
{
nodeName: "2.2",
nodes: [
{ nodeName: "2.2.1" },
{ nodeName: "2.2.2" },
{ nodeName: "2.2.3" }
]
}
]
}
];
var nodes = new NodeCollection(nodeData);
So that’s the data that will be used for the example. Let’s prepare the views.
We’re going to create a CompositeView
that creates a simple list out of this data, like this:
•1
•1.1
•1.1.1
•1.1.2
•1.1.3
•1.2
•1.2.1
•1.2.2
•1.2.2.1
•1.2.2.2
•1.2.2.3
•1.2.3
•2
•2.1
•2.1.1
•2.1.2
•2.1.3
•2.2
•2.2.1
•2.2.2
•2.2.3
To do this, each model will be represented by a li
element, and each collection of models will be represented by a ul
element. Since the CompositeView
itself actually represents the model, we need a way to put all of them into an initial ul
element. For this, we’ll also create the following CollectionView
:
var TreeRoot = Marionette.CollectionView.extend({
tagName: "ul",
itemView: TreeView
});
TreeView
will be our CompositeView
. TreeRoot
will take the nodes
collection we created earlier and create the two main CompositeView
s, which will take it from there. First of all, we should have a template for the CompositeView
. Let’s add that to the HTML right now:
<script id="node-template" type="text/template">
<%= nodeName %>
<ul></ul>
</script>
It’s a very simple template. It just displays the nodeName
from the model and has a ul
to add the child views to. Alright, let’s stop beating around the bush and finally create that CompositeView
.
var TreeView = Marionette.CompositeView.extend({
template: "#node-template",
tagName: "li",
itemViewContainer: "ul",
initialize: function(){
// grab the child collection from the parent model
// so that we can render the collection as children
// of this parent node
this.collection = this.model.get('nodes');
},
});
Let’s step through each line:
•Create TreeView
by extending Marionette.CompositeView
.
•Assign the template that we created.
•Give it a tagName
of li
. This is built into Backbone and changes the root element’s (this.el
) type.
•Set the itemViewContainer
to ul
, so that all the child views will be appended to the ul
in the template.
•Use the initialize
function to set up our collection. Since each TreeView
will only be receiving the models, we need to extract the collections out of the models.
None of that should have been new except the last step. That last step is very important, and you may often need to do something similar when using the CompositeView
in its default recursive mode.
Now the only thing that’s left to do is render it all and put it on the page:
var tree = new TreeRoot({collection: nodes});
tree.render();
$('body').append(tree.el);
Practically everything from ItemView
also applies to CompositeView
. In particular, templateHelpers
and dynamic templates via overriding getTemplate
make an appearance at the CompositeView
party. There isn’t really any reason to go deeper into either one, but I thought I’d show you a way of overriding getTemplate
that would make our example a little better.
Right now, we only have a single template and that template has the child ul
element in it. This means that even CompositeView
s that have no collection will display the ul
but it will be empty. Let’s come up with a way to render a different template when we don’t need the ul
.
First, let’s add the new template. It will simply remove the ul
from the current template.
<script id="leaf-template" type="text/template">
<%= nodeName %>
</script>
Now we need to change TreeView
so that it will use a dynamic template:
var TreeView = Backbone.Marionette.CompositeView.extend({
getTemplate: function() {
if (_.isUndefined(this.collection)) {
return "#leaf-template";
} else {
return "#node-template";
}
},
tagName: "li",
itemViewContainer: "ul",
initialize: function(){
this.collection = this.model.get('nodes');
},
});
We don’t even need to specify a template
property, but we do need to override getTemplate
to choose which template to use depending on whether or not we have a collection. This may make the JavaScript code a bit more cluttered, but it cleans up the HTML, and depending on your style sheet, clearing those extra ul
elements out of there may be absolutely necessary.
As usual, CompositeView
has built-in events and callbacks, most of which are inherited from CollectionView
. CompositeView
implements a few of its own events between the ones it inherits. The only events it adds, though, are during rendering. All the other events are inherited. Let’s take a look at the new events and callbacks.
Event Name | Callback Name | Description |
---|---|---|
composite:model:rendered | onCompositeModelRendered | After all of the before:render events, the template and model data will be rendered. This event will fire immediately after that rendering. |
composite:collection:rendered | onCompositeCollectionRendered | This event fires following the child views being rendered. |
composite:rendered | onCompositeRendered | This event fires immediately after composite:collection:rendered and just before the render and collection:rendered events. |
Table 5: New events and callbacks in CompositeView
. (4.)
Like CollectionView
, CompositeView
also listens for and forwards the events of the child views. That’s a lot of events being fired. Here’s a sample of the events (in order) that you would see from a recursive CompositeView
that is rendering only two child views:
•before:render
•collection:before:render
•composite:model:rendered
•before:item:added
•itemview:before:render
•itemview:collection:before:render
•itemview:composite:model:rendered
•itemview:composite:collection:rendered
•itemview:composite:rendered
•itemview:render
•itemview:collection:rendered
•after:item:added
•before:item:added
•itemview:before:render
•itemview:collection:before:render
•itemview:composite:model:rendered
•itemview:composite:collection:rendered
•itemview:composite:rendered
•itemview:render
•itemview:collection:rendered
•after:item:added
•composite:collection:rendered
•composite:rendered
•render
•collection:rendered
That’s noticeably more than CollectionView
, but as I said earlier, you don’t need to memorize them. Just look them up when you need them.
It’s truly amazing that something as intricate and complex as displaying a collection or a composite architecture can — for the most part — be simplified down to just a few pieces of configuration. Together, the view classes are probably the most powerful and useful pieces that Marionette provides. Of course, that doesn’t mean that Marionette isn’t chock-full of other extremely useful goodies, as you’ve already seen and will soon see again in the upcoming chapters.