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.