Lists of Data
A key component of views is being able to handle lists (or collections) of data.
The CollectionView
and CompositeView
are specifically designed to deal with
these lists by binding to the Backbone.Collection
.
When you bind a collection to your collection view, it will iterate over each
item in the list, rendering a new view for each one. Let's start with an example
CollectionView
with our items using a item.html
template called 'list.js':
<a href="<%- url %>"><%- text %></a>
var Item = Marionette.LayoutView.extend({
tagName: 'li',
template: require('./item.html')
});
var List = Marionette.CollectionView.extend({
tagName: 'ul',
childView: Item
});
module.exports = List;
Now let's create an instance of this with our collection:
var List = require('./list');
var collection = new Backbone.Collection([
{text: 'Some Text', url: '/items/1'},
{text: 'Some other text', url: '/items/4'}
]);
var view = new List({
collection: collection
});
view.render();
The output from this will look like:
<ul>
<li><a href="/items/1">Some Text</a></li>
<li><a href="/items/2">Some other text</a></li>
</ul>
If we want to add an item to this list, we simply do:
collection.add({
text: 'New Item',
url: '/items/8'
});
The CollectionView
will recognize the item has been added and inject the item
into the HTML template at the right location.
Events on Collections
Just like we can bind modelEvents
we can also bind collectionEvents
to our
views. The full list of events can be found in the
Backbone documentation. This includes events for add
and remove
(which is what Marionette listens to internally). Let's see an example:
var List = Marionette.CollectionView.extend({
tagName: 'ul',
childView: Item,
collectionEvents: {
add: 'itemAdded'
},
itemAdded: function(collection) {
alert('New item added');
}
});
Now, whenever an item gets added, no matter how, an alert box will be displayed for each one.
Rendering Tables
You'll notice the CollectionView
doesn't define its own template. This makes
it unsuitable for more complex listed layouts such as tables. To solve this
problem, we have the CompositeView
- a view that lets us assign a template to
be rendered.
Let's take two templates making up a table: row.html
and table.html
<thead>
<tr>
<th>Name</th>
<th>Nationality</th>
<th>Gender</th>
</tr>
</thead>
<tbody></tbody>
<td><%- name %></td>
<td><%- nationality %></td>
<td><%- gender %></td>
We'll now use the CompositeView
to build this:
var Row = Marionette.LayoutView.extend({
tagName: 'tr',
template: require('row.html')
});
var Table = Marionette.CompositeView.extend({
tagName: 'table',
template: require('table.html'),
childView: Row,
childViewContainer: 'tbody'
});
module.exports = Table;
The CompositeView
takes two extra required attributes: the familiar template
and childViewContainer
- a jQuery selector to the element in our template to
attach the childView
elements. Using the CompositeView
is identical to the
CollectionView
:
var Table = require('./table');
var collection = new Backbone.Collection([
{name: 'John Smith', gender: 'male', nationality: 'UK', url: '/items/1'},
{name: 'Jane Doe', gender: 'female', nationality: 'USA', url: '/items/4'}
]);
var view = new Table({
collection: collection
});
view.render();
Adding and removing items works exactly as you'd expect - the new views are injected at the correct locations in the template.
Binding Models
The CompositeView
has another advantage over CollectionView
- it can take
and additional model
argument and render based on the contents of the model.
Let's say we wanted to know how many people were in the final list above, we'll
modify our table.html
:
<thead>
<tr>
<th>Name</th>
<th>Nationality</th>
<th>Gender</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<th>Total</th>
<td colspan="2"><%- total %></td>
</tr>
</tfoot>
This total needs to come from a model which we'll pass when creating the table:
var Table = require('./table');
var collection = new Backbone.Collection([
{name: 'John Smith', gender: 'male', nationality: 'UK', url: '/items/1'},
{name: 'Jane Doe', gender: 'female', nationality: 'USA', url: '/items/4'}
]);
var model = new Backbone.Model({
total: 30
});
var view = new Table({
collection: collection,
model: model
});
view.render();
And that's it, our table now has access to the total
field in the model!
Events
Our CompositeView can now listen to both collectionEvents
and modelEvents
at
the same time. One good use for this is a report table with a summary that is
calculated and fetched separately:
var Table = Marionette.CompositeView.extend({
tagName: 'table',
template: require('table.html'),
childView: Row,
childViewContainer: 'tbody',
modelEvents: {
sync: 'render'
},
collectionEvents: {
update: 'checkStatus'
},
checkStatus: function(collection) {
collection.each(function(model) {
// Do something
});
}
});
With this example, when our model is fetched from the server, we'll re-render the table. When our collection is modified, we run another handler that, in this case, does some form of checking/modification for the collection.