Backbone, 3 Ways

@pamelafox

Why use a framework?


Our website:
242 JS files.
28848 lines of code.
5-person frontend team.



Things can get messy fast.

Why Backbone?

  • Structure
  • Modularity
  • Persistence Layer
  • Common Services
  • Best Practices

Backbone

Lightweight Model View Controller framework

Models

var Book = Backbone.Model.extend(
  defaults: {
    title: 'Untitled',
    author: 'Unknown',
    owned: false
  },
  url: "/api/books",
  displayString: function() {
    return this.get('title') + ' By ' + this.get('author');
  },
  markAsOwned: function() {
    this.set('owned', true);
  }
});
var book = new Book({title: 'I, Robot',
                     author: 'Isaac Asimov'});

Collections

var BookCollection = Backbone.Collection.extend({
    model: Book,
    url: "/api/books"
});
var book1 = new Book({title: 'I, Robot',
                      author: 'Isaac Asimov'});
var book2 = new Book({title: 'Caves of Steel',
                      author: 'Isaac Asimov'});
var robotSeries = new BookCollection([book1, book2]);

The Backbone Backend

var Book = Backbone.Model.extend(
  url: "/api/books",
  // ...
});
var book = new Book({id: 12});
book.fetch();
HTTP GET /api/books
{id: 12,
 title: 'Robot Dreams',
 author: 'Isaac Asimov',
 owned: false
}
book.markAsOwned();
book.save();
HTTP PUT /api/books/12
{id: 12,
 title: 'Robot Dreams',
 author: 'Isaac Asimov',
 owned: true
}

The Backbone Backend


var BookCollection = Backbone.Collection.extend({
  url: "/api/books",
  // ...
});
var myBooks = new BookCollection();
myBooks.fetch();
HTTP GET /api/books
[{id: 12,
 title: 'The Naked Sun',
 author: 'Isaac Asimov'
}, ...
]
myBooks.create(new Book({title: 'Foundation',
                         author: 'Isaac Asimov'}));
HTTP POST /api/books
{title: 'Foundation', author: 'Isaac Asimov'}

Views

var BookItemView = Backbone.View.extend({
  tagName:"li",
  template:_.template($('#tpl-book-list-item').html()),
  render:function (eventName) {
    $(this.el).html(this.template(this.model.toJSON()));
    return this;
  }
});

var BookListView = Backbone.View.extend({
  tagName:'ul',
  initialize:function () {
    this.collection.bind("reset", this.render, this);
    this.collection.fetch();
  },
  render:function (eventName) {
    _.each(this.collection.models, function (book) {
      var bookItemView = new BookItemView({model:book});
      $(this.el).append(bookItemView.render().el);
    }, this);
    return this;
  }
});

Router

var AppRouter = Backbone.Router.extend({
  routes:{
    "/list":"list"
  },
  list:function () {
    this.books = new BookCollection();
    this.bookListView = new BookListView({collection:this.books});
    $('#sidebar').html(this.bookListView.render().el);
  },
});
 
var app = new AppRouter();
Backbone.history.start();

Backbone & jQuery


Standalone jQuery:

$('body').append('<br>');
$('.coursera-listing').html('Best Course Ever');

Integrated with Backbone:

var view = BackboneView.extend({
  
  render: function() {
    this.$el.append('<br>');
    this.$el.find('.coursera-listing').html('Best Course Ever');
    // or shorter:
    this.$('.coursera-listing').html('Best Course Ever');
  }
})

Backbone & Underscore


Standalone Underscore:

// On collections:
_.each(coll, iterator); _.find(coll, iterator); // etc
// On arrays
_.flatten(arr), _.union(*arrs), _.uniq(arr), // etc
// On functions
_.bind(func, obj), _.throttle(func, wait) // etc

All collection functions work on BackboneCollections but return arrays.

BackboneCollection.each(iterator);
BackboneCollection.find(iterator);
// etc

Backbone & RequireJS

<script src="path/to/require.js" />
<script>
require.config(
  baseUrl: "path/to/assets",
  callback: {
    require(["js/routes/main"]); 
  }
});
</script>
define(
  ["js/lib/jquery", 
   "js/lib/underscore", 
   "js/lib/backbone",], 
function($, _, Backbone) {
  var view = Backbone.View.extend({
    render: function() {
      _.each(['a', 'b', 'c'], function(letter) {
        var $button = $('button').html(letter);
      }
    }
  });
  return view;
});

Backbone @ Coursera

Backbone Customizations

  • Custom router
    Coursera.router.navigate('/account/profile');
  • Custom region manager
    Coursera.region.open({
      'pages/home/template/page',
      regions: {
        body: 'pages/home/account/profile'
      }
    })
  • Custom API layer
    Coursera.api.post('course/enroll', {data: {'course-id': 2}})
      .done(function(data) {
        alert('Success!');
      })
      .fail(function(xhr) {
        alert(xhr.responseText);
      });

3 Frontends, 3 Ways



home

Models


Backbone.Model
 ↳ Topic
 ↳ University
 ↳ Course
 ↳ User

Topic


define(["jquery",
        "backbone",
        "underscore",
        "js/core/coursera",
        "js/lib/backbone.relational"],
function($, Backbone, _, Coursera) {
  var topic = Backbone.extend({
    defaults: {},

    sync: function(callback) {
      var that = this;

      if(this.get("description") || this.get("about_the_course")) {
        callback();
      } else if(this.get("short_name")) {
        Coursera.api.get('topic/information', {
           data: {"topic-id":that.get('short_name')},
           message: {"waiting": "loading course page ..."}
        })
        .done(function(data)
        {
           that.set(data);
           callback();
        })
        .fail(function(jqXHR)
        {
           callback(jqXHR.status);
        });
      }
    }
  });
  return topic;
});

Collections


Backbone.Collections
 ↳ Topics
 ↳ Courses
 ↳ Universities

Topics


define(
   ["backbone", 
    "underscore",
    "js/models/topic", 
    "js/core/coursera", 
    "jquery"], 
function(Backbone, _, Topic, Coursera, $) {
  var Topics = Backbone.Collection.extend({
    model: Topic,

    comparator: function(topic) {
      return topic.get('name');
    },

    retrieve: function(filter, callback) {
      var self = this;

      // hack, until we improve our apis and models
      if(self.length) {
        callback(null, self);
      } else {
        Coursera.api.get("topic/list", {
          data:filter || {},
          message: {waiting: "loading classes ..."}
          })
          .done(function(data)
          {
             self.add(data);
             callback(null, self);
          })
          .fail(function()
          {
             callback(true, self);
          });
      }
    },
  }
});

Views

All views extend body or Backbone.View


pages/home/
  about/
    ...
  account/
    ...
  catalog/
    catalogBody.js
    catalogBody.js.jade
    catalogListing.js.jade
    index.styl
  course/
    ...
  front/
    ...
  template/
    ...
  universities/
    ...
  university/

View: catalogBody

The Router

var routes = {};

routes[home + "account/records"] = function() {
  if (Coursera.user.get('authenticated')) {
    Coursera.user.getTopics(function(topics) {
        Coursera.region.open({
          "pages/home/template/page": {
            regions: {
              body: {
                "pages/home/account/courseRecords": {
                  id: "account-records"
                }
              }
            }
        });
      });
    });
  } else {
    this.navigate(config.dir.home, true);
  }
};

// ...

Backbone.history.start({pushState: true});

Site Admin

Models


Backbone.Model
 ↳ AdminModel
     ↳ TopicAdminModel
     ↳ CourseAdminModel
     ↳ UniversityAdminModel
     ↳ StaffAdminModel
        ↳ TeachingAssistantAdminModel
        ↳ InstructorAdminModel
        ↳ UniversityAdminAdminModel

AdminModel

Extends Backbone.Model and adds:


label
webUrl()
displayName()
fieldsets()
inline()
buttons()
instructions()
filter()
columns()

StaffAdminModel


 define(["backbone",
        "js/core/coursera",
        "pages/site-admin/models/AdminModel"
        ],
function(Backbone, Coursera, AdminModel) {

  var model = AdminModel.extend({

    displayName: function() {
      return this.get('user__email');
    },
    
    fieldsets: function() {
      return [
        {
          name: 'user',
          type: 'select2search',
          readonly: !this.isNew(),
          extras: {
            multiple: false,
            search: {
              url: Coursera.config.url.maestro + 'admin/search?',
              param: 'email'
            }
          }
        },
        {
          name: 'official_title',
          type: 'text'
        },
        {
          name: 'universities',
          type: 'select2',
          extras: {
            multiple: true,
            dataUrl: Coursera.config.url.maestro  + 'admin/universities'
          }
        }
        ];
   }
  })
  return model;
});

Model Views


Backbone.View
 ↳ body
    ↳ ModelAdminPageView
 ↳ ModelAdminFieldsView
 ↳ FieldView
    ↳ CheckboxView
    ↳ DatePickerView
    ↳ DropdownView
    ↳ HiddenInputView
    ↳ NumberRangeView
    ↳ Select2SearchView
    ↳ TextAreaView
    ↳ TransloaditUploadView
    ↳ WysiHTMLEditorView
    ↳ ...

ModelAdminPageView

Model Saving

POST /instructor {...} → {...}
GET /instructor/2  → {...}
PUT /instructor/2 {...} → {...}

Collections


Backbone.Collection
 ↳ AdminCollection
     ↳ TopicsAdminCollection
     ↳ CoursesAdminCollection
     ↳ UniversitiesAdminCollection
     ↳ InstructorsAdminCollection
     ↳ TeachingAssistantsAdminCollection
     ↳ UniversitiesAdminsAdminCollection

AdminCollection

Extends Backbone.Collection and adds:


label
webUrl()
comparator()
showNewButton

CoursesAdminCollection


define(["backbone",
        "js/core/coursera",
        "pages/site-admin/collections/AdminCollection",
        "pages/site-admin/models/CourseAdminModel"
        ],
function(Backbone, Coursera, AdminCollection, CourseAdminModel) {

  var collection = AdminCollection.extend({

    url: 'admin/courses',

    webUrlLabel: 'sessions',

    label: 'Sessions',
    
    model: CourseAdminModel,

    showNewButton: false,

    comparator: function(model) {
      return model.get('topic__name');
    }
    
  });

  return collection;
});

Collection Views


Backbone.View
 ↳ body
    ↳ AdminDashboardView
    ↳ CollectionAdminPageView
 ↳ CollectionAdminListView

AdminDashboardView

The Router

Determines what models/collections are valid URLs based on groups in cookie.

var routes = {};

routes[""] = function() {
  var region = {};
  region[pageTemplate] = {
    regions: {
      body: {
        "pages/site-admin/views/AdminDashboardView": {
          id: "AdminDashboard",
          initialize: {
            collections: getAllowedCollections()
          }
        }
      }
    }
  }
  Coursera.region.open(region);
};

Backbone.history.start({pushState: true, root: "/admin/"});

Course Admin

Relational Models

Powered by BackboneRelational


var item = Backbone.Model.extend({
  api: Coursera.api,
  partialUpdate: true,
  url: "quizzes"
});

var section = Backbone.RelationalModel.extend({
  relations: [{
    type: Backbone.HasMany,
    key: "items",
    relatedModel: Item,
    collectionType: Items,

    reverseRelation: {
      'key': '__section',
      'includeInJSON': false
    }

  }],

  partialUpdate: true,

  // ...
});

So you want to try Backbone?



Start with a tutorial like this one, not the docs.



Port a simple app or make one from scratch, the "Backbone" way.

Keep your options open



Figure out what's important to you:
modularity? data binding? testability? persistance?



Review your options:
Ember? Spine? AngularJS? Enyo? and many more...



Or when all else fails...
write your own.

There's no "right answer"

...but some are better than others.