Tutorial: Nested views using Backbone Marionette’s CompositeView
A reader recently asked me how I would go about using Marionette to create an accordion like Twitter’s Bootstrap collapse component. In this post, I’ll not only give you my take on the answer, but also guide you through my reasoning. My hope is that by following my reasoning, you’ll be able to apply any time you time “how can I do this with Marionette” ?
Update (June, 2013): I’ve written a book on Marionette. It guides you through developing a complete Marionette application, and will teach you how to structure your code so large apps remain manageable.
The Goal
So, what are we after ? Well, in one URL: this. Let’s take a hint from Newton and “stand on the shoulders of giants”. In other words, we’ll build the DOM using Marionette but we’ll build it exactly like Bootstrap expects it, so that we can delegate the accordion functionality to Bootstrap. As we’ll see, this will allow us to have the accordion functionality “for free” while also being able to interact with the views using Backbone (e.g. responding to
click
events).
Getting Started
As usual, you can follow along using the source code. Grab this commit to get a head start: it contains a basic index.html file and the required support files.
So what are we setting out to do? We’ll create an accordion listing a few superheroes in an accordion, and we’ll list some of their enemies when the user clicks on a superhero.
The HTML we need
The first step is getting a working example of the behavior we want leveraging, the proper libraries. In other words, we need to figure out how to get the accordion working with Bootstrap’s js library.
After reading Bootstrap’s documentation, we find out we need to have the following HTML and Bootstrap will take care of the rest:
<div id='heroesList'>
<div>
<div>
<a data-toggle='collapse' data-parent='#heroesList' href='#hero1'>Batman</a>
</div>
<div id='hero1' style='height: 0px;'>
<div>
<ul>
<li>Bane</li>
<li>Ra's Al Ghul</li>
<li>The Joker</li>
<li>The Riddler</li>
</ul>
</div>
</div>
</div>
<div>
<div>
<a data-toggle='collapse' data-parent='#heroesList' href='#hero2'>Spider-Man</a>
</div>
<div id='hero2' style='height: 0px;'>
<div>
<ul>
<li>Green Goblin</li>
<li>Venom</li>
</ul>
</div>
</div>
</div>
<div>
<div>
<a data-toggle='collapse' data-parent='#heroesList' href='#hero3'>Wonder Woman</a>
</div>
<div id='hero3' style='height: 0px;'>
<div>
<ul>
<li>Ares</li>
<li>Doctor Psycho</li>
<li>Silver Swan</li>
</ul>
</div>
</div>
</div>
</div>
If you drop that into index.html
, you’ll have a working example of what we’re trying to achieve here.
First Step: Listing the Superheroes
So what’s the first thing we should try doing? Well, when we look at our ultimate goal, the first thing we see is a list of superheroes. So let’s start there!
Obviously, we first need to have a Backbone.marionette application to use, and a region to display things in. Let’s add them in quickly with this commit. And we’ll have a hard time displaying things without any data, so let’s add data to index.html.
Now that we have the basics wired up, let’s see about displaying these superheroes. When all sections are collapsed, the display really looks like a simple collection view, so let’s start there.
Our collection view will need a template, so let’s see what we should use. Looking at our “HTML goal”, we see only the accordion header is needed to display our superheroes’ names. So our template will be (commit):
<script type='text/template' id='accordion-group-template'>
<div>
<a data-toggle='collapse' data-parent='#heroList'
href='#hero<%= id %>'>
<%= name %>
</a>
</div>
</script>
With our template in place, we can now define our collection and item views (commit):
HeroView = Backbone.Marionette.ItemView.extend({
template: '#accordion-group-template'
});
AccordionView = Backbone.Marionette.CollectionView.extend({
itemView: HeroView
});
We still don’t have anything functional though: Marionette’s collection view is designed to take a Backbone collection and render each item. Where’s our collection? Let’s add it along with the base model (commit):
Hero = Backbone.Model.extend({});
Heroes = Backbone.Collection.extend({
model: Hero
});
Does our app work, now? Nope: we haven’t started it, or given it any data work with. Let’s do that now, in index.html (commit):
MyApp.addInitializer(function (options) {
var heroes = new Heroes(options.heroes);
var accordionView = new AccordionView({
collection: heroes
});
MyApp.mainRegion.show(accordionView);
});
MyApp.start({ heroes: heroes });
Quick side note: this code could also be put in the “application.js” file. But in a large (possibly progressively enhanced) application, you’re going to initialize the application with different data (depending on which page the user landed on). Therefore, you’re better off putting the initializing code in the html.
So, what’s going on in the code we just added ? We added an initializer, which is essentially “code to be run right after the Marionette application is started”. This initializer has the “options” argument, which contains anything passed on to the application when the call to “start()” is made.
Here, we’re saying:
- I will give you some heroes when I start the application
- Take those heroes and create a Backbone collection with them
- Instantiate an AccordionView with the heroes
- Display that view in the “mainRegion” we defined at the beginning
After all of that is clarified we simply start the application, giving it the list of heroes, as promised. These heroes are the ones we defined in the “add data” step, and which we stored in the “heroes” javascript variable. In other words, the options given to the application contains “heroes: heroes”, because the one on the left is the option name (which we refer to in the initializer with “options.heroes”), and the one on the right is the name of the javascript variable we used to store the initial data.
If you now run the application, you’ll see a list of superheroes. Except they’re not really styled properly: there’s no border around them. Why is that? Most likely, we’re missing some CSS classes for the styling to be applied. Looking at our “HTML goal”, we can see that assumption is correct. Let’s fix that (commit):
HeroView = Backbone.Marionette.ItemView.extend({
template: '#accordion-group-template',
className: 'accordion-group';
});
AccordionView = Backbone.Marionette.CollectionView.extend({
className: 'accordion',
itemView: HeroView
});
Much better ! Just to show ourselves that we’re indeed dealing with Backbone views, let’s add an event we’ll respond to. When we click the header in the item view, we want to display the superhero’s wikipedia page in Firebug’s console (commit):
events: {
'click a': 'logInfoUrl'
},
logInfoUrl: function() {
console.log(this.model.get('info_url'));
}
Introducing CompositeView
So we have something that look like a good start: our superheroes are being displayed properly, and our Backbone views can process events. But we still need to display each hero’s list of villains.
And now, our problem is that our handy item view really isn’t designed for this. It’s made to display ONE item, just like it’s name implies. So what are we to do? First, read Derick Bailey’s excellent blog post on nested views. Seriously: go read it, then come back. You’ll have a much better understanding of what’s going on.
As you’ve gathered from Derick’s blog post, we’ll need to use composite views instead of item views. So let’s start by replacing our current item view with a composite view (commit):
HeroView = Backbone.Marionette.CompositeView.extend({
// the rest of the code stays the same
});
You’ll notice our application still fully works. Nice !
So now we need to alter our composite view to add the HTML that will contain our villains (commit):
<script type='text/template' id='accordion-group-template'>
<div>
<a data-toggle='collapse' data-parent='#heroList'
href='#hero<%= id %>'>
<%= name %>
</ a>
</div>
<div id='hero<%= id %>' style='height: 0px;'>
<div>
<ul>
</ul>
</div>
</div>
</script>
Now that we have a composite view with a template to render the “outer” HTML, we still need an item view (and corresponding template) to render the list of villains (commit):
<script type='text/template' id='villain-template'>
<%= name %>
</script>
VillainView = Backbone.Marionette.ItemView.extend({
template: '#villain-template',
tagName: 'li'
});
Great. But we still need to tell the composite view that it should render each villain using the VillainView. But where should the VillainView be rendered? Well, we need to tell the composite view where to render it, too (commit):
HeroView = Backbone.Marionette.CompositeView.extend({
// edited for brevity
itemView: VillainView,
itemViewContainer: 'ul'
// edited for brevity
});
So our composite view will take a collection and render the VillainView for each one (and it will be rendered within the
ul
tag). But which collection will be rendered? Right now, the composite view gets called as the item view specified in our AccordionView collection view, so it gets a superhero model passed on to it.
But our composite view must render the villain collection, which it has no notion of. So let’s set the composite view’s collection when the view gets initialized (commit):
HeroView = Backbone.Marionette.CompositeView.extend({
// edited for brevity
initialize: function () {
this.collection = this.model.get('villains');
},
// edited for brevity
});
As I said, the composite view is given a hero model, because it’s being used as the “item view” referenced by a collection view. In other words, here’s what happens : our AccordionView is given a collection of superheroes and for each one, it will use HeroView to render the model. So now that we’re in HeroView, we can access the superhero using
this.model
and we can set the composite view’s collection by accessing the proper attribute.
And since we’re talking about a collection of villains, we should add the collection and model to our application (commit):
Villain = Backbone.Model.extend({});
Villains = Backbone.Collection.extend({
model: Villain
});
Are we ready to try our new functionality? Not quite: the initial data we use doesn’t have a collection of villains. It has an array of objects linked to the
villains
attribute. Seems similar enough, but computers need to be told everything. So let’s initialize the villains as collections in our initializer (commit):
MyApp.addInitializer(function (options) {
var heroes = new Heroes(options.heroes);
// each hero's villains must be a backbone collection
// we initialize them here
heroes.each(function (hero) {
var villains = hero.get('villains');
var villainCollection = new Villains(villains);
hero.set('villains', villainCollection);
});
// edited for brevity
});
Now, if we try out our application we can see the accordions open properly, and will close again if we click on the header again. But if we click on “Batman”, then “Wonder Woman”, the first accordion should close, displaying only Wonder Woman’s villains. Why isn’t it working properly ?
If we look at our “HTML goal” once again, we’ll notice our DOM isn’t quite the same: we’re missing the
id
attribute on the topmost level. This is used by Bootstrap’s
data-parent
to be able to close the open accordion when another one is opened. So let’s add it (commit):
AccordionView = Backbone.Marionette.CollectionView.extend({
id: 'heroList',
className: 'accordion',
itemView: HeroView
});
Note that “javascript library that doesn’t quite work” when rendered using Backbone and/or Marionette is usually because the DOM doesn’t match what the javascript library expects. Particularly, make sure you don’t have extra elements polluting your DOM tree: Backbone create a new DOM element for each view (a div by default). To prevent this, use the className
, tagName
, and
id
attributes in your views to add the proper HTML attributes in the rendered element.
Before we call it a day, let’s quickly add event listeners our our villain view (commit):
VillainView = Backbone.Marionette.ItemView.extend({
template: '#villain-template',
events: {
'click': 'logInfoUrl'
},
logInfoUrl: function () {
console.log(this.model.get('info_url'));
}
});
And there you have it ! An accordion driven by Bootstrap, which is rendered using Backbone.Marionette, and responds to Backbone events by displaying information in the console. Enjoy !
Would you like to see more Elixir content like this? Sign up to my mailing list so I can gauge how much interest there is in this type of content.