A simple Backbone.Marionette tutorial (part 2)

In the previous post, we started building a simple cat leader board (see it live!) and got all the way to displaying the list of cat names. Let’s finish and get to the result!

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. Also, since this tutorial was written, Marionette has evolved with new functionality and better techniques to write scalable javascript apps. These are naturally covered in the book.

Note: Backbone & Marionette have come out with new versions that required modifications to the code, which you can see here.

Adding images

Let’s start by simply adding images in our table. All we need to do is add an attribute to store the path to the image that represents the cat, and to update our templates (code):


$(document).ready(function(){
  var cats = new AngryCats([
    { name: 'Wet Cat', image_path: 'assets/images/cat2.jpg' },
    { name: 'Bitey Cat', image_path: 'assets/images/cat1.jpg' },
    { name: 'Surprised Cat', image_path: 'assets/images/cat3.jpg' }
  ]);

  MyApp.start({cats: cats});
});

 


<script type="text/template" id="angry_cats-template">
<thead>
<tr class='header'>
<th>Name</th>
<th>Image</th>
</tr>
</thead>
<tbody>
</tbody>
</script>

 

<script type="text/template" id="angry_cat-template">
<td><%= name %></td>
<td><img class='angry_cat_pic' src='<%= image_path %>' /></td>
</script>

Displaying rank

To display each cat’s rank, we’ll once again add an attribute and change the templates. Let’s start by modifying the templates (code):


<script type="text/template" id="angry_cats-template">
<thead>
<tr class='header'>
<th>Rank</th>
<th>Name</th>
<th>Image</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</script>

 


<script type="text/template" id="angry_cat-template">
<td><%= rank %></td>
<td><%= name %></td>
<td><img class='angry_cat_pic' src='<%= image_path %>' /></td>
<td>
<div class='rank_up'><img src='assets/images/up.gif' ></div>
<div class='rank_down'><img src='assets/images/down.gif' ></div>
</td>
</script>

As you can see, we just display the cat’s “rank” attribute, and add some arrows to move the cats up and down within the leader board. But where does the cat’s rank come from? Well, it’s a simple attribute for which we define a default of 0:


AngryCat = Backbone.Model.extend({
  defaults: {
    rank: 0
  }
});

By defining an object associated to the “defaults” key, we can specify default values for a model’s attributes (more on that here). With a default rank set up, we can create our models just as before (i.e. without specifying a rank), and the cat’s rank will be initialized to 0.

Backbone initializers

First order of business to get the ranking to work is to set the proper rank when we create a collection by using an initializer in the collection. We’ve already talked about initializers in the previous post, because we’ve added initializing code to our Marionette app.

Backbone itself has several objects that can have initializers. If you define an initializer function, it will get called right after object instantiation: perfect for setting the rank in the collections models. Here we go (code):


initialize: function(cats){
  var rank = 1;
  _.each(cats, function(cat) {
    cat.set('rank', rank);
    ++rank;
  });
}

On a side note, as we add things within our objects (like we added to the collection above), don’t forget to separate everything with commas. So in our case, as we’re adding this initializer below the model attribute, we need to add a comma after the model value.

Since we’re now setting the model’s rank within the collection’s initializer, we no longer need default values, and we’re back to the following model definition:


AngryCat = Backbone.Model.extend({});

Events

Right now, we’ve got something that looks functional, but doesn’t actually do anything: you can go ahead and show this to your mom, but don’t let her play around with it… So how do we get those up and down buttons to work? As the title implies, we’ll use events and messages.

The concept is quite intuitive. Let’s say you’ve just bought some fast food and sat down to eat. You quickly realize you forgot to buy a drink to wash everything down, but luckily your friend is still ordering. What do you do do? Well, simply ask him to grab you a drink. So 2 things happened: 1) realize you forgot a drink (an event), 2) ask your friend to get a drink for you (a broadcasted message).

So how do we do this in Marionette? Well, Marionette provides us with an event aggregator, which comes from a software pattern. This event aggregator works similarly to the publish/subscribe concept: if one part of the application does something of interest to the other parts, it publishes a message on the event aggregator, and other parts of the application listen for messages that are of interest to them. Easy, no?

So the first step is to define events linked to our buttons and make sure they’re firing properly. We’re going to do this in our AngryCatView, because that’s where the ranking buttons are. This may seem silly, but it’s important: you can only capture events that happen on elements from the same view. Let’s add event listeners to our view (code):


events: {
  'click .rank_up img': 'rankUp',
  'click .rank_down img': 'rankDown'
},

rankUp: function(){
  console.log('rank up');
},

rankDown: function(){
  console.log('rank down');
}

Events are defined within a hash object where the key is “<action> <jQuery selector>”. So the first event will capture clicks on the image within an element with the “rank_up” class. If you look at the template for the view, or at the generated markup, you’ll see that this is no coincidence: that corresponds to the “upvote” image. You’ll note there’s no need to be overly specific with your selector: it will only consider the elements within the view (it will NOT see all elements in the DOM).

The value corresponding the event key is the string representation of the function to call when the event takes place. In our case, we want to call “rankUp”, which currently only displays a message to the browser’s console.

If you run the code now, you’ll see that clicking on the arrows will display messages in the console. So our events are being captured properly!

Using the event aggregator

Now that our events work, let’s use them. Each time we click on a button, we want to swap the cat whose button we clicked with the one that is above or below him. The tricky part is that we’re capturing the event from the ItemView, so we really only has access to the model. The ranking, however, happens within the collection. Luckily our trusty event aggregator comes to our rescue!

To get out of our bind, we’ll simply publish a message saying that an upvote or a downvote has been registered for the model. Then, within the collection, we can listen for that message and act appropriately (code).

In our ItemView, we’ll publish an event instead of printing to the browser console:


rankUp: function(){
  MyApp.trigger('rank:up', this.model);
},

rankDown: function(){
  MyApp.trigger('rank:down', this.model);
}

And within our collection initializer, we’ll listen for those messages:


initialize: function(cats){
  // here, we initialize each model's rank

  //...

  MyApp.on('rank:up', function(cat){
    console.log('rank up');
  });

  MyApp.on('rank:down', function(cat){
    console.log('rank down');
  });
}

I know it looks quite similar to before (it will even act the same in the browser console when you click the buttons). But there’s a major difference: this time around, we’re communication across objects: the ItemView says what happened, and the collection acts appropriately.

Implementing ranking

The first thing we’ll need is some way to sort the collection according to rank. Happily, Backbone provides this for us, all we have to do is implement a comparator. To sort by rank, easy breezy (code):


comparator: function(cat) {
  return cat.get('rank');
}

Now that we have a collection comparator defined, adding and removing models will keep them in the proper order. And when the need arises, we can simply call “cats.sort()” to have our felines ordered by rank.

Now that we have event communication wired up and a means to sort the collection, all that’s left to do is implement the swapping functionality (code):


initialize: function(cats){
  // initialize the model ranks

  // ...
  MyApp.on('rank:up', function(cat){
    if (cat.get('rank') === 1) {
      // can't increase rank of top-ranked cat
      return true;
    }
    self.rankUp(cat);
    self.sort();
    self.trigger('reset');
  });

  MyApp.on('rank:down', function(cat){
    if (cat.get('rank') === self.size()) {
      // can't decrease rank of lowest ranked cat
      return true;
    }
    self.rankDown(cat);
    self.sort();
    self.trigger('reset');
 });
},

comparator: function(cat) {
 return cat.get('rank');
},

var self = this;

rankUp: function(cat) {
 // find the cat we're going to swap ranks with
 var rankToSwap = cat.get('rank') - 1;
 var otherCat = this.at(rankToSwap - 1);

 // swap ranks
 cat.rankUp();
 otherCat.rankDown();
},

rankDown: function(cat) {
 // find the cat we're going to swap ranks with
 var rankToSwap = cat.get('rank') + 1;
 var otherCat = this.at(rankToSwap - 1);

 // swap ranks
 cat.rankDown();
 otherCat.rankUp();
}

We need to tell the collection to trigger the “reset” event when it is sorted, so the collection view knows the order of our cats has changed, and can rerender the child views for us.

As you can tell, we need to define the “rankUp” and “rankDown” functions on our model:


AngryCat = Backbone.Model.extend({
  rankUp: function() {
    this.set({rank: this.get('rank') - 1});
  },

  rankDown: function() {
    this.set({rank: this.get('rank') + 1});
  }
});

Now if you click on the voting arrows, you’ll see cats moving up and down. Yay! Everything works! This is an example of the niceties that Marionette provides: when you sort the collection, Marionette’s collection view notices and rerenders everything for you. With plain Backbone, you’d have to listen to the collection’s “reset” event and rerender the view(s) yourself…

That’s great, but what happens if we need to add a cat after the collection’s been rendered? Well, let’s see (code):


MyApp.start({cats: cats});

cats.add(new AngryCat({ name: 'Cranky Cat', image_path: 'assets/images/cat4.jpg' }));

If we look into the console, we’ll see an error message complaining the cat’s rank isn’t defined. Let’s add a more explicit error in the collection’s initializer to catch this mistake next time (code):


this.on('add', function(cat){

  if( ! cat.get('rank') ) {
    var error =  Error('Cat must have a rank defined before being added to the collection');
    error.name = 'NoRankError';
    throw error;
  }

});

Just like you can listen to messages/events on the event aggregator, you can listen to event on models and collections. In this case, we’re listening to the collection’s “add” event which will be triggered when you call “cats.add(…)”. The second argument is simply an anonymous function that checks for the rank.

Now let’s add the cat with the rank initialized properly (code):


MyApp.start({cats: cats});

cats.add(new AngryCat({

  name: 'Cranky Cat',

  image_path: 'assets/images/cat4.jpg',

  rank: cats.size() + 1

}));

Not only won’t you see an error, you’ll notice that the cat’s ItemView was automagically rendered and appended to the CompositeView. Isn’t life sweet?

Counting votes

We’re not done yet: let’s count and display the votes each cat receives. For that, we’ll need a vote attribute that gets incremented each time a voting button is clicked (code):

We need to implement a vote incrementer in our model:


AngryCat = Backbone.Model.extend({
  defaults: {
    votes: 0
  },

  addVote: function(){
    this.set('votes', this.get('votes') + 1);
  }

  // define rankUp etc.

});

We also need to add a vote to our model each time a vote button is clicked in the ItemView:


rankUp: function(){
  this.model.addVote();
  MyApp.trigger('rank:up', this.model);
},

rankDown: function(){
  this.model.addVote();
  MyApp.trigger('rank:down', this.model);
}

And of course, we need to update our template to show the vote count:


<script type="text/template" id="angry_cats-template">
<thead>
<tr class='header'>
<th>Rank</th>
<th>Votes</th>
<!-- the rest is the same as before -->
</tr>
</thead>
<tbody>
</tbody>
</script>

<script type="text/template" id="angry_cat-template">
<td><%= rank %></td>
<td><%= votes %></td>
<!-- the rest is the same as before -->
</script>

Ok, let’s run this and try the following example: click “up” on Wet Cat, then “down”. If you pay attention, you’ll notice that when upvoting the vote count doesn’t refresh, but when you down vote, you see the correct vote count (namely 2). What’s up with that?

It’s due to rendering (or more accurately, rerendering). When the information on your model changes, you need to have you view rendered again to display the changes. When you down voted, the collection order changed and the CompositeView was rerendered by Marionette as I explained above. However, when you upvotes the topmost cat, the vote was counted but no view was rerendered (due to the “if” condition we have in the collection’s event aggregator listeners).

What’s the fix? Once again, it’s quite easy when we know what’s going on: we need to rerender our ItemView each time the model’s “votes” attribute changes. I’m sure you guessed how we’ll add this event listener: using an initializer in the ItemView (code):


initialize: function(){
  this.bindTo(this.model, 'change:votes', this.render);
}

UPDATE: Backbone’s API has changed and the “bindTo” method has been replaced by “listenTo”. Your code should now look like this commit.

What we’re doing is binding to the model’s “change:votes” event, and calling the ItemView’s render method. We also could have bound ourselves to the whole “change” event, but we’ll only need to rerender the view when the votes change. Binding only to the events you need will help you avoid double rendering issues, etc. which reduce your application’s performance.

Another nice amenity Marionette provides is that this event we’re binding to will be unbound when the view is closed. In plain vanilla Backbone, you’d have to undo the bindings yourself before disposing of the view, or you’d risk memory leaks.

For example, another way to get the ItemView to refresh would be to bind directly to the model within the initializer, like so:


initialize: function(){

  this.model.bind('change:votes', this.render, this);

}

However, if you do this, you’ll need to unbind the event yourself when the view gets closed, or you’ll run into memory leaks (as explained here):

onClose: function(){
  this.model.unbind('change:votes', this.render);
}

This onClose method is called automatically by Marionette when the ItemView is closed.

The events that Marionette unbinds automatically when the ItemView is closed are documented in the “ItemView close” section here. As you can see, since the ItemView can unbind “bindTo” events on its own, we don’t need to specifically unbind it. But when we’re binding directly to the model, we need to unbind our event within the ItemView’s onClose function, or we’ll have memory leaks.

Destroying models

Let’s say we need to be able to remove cats from the rankings by clicking on a “disqualify” link. How would we go about that? Once again, Marionette makes things easy for us (code).

We’ll simply change our templates to display a table cell with the disqualifying link:


<script type="text/template" id="angry_cats-template">
<thead>
<tr class='header'>
<th>Rank</th>
<th>Votes</th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</script>

 


<script type="text/template" id="angry_cat-template">
<td><%= votes %></td>
<td><%= name %></td>
<td><img class='angry_cat_pic' src='<%= image_path %>' /></td>
<td>
<div class='rank_up'><img src='assets/images/up.gif' /></div>
<div class='rank_down'><img src='assets/images/down.gif' /></div>
</td>
<td>a href="#">Disqualify</a></td>
</script>

And then, within our ItemView, we bind to a new event so that when the disqualifying link is clicked, we delete the model:


AngryCatView = Backbone.Marionette.ItemView.extend({

  // here we have various properties defined

  events: {
    'click .rank_up img': 'rankUp',
    'click .rank_down img': 'rankDown',
    'click a.disqualify': 'disqualify'
  },

  // here we have the initializer and other functions defined
  disqualify: function(){
    this.model.destroy();
  }
});

If you fire up your trusty browser, and click on the 2nd cat, you’ll see it’s properly removed. However, now the ranking go form 1 to 3. Not a very effective ranking system…

So each time we destroy a cat, we need to adjust the other cat’s rankings. And we’ll accomplish that by broadcasting an event (along with the deleted cat) before actually destroying the model (code):


disqualify: function(){
  MyApp.trigger('cat:disqualify', this.model);
  this.model.destroy();
}

And we’ll listen for that event within the collection, by adding this code to our collection initializer:


MyApp.on('cat:disqualify', function(cat){
  var disqualifiedRank = cat.get('rank');
  var catsToUprank = self.filter(
    function(cat){ return cat.get('rank') > disqualifiedRank; }
  );
  catsToUprank.forEach(function(cat){
    cat.rankUp();
  });
});

When a cat is removed, we need to go through each cat ranked below it and reduce that cat’s rank by 1. You’ll notice that to achieve this, we use various handy methods defined on collection by the Underscore library.

And now,when we run the example and disqualify the second cat, the cats below it still have the wrong rank. But if we downvote one, everything fixes itself. What going on?

The same thing as before is taking place: even though the ranks are being updated properly, the cat views aren’t being rerendered. But as soon as we down vote a cat, the CompositeView is being rerendered (because the collection gets changed when we sort it).

So how do we get the CompositeView to rerender when we disqualify a cat? Several options are available to us.

  • We could have the ItemView rerender itself on any model change (instead of just a change in votes). This isn’t the best way, since the view would get rendered twice if a model change triggered a collection change (e.g. when two cats swap ranks);
  • We could resort the collection, but that wouldn’t be efficient either: our collection is still sorted by rank;
  • We could force a rerendering of the CompositeView’s collection, but let’s avoid that and leverage Marionette’s capabilities instead;
  • We could simply trigger a ‘reset’ event on the collection, which let our app know that the collection changed. The CompositeView will rerender itself automatically, as it listens for the reset event, among others. Incidentally, sorting a collection triggers the reset event, which is why our view is refreshed when we upvote or downvote a cat.

To finally get our cat disqualification system running properly, we’ll simply trigger the collection’s “reset”event once we’re done updating the cat ranks (code):


MyApp.on('cat:disqualify', function(cat){
  var disqualifiedRank = cat.get('rank');
  var catsToUprank = self.filter(
    function(cat){ return cat.get('rank') > disqualifiedRank; }
  );
  catsToUprank.forEach(function(cat){
    cat.rankUp();
  });
  self.trigger('reset');
});

So there you have it, our first Backbone.Marionette app! Hold tight, there are more to come…

This entry was posted in Backbone.js, Backbone.Marionette. Bookmark the permalink.

6 Responses to A simple Backbone.Marionette tutorial (part 2)

  1. Pingback: Quora

  2. J.I. Smith says:

    Thanks for posting, this was really useful, and pretty interesting to work through too!

  3. Great job with these simple Backbone Marionette tutorials! Thanks for going through the effort, well documented.

  4. GO says:

    Pretty good intro, I like it. :)

    There is a little typo in the article, you should add class attrib here:
    <a href=”#” rel=”nofollow”>Disqualify</a>

  5. david says:

    Thanks for catching the missing “nofollow” ! I was trying to keep the tutorial as simple as possible, and omitted that attribute…

  6. olivier says:

    Thank you, that was very helpful. You should keep blogging!

Comments are closed.