Simple Backbone.Marionette tutorial

The result of blog posts http://davidsulc.com/blog/2012/04/15/a-simple-backbone-marionette-tutorial/ http://davidsulc.com/blog/2012/04/22/a-simple-backbone-marionette-tutorial-part-2/

by Francisco Carvalho

HTML

<script src="http://documentcloud.github.com/underscore/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js"></script>
<script src="https://raw.github.com/derickbailey/backbone.marionette/master/lib/backbone.marionette.js"></script>
    <div id="content">
    </div>

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

    <script type="text/template" id="angry_cat-template">
      <td><%= rank %></td>
      <td><%= votes %></td>
      <td><%= name %></td>
      <td><img class='angry_cat_pic' src='<%= image_path %>' /></td>
      <td>
    <div class='rank_up'><img id="rank_up" src='http://placehold.it/12x24&text=^' /></div>
    <div class='rank_down'><img src='http://placehold.it/12x24&text=v' /></div>
      </td>
      <td><a href="#" class="disqualify">Disqualify</a></td>
    </script>

CSS

/*!
 * Bootstrap v2.0.2
 *
 * Copyright 2012 Twitter, Inc
 * Licensed under the Apache License v2.0
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Designed and built with all the love in the world @twitter by @mdo and @fat.
 */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
nav,
section {
  display: block;
}
audio,
canvas,
video {
  display: inline-block;
  *display: inline;
  *zoom: 1;
}
audio:not([controls]) {
  display: none;
}
html {
  font-size: 100%;
  -webkit-text-size-adjust: 100%;
  -ms-text-size-adjust: 100%;
}
a:focus {
  outline: thin dotted #333;
  outline: 5px auto -webkit-focus-ring-color;
  outline-offset: -2px;
}
a:hover,
a:active {
  outline: 0;
}
sub,
sup {
  position: relative;
  font-size: 75%;
  line-height: 0;
  vertical-align: baseline;
}
sup {
  top: -0.5em;
}
sub {
  bottom: -0.25em;
}
img {
  height: auto;
  border: 0;
  -ms-interpolation-mode: bicubic;
  vertical-align: middle;
}
button,
input,
select,
textarea {
  margin: 0;
  font-size: 100%;
  vertical-align: middle;
}
button,
input {
  *overflow: visible;
  line-height: normal;
}
button::-moz-focus-inner,
input::-moz-focus-inner {
  padding: 0;
  border: 0;
}
button,
input[type="button"],
input[type="reset"],
input[type="submit"] {
  cursor: pointer;
  -webkit-appearance: button;
}
input[type="search"] {
  -webkit-appearance: textfield;
  -webkit-box-sizing: content-box;
  -moz-box-sizing: content-box;
  box-sizing: content-box;
}
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none;
}
textarea {
  overflow: auto;
  vertical-align: top;
}
.clearfix {
  *zoom: 1;
}
.clearfix:before,
.clearfix:after {
  display: table;
  content: "";
}
.clearfix:after {
  clear: both;
}
.hide-text {
  overflow: hidden;
  text-indent: 100%;
  white-space: nowrap;
}
.input-block-level {
  display: block;
  width: 100%;
  min-height: 28px;
  /* Make inputs at least the height of their button...

JavaScript

MyApp = new Backbone.Marionette.Application();

MyApp.addRegions({
  mainRegion: "#content"
});

AngryCat = Backbone.Model.extend({
  defaults: {
    votes: 0
  },
  
  addVote: function(){
    this.set('votes', this.get('votes') + 1);
  },
  
  rankUp: function() {
    this.set({rank: this.get('rank') - 1});
  },
  
  rankDown: function() {
    this.set({rank: this.get('rank') + 1});
  }
});

AngryCats = Backbone.Collection.extend({
  model: AngryCat,
  
  initialize: function(cats){
    var rank = 1;
    _.each(cats, function(cat) {
      cat.set('rank', rank);
      ++rank;
    });
    
    this.on('add', function(cat){ if( ! cat.get('rank') ) console.error("Cat must have a rank defined before being added to the collection"); });
    
    var self = this;

    MyApp.vent.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();
    });

    MyApp.vent.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();
    });
    
    MyApp.vent.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');
    });
  },

  comparator: function(cat) {
    return cat.get('rank');
  },
  
  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...