Backbone.js

An unopinionated frontend framework





Presented By
Rui Jiang
Creator of GistBox

Why Frontend MVC?


You're writing a large single-page app

You have insanely-nested functions
for making server requests

You have code for rendering UI mixed
into functions for transforming data

You can't figure out how to write tests

The 3 Letters


Model
The Data and Business Logic

View
The User Interface

Controller
Coordinates Views and Models

Backbone isn't really MVC


Models + Collections

Views

Router

Events Dispatcher



Backbone doesn't care



Models + Collections

Views

Router

Events Dispatcher

Backbone doesn't care



Models + Collections

Views

Router

Events Dispatcher

The (Small) Project

The Requirements


Rails 4 project with one model: Country

RESTful interface to list and update Countries

Browser keeps track of # votes left for user

No "push" mechanism

Backbone Folders


                                             /app
                                               /collections
                                               /models
                                               /views
                                               router.js
                                               app.js

Let's Agree on the Model

Agreeing on a schema allows Rails + Backbone 
work to progress in parallel

                                       Country
                                       - name [string]
                                       - code [string]
                                       - num_votes [integer]

CountryModel


Ranker.CountryModel = Backbone.Model.extend({
    defaults: {
        name: "",
        code: "",
        num_votes: 0
    }
});

CountriesCollection


Ranker.CountriesCollection = Backbone.Collection.extend({
    model: Ranker.CountryModel,
    url: "/countries",

    // sort by votes on a country, most first
    comparator: function(country) {
        return -country.get("num_votes");
    }
});

Mapping Requests


CountriesCollection.fetch() -> GET /countries

CountryModel.save() -> PUT /countries/:id

The Router


Ranker.Router = Backbone.Router.extend({
    currentView: null,

    routes: {
        "": "home"
    },

    home: function() {
        var countries = new Ranker.CountriesCollection();
        countries.fetch();

        this.currentView = new Ranker.AppView({
            collection: countries
        });

        $("body").html(this.currentView.el);
    }
});

The App View

The App View - Template


<header>
  <span class="logo-large">World Cup</span>
  <span class="logo-small">Ranker</span>
</header>

<div class="votes-left">
  {{ if (can_vote) { }}
    You have <span class="votes-left-highlight">{{= votes_left}}</span> votes left
  {{ } else { }}
    You have no votes left
  {{ } }}
</div>

<ul class="country-rankings"></ul>

The App View - View


Ranker.AppView = Backbone.View.extend({
    className: "app-container",

    views: {
        countries: []
    },

    template: function() {
        return _.template(
            $("#app-template").html(),
            {
                votes_left: this.votesLeft(),
                can_vote: this.canVote()
            }
        );
    },

    initialize: function() {
        this.views = {
            countries: []
        };

        this.render();

        // when collection is fetched, re-render
        this.listenTo(this.collection, "sync", this.render);

        // when votes are cast on a country, re-render
        this.listenTo(this.collection, "change", this.sortAndRender);
    },

    sortAndRender: function() {
        this.decrementVote();

        // rankings have to be sorted explicitly after votes change
        this.collection.sort();
        this.render();
    },

    render: function() {
        var _this = this;

        // container
        _this.$el.html(
            _this.template()
        );

        // country rankings
        var $countryRankings = _this.$el.find(".country-rankings");
        $countryRankings.empty();

        _this.collection.each(function(country, i) {
            var countryView = new Ranker.CountryView({
                model: country,
                rank: i + 1,
                canVote: _this.canVote()
            });

            $countryRankings.append(countryView.el);
        });
    },

    votesLeft: function() {
        var tryValue = localStorage["votes_left"];

        if (!tryValue) {
            localStorage["votes_left"] = 5;
            return 5;
        } else {
            return parseInt(tryValue);
        }
    },

    decrementVote: function() {
        localStorage["votes_left"] = this.votesLeft() - 1;
    },

    canVote: function() {
        return (this.votesLeft() > 0);
    }
});



The Country View



The Country View - Template


<li>
    <div class="country-rank">{{= rank}}</div>
    <div class="country-flag country-flag-{{= country.code}}"></div>
    <div class="country-name">{{= country.name }}</div>
    <div class="vote-up {{if (!can_vote) { }}vote-up-disabled{{ } }}">
         {{= country.num_votes }}
    </div>
</li>

The Country View - View


Ranker.CountryView = Backbone.View.extend({
    tagName: "li",
    rank: null,
    canVote: null,

    template: function() {
        return _.template(
            $("#country-template").html(),
            {
                country: this.model.toJSON(),
                rank: this.rank,
                can_vote: this.canVote
            }
        );
    },

    events: {
        "click .vote-up": "clickVoteUp"
    },

    initialize: function(options) {
        this.rank = options.rank;
        this.canVote = options.canVote;

        this.render();
    },

    render: function() {
        this.$el.html(
            this.template()
        );
    },

    clickVoteUp: function(evt) {
        var $button = $(evt.target);

        if (!$button.hasClass("vote-up-disabled")) {
            // bump up votes
            var numVotes = this.model.get("num_votes");

            // only trigger change event once: on save
            this.model.set("num_votes", numVotes + 1, { silent: true });
            this.model.save();
        }
    }
});

What's Going On?

App.js - Kick Everything Off


$(function() {
    app.router = new Ranker.Router();

    // kicks off the router's default route
    Backbone.history.start();
});

Pro Tip: View Mixins


 _.extend(Backbone.View.prototype, {
    // function can then be called with: viewInstance.escapeHtml()
    escapeHtml: function(html) {
        return $('<div></div>').text(html).html();
    }
});

Backbone - What is it good for?


When you need to clean up a mess

When you need to pick and choose components

Using with jQuery (though you don't have to)


Backbone - What's Not Great


Lots of rope

You need to be extra careful with cleanup

No "deep" models

No nested views



Resources


Testing Backbone with Jasmine

Nested models and collections

More "conventional" Backbone




Questions?



Ask now 


 
Email me later
rui@gistboxapp.com

Links to Stuff


These slides

World Cup Ranker Code

Check out GistBox

The beautiful code snippet organizer

Backbone.js

By Rui Jiang

Backbone.js

Intro to Backbone.js, the popular Javascript framework. Created for the Boston Frontend Developers meetup.

  • 1,693