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
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