Skip to content

Instantly share code, notes, and snippets.

@digitaldreamer
Last active December 15, 2015 00:29
Show Gist options
  • Save digitaldreamer/5173697 to your computer and use it in GitHub Desktop.
Save digitaldreamer/5173697 to your computer and use it in GitHub Desktop.
// Backbone Refactor Tutorial breakdown from:
// https://github.com/kjbekkelund/writings/blob/master/published/understanding-backbone.md
///////////////////////////////////////////////////////////////////////////
//Step by step from jQuery to Backbone
//
// I've seen many struggle when they first meet Backbone.js.
// In this blog post I will gradually refactor a bit of code from how I
// used to write JavaScript before, into proper Backbone.js code using models,
// collections, views and events. Hopefully this process will give you a
// firm understanding of the core abstractions in Backbone.js.
//
// Let's start with the code we're going to work with throughout this blog post:
$(document).ready(function() {
$('#new-status form').submit(function(e) {
e.preventDefault();
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: { text: $('#new-status textarea').val() },
success: function(data) {
$('#statuses ul').append('<li>' + data.text + '</li>');
$('#new-status').find('textarea').val('');
}
});
});
});
///////////////////////////////////////////////////////////////////////////
// And here's the application up and running: Monologue.
// This simple application is based on a great JavaScript presentation by Brandon Keepers.
// However, while his focus is primarily on testing and design patterns
// — of which he does an amazing job — mine is on explaining the core Backbone.js
// abstractions one step at a time.
//
// http://monologue-js.herokuapp.com/
// http://opensoul.org/blog/archives/2012/05/16/the-plight-of-pinocchio/
//
// Basically, the app lets you to write some text, click on "Post",
// and see what you've written show up in the list while the textarea is prepared for new input.
// Translating this to code, we start by waiting for the DOM to load,
// set a submit listener, and when the form is submitted we send the input to the server,
// append the response to the list of statuses and reset the input.
//
// But, what's the problem? This code does a lot of stuff at the same time.
// It listens for page events, user events, network events, it does network IO,
// handles user input, parses the response, and does HTML templating.
// All in 16 lines of code. It looks like most JavaScript code I wrote a year ago.
// Throughout this blog post I will gradually work my way to code that follows the
// single responsibility principle, and which is far easier to test, maintain, reuse and extend.
//
// There are three things we want to achieve:
//
// 1) We want to move as much as possible out of $(document).ready, so we can trigger
// the code ourselves — we only want to bootstrap the application when the DOM is ready.
// In its current state the code is nearly impossible to test.
//
// 2) We want to adhere to the single responsibility principle, and make the code
// more reusable and easier to test.
//
// 3) We want to break the coupling between the DOM and Ajax.
//
// Separating DOM and Ajax
//
// We start with splitting Ajax and DOM from each other, and the first step is
// to create an addStatus function:
function addStatus(options){
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: { text: $('#new-status textarea').val() },
success: function(data) {
$('#statuses ul').append('<li>' + data.text + '</li>');
$('#new-status').find('textarea').val('');
}
});
}
$(document).ready(function() {
$('#new-status form').submit(function(e) {
e.preventDefault();
addStatus();
});
});
///////////////////////////////////////////////////////////////////////////
// However, both within data and success we work with the DOM. We can break
// this coupling by sending these as arguments to addStatus:
function addStatus(options){
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: {text: options.text},
success: options.success
});
}
$(document).ready(function() {
$('#new-status form').submit(function(e) {
e.preventDefault();
addStatus({
text: $('#new-status textarea').val(),
success: function(data) {
$('#statuses ul').append('<li>' + data.text + '</li>');
$('#new-status').find('textarea').val('');
}
});
});
});
///////////////////////////////////////////////////////////////////////////
// However, I want to wrap these statuses in an object and be able to write
// statuses.add instead of addStatus. To achieve this we can use the
// constructor pattern with prototypes to introduce a Statuses "class":
//
// http://addyosmani.com/resources/essentialjsdesignpatterns/book/#constructorpatternjavascript
var Statuses = function() {};
Statuses.prototype.add = function(options) {
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: {text: options.text},
success: options.success
});
};
$(document).ready(function() {
var statuses = new Statuses();
$('#new-status form').submit(function(e) {
e.preventDefault();
statuses.add({
text: $('#new-status textarea').val(),
success: function(data) {
$('#statuses').append('<li>' + data.text + '</li>');
$('#new-status').find('textarea').val('');
}
});
});
});
///////////////////////////////////////////////////////////////////////////
// Creating a view
//
// Our submit handler now has one dependency, the statuses variable,
// and everything else within it is focused on the DOM. Let's move the
// submit handler and everything inside it into its own class, NewStatusView:
var Statuses = function() {};
Statuses.prototype.add = function(options) {
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: {text: options.text},
success: options.success
});
};
var NewStatusView = function(options) {
var statuses = options.statuses;
$('#new-status form').submit(function(e) {
e.preventDefault();
statuses.add({
text: $('#new-status textarea').val(),
success: function(data) {
$('#statuses ul').append('<li>' + data.text + '</li>');
$('#new-status').find('textarea').val('');
}
});
});
}
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({statuses: statuses});
});
///////////////////////////////////////////////////////////////////////////
// Now we only bootstrap our application when the DOM is loaded,
// and everything else is moved out of $(document).ready.
// The steps we have taken so far has given us two components which are
// easier to test and have more well-defined responsibilities.
// However, there is still much to clean up. Let's start by splitting
// the submit handler in NewStatusView into its own addStatus method:
var Statuses = function() {};
Statuses.prototype.add = function(options) {
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: {text: options.text},
success: options.success
});
};
var NewStatusView = function(options) {
this.statuses = options.statuses;
$('#new-status form').submit(this.addStatus);
}
NewStatusView.prototype.addStatus = function(e) {
e.preventDefault();
this.statuses.add({
text: $('#new-status textarea').val(),
success: function(data) {
$('#statuses ul').append('<li>' + data.text + '</li>');
$('#new-status').find('textarea').val('');
}
});
});
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({statuses: statuses});
});
///////////////////////////////////////////////////////////////////////////
// Running this in Chrome we get this error:
//
// Uncaught TypeError: Cannot call method 'add' of undefined
//
// We get this error as this means different things in the constructor and
// the addStatus method, as it is jQuery that actually calls the latter
// when the user submits the form.
// (If you don't fully grasp how this works, I recommend reading Understanding
// JavaScript Function Invocation and “this”.) To solve this problem we can use
// $.proxy, which creates a function where this is always the same —
// the context you specify as the second argument.
//
// http://yehudakatz.com/2011/08/11/understanding-javascript-function-invocation-and-this/
var Statuses = function() {};
Statuses.prototype.add = function(options) {
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: {text: options.text},
success: options.success
});
};
var NewStatusView = function(options) {
this.statuses = options.statuses;
var add = $.proxy(this.addStatus, this);
$('#new-status form').submit(add);
}
NewStatusView.prototype.addStatus = function(e) {
e.preventDefault();
this.statuses.add({
text: $('#new-status textarea').val(),
success: function(data) {
$('#statuses ul').append('<li>' + data.text + '</li>');
$('#new-status').find('textarea').val('');
}
});
});
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({statuses: statuses});
});
///////////////////////////////////////////////////////////////////////////
// Let's make the success callback call methods instead of working directly
// on the DOM, which makes the callback easier to read and work with:
var Statuses = function() {};
Statuses.prototype.add = function(options) {
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: {text: options.text},
success: options.success
});
};
var NewStatusView = function(options) {
this.statuses = options.statuses;
var add = $.proxy(this.addStatus, this);
$('#new-status form').submit(add);
}
NewStatusView.prototype.addStatus = function(e) {
e.preventDefault();
var that = this;
this.statuses.add({
text: $('#new-status textarea').val(),
success: function(data) {
that.appendStatus(data.text);
that.clearInput();
}
});
});
NewStatusView.prototype.appendStatus = function(text) {
$('#statuses').append('<li>' + text + '</li>');
}
NewStatusView.prototype.clearInput = function() {
$('#new-status').find('textarea').val('');
}
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({statuses: statuses});
});
///////////////////////////////////////////////////////////////////////////
// For the next step we need to introduce our first bit of Backbone:
// events. Events are basically just a way to say:
// "Hi, I want to know when some action occurs" and "Hi, you know what?
// The action you're waiting for just occurred!" We are used to this idea
// from jQuery DOM events such as listening for click and submit, e.g.
//
// $('form').bind('submit', function() {
// alert('submit form');
// });
//
// The Backbone.js documentation describes Backbone.Events as follows:
// "Events is a module that can be mixed in to any object, giving the
// object the ability to bind and trigger custom named events." The docs
// also shows us how we can use Underscore.js to create an event dispatcher,
// i.e. a component on which we can bind and trigger events:
//
// var events = _.clone(Backbone.Events);
//
// With this little bit of functionality we can let the success callback
// trigger an event instead of calling methods. We can also declare in the
// constructor which methods we want to call when the event is triggered:
var events = _.clone(Backbone.Events);
var Statuses = function() {};
Statuses.prototype.add = function(options) {
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: {text: options.text},
success: options.success
});
};
var NewStatusView = function(options) {
this.statuses = options.statuses;
events.on('status:add', this.appendStatus, this);
events.on('status:add', this.clearInput, this);
var add = $.proxy(this.addStatus, this);
$('#new-status form').submit(add);
}
NewStatusView.prototype.addStatus = function(e) {
e.preventDefault();
this.statuses.add({
text: $('#new-status textarea').val(),
success: function(data) {
events.trigger('status:add', data.text);
}
});
});
NewStatusView.prototype.appendStatus = function(text) {
$('#statuses ul').append('<li>' + text + '</li>');
}
NewStatusView.prototype.clearInput = function() {
$('#new-status').find('textarea').val('');
}
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({statuses: statuses});
});
///////////////////////////////////////////////////////////////////////////
// Now we can declare in the constructor what we want to happen when a status is added,
// instead of addStatus being responsible for handling success. The only responsibility
// addStatus should have is backend communication, not updating the DOM.
//
// As we no longer deal with the view in the success callback we can move
// the triggering of the event into the add method on Statuses:
var events = _.clone(Backbone.Events);
var Statuses = function() {};
Statuses.prototype.add = function(text) {
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: {text: text},
success: function(data) {
events.trigger('status:add', data.text);
}
});
};
var NewStatusView = function(options) {
this.statuses = options.statuses;
events.on('status:add', this.appendStatus, this);
events.on('status:add', this.clearInput, this);
var add = $.proxy(this.addStatus, this);
$('#new-status form').submit(add);
}
NewStatusView.prototype.addStatus = function(e) {
e.preventDefault();
this.statuses.add($('#new-status textarea').val());
}
NewStatusView.prototype.appendStatus = function(text) {
$('#statuses ul').append('<li>' + text + '</li>');
}
NewStatusView.prototype.clearInput = function() {
$('#new-status').find('textarea').val('');
}
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({statuses: statuses});
});
///////////////////////////////////////////////////////////////////////////
// A view's responsibilities
//
// Looking at appendStatus and clearInput in NewStatusView,
// we see that these methods focus on two different DOM elements,
// #statuses and #new-status, respectively. I've given them different colors in the app,
// so you can see the difference. Working on two elements in a view does not adhere to
// the principles I outline in my blog post on a view's responsibilities.
//
// Let's pull a StatusesView out of NewStatusView, and let it be responsible for #statuses.
// Separating these responsibilities is especially simple now that we use
// events — with a regular success callback this would be far more difficult.
var events = _.clone(Backbone.Events);
var Statuses = function() {};
Statuses.prototype.add = function(text) {
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: {text: text},
success: function(data) {
events.trigger('status:add', data.text);
}
});
};
var NewStatusView = function(options) {
this.statuses = options.statuses;
events.on('status:add', this.clearInput, this);
var add = $.proxy(this.addStatus, this);
$('#new-status form').submit(add);
}
NewStatusView.prototype.addStatus = function(e) {
e.preventDefault();
this.statuses.add($('#new-status textarea').val());
}
NewStatusView.prototype.clearInput = function() {
$('#new-status').find('textarea').val('');
}
var StatusesView = function() {
events.on('status:add', this.appendStatus, this);
}
StatusView.prototype.appendStatus = function(text) {
$('#statuses ul').append('<li>' + text + '</li>');
}
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({statuses: statuses});
new StatusesView();
});
///////////////////////////////////////////////////////////////////////////
// As each view is now responsible for only one HTML element,
// we can specify them when instantiating the views:
var events = _.clone(Backbone.Events);
var Statuses = function() {};
Statuses.prototype.add = function(text) {
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: {text: text},
success: function(data) {
events.trigger('status:add', data.text);
}
});
};
var NewStatusView = function(options) {
this.statuses = options.statuses;
this.el = $('#new-status');
events.on('status:add', this.clearInput, this);
var add = $.proxy(this.addStatus, this);
this.el.find('form').submit(add);
}
NewStatusView.prototype.addStatus = function(e) {
e.preventDefault();
this.statuses.add(this.el.find('textarea').val());
}
NewStatusView.prototype.clearInput = function() {
this.el.find('textarea').val('');
}
var StatusesView = function() {
this.el = $('#statuses');
events.on('status:add', this.appendStatus, this);
}
StatusView.prototype.appendStatus = function(text) {
this.el.find('ul').append('<li>' + text + '</li>');
}
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({statuses: statuses});
new StatusesView();
});
///////////////////////////////////////////////////////////////////////////
// Our views, NewStatusView and StatusesView are still difficult to test
// because they depend on having the HTML present,
// e.g. in order to find $('#statuses').
//
// To remedy this we can pass in its DOM dependencies when we instantiate a view.
var events = _.clone(Backbone.Events);
var Statuses = function() {};
Statuses.prototype.add = function(text) {
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: {text: text},
success: function(data) {
events.trigger('status:add', data.text);
}
});
};
var NewStatusView = function(options) {
this.statuses = options.statuses;
this.el = options.el;
events.on('status:add', this.clearInput, this);
var add = $.proxy(this.addStatus, this);
this.el.find('form').submit(add);
}
NewStatusView.prototype.addStatus = function(e) {
e.preventDefault();
this.statuses.add(this.el.find('textarea').val());
}
NewStatusView.prototype.clearInput = function() {
this.el.find('textarea').val('');
}
var StatusesView = function(options) {
this.el = options.el;
events.on('status:add', this.appendStatus, this);
}
StatusView.prototype.appendStatus = function(text) {
this.el.find('ul').append('<li>' + text + '</li>');
}
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({el: $('#new-statuses'), statuses: statuses});
new StatusesView({el: $('statuses')});
});
///////////////////////////////////////////////////////////////////////////
// Now, this is easy to test! With this change we can use a jQuery
// trick to test our views. Instead of initiating our views by passing
// in for example $('#new-status'), we can pass in the necessary HTML wrapped
// in jQuery, e.g. $('<div><form>…</form></div>').
// jQuery will then create the needed DOM elements on the fly.
// This ensures blazingly fast tests — on my current project our nearly 200
// tests run in less than one second.
//
// Our next step is introducing a helper to clean up our views a little bit.
// Instead of writing this.el.find we can create a simple helper so we can
// write this.$ instead. With this little change it feels like we are saying,
// I want to use jQuery to look for something locally on this view instead
// of globally in the entire HTML. And it's so easy to add:
var events = _.clone(Backbone.Events);
var Statuses = function() {};
Statuses.prototype.add = function(text) {
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: {text: text},
success: function(data) {
events.trigger('status:add', data.text);
}
});
}
var NewStatusView = function(options) {
this.statuses = options.statuses;
this.el = options.el;
events.on('status:add', this.clearInput, this);
var add = $.proxy(this.addStatus, this);
this.$('form').submit(add);
}
NewStatusView.prototype.addStatus = function(e) {
e.preventDefault();
this.statuses.add(this.$('textarea').val());
}
NewStatusView.prototype.clearInput = function() {
this.$('textarea').val('');
}
NewStatusView.prototype.$ = function(selector) {
return this.el.find(selector);
}
var StatusesView = function(options) {
this.el = options.el;
events.on('status:add', this.appendStatus, this);
}
StatusView.prototype.appendStatus = function(text) {
this.$('ul').append('<li>' + text + '</li>');
}
StatusView.prototype.$ = function(selector) {
return this.el.find(selector);
}
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({el: $('#new-statuses'), statuses: statuses});
new StatusesView({el: $('statuses')});
});
///////////////////////////////////////////////////////////////////////////
// However, adding this functionality for every view is a pain.
// That's one of the reasons to use Backbone.js views —
// reusing functionality across views.
//
// Getting started with views in Backbone
//
// With the current state of our code, it's just a couple of lines of
// change needed to add Backbone.js views:
var events = _.clone(Backbone.Events);
var Statuses = function() {};
Statuses.prototype.add = function(text) {
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: {text: text},
success: function(data) {
events.trigger('status:add', data.text);
}
});
};
var NewStatusView = Backbone.View.extend({
initialize: function(options) {
this.statuses = options.statuses;
this.el = options.el;
events.on('status:add', this.clearInput, this);
var add = $.proxy(this.addStatus, this);
this.$('form').submit(add);
}
});
NewStatusView.prototype.addStatus = function(e) {
e.preventDefault();
this.statuses.add(this.$('textarea').val());
}
NewStatusView.prototype.clearInput = function() {
this.$('textarea').val('');
}
NewStatusView.prototype.$ = function(selector) {
return this.el.find(selector);
}
var StatusesView = function(options) {
this.el = options.el;
events.on('status:add', this.appendStatus, this);
}
StatusView.prototype.appendStatus = function(text) {
this.$('ul').append('<li>' + text + '</li>');
}
StatusView.prototype.$ = function(selector) {
return this.el.find(selector);
}
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({el: $('#new-statuses'), statuses: statuses});
new StatusesView({el: $('statuses')});
});
///////////////////////////////////////////////////////////////////////////
// As you can see from the code, we use Backbone.View.extend to create a
// new view class in Backbone. Within extend we can specify instance
// methods such as initialize, which is the name of the constructor.
//
// Now that we have started the move over to Backbone.js views,
// let's go on and move both views fully over:
var events = _.clone(Backbone.Events);
var Statuses = function() {};
Statuses.prototype.add = function(text) {
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: {text: text},
success: function(data) {
events.trigger('status:add', data.text);
}
});
};
var NewStatusView = Backbone.View.extend({
initialize: function(options) {
this.statuses = options.statuses;
this.el = options.el;
events.on('status:add', this.clearInput, this);
var add = $.proxy(this.addStatus, this);
this.$('form').submit(add);
},
addStatus: function(e) {
e.preventDefault();
this.statuses.add(this.$('textarea').val());
},
clearInput: function() {
this.$('textarea').val('');
},
$: function(selector) {
return this.el.find(selector);
}
});
var StatusesView = Backbone.View.extend({
initialize: function(options) {
this.el = options.el;
events.on('status:add', this.appendStatus, this);
},
appendStatus: function(text) {
this.$('ul').append('<li>' + text + '</li>');
},
$: function(selector) {
return this.el.find(selector);
}
})
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({el: $('#new-statuses'), statuses: statuses});
new StatusesView({el: $('statuses')});
});
///////////////////////////////////////////////////////////////////////////
// Now that we use Backbone.js views we can remove the this.$ helper,
// as it already exists in Backbone. We also no longer need to set this.el
// ourselves, as Backbone.js does it automatically when a view is
// instantiated with an HTML element.
var events = _.clone(Backbone.Events);
var Statuses = function() {};
Statuses.prototype.add = function(text) {
$.ajax({
url: '/status',
type: 'POST',
dataType: 'json',
data: {text: text},
success: function(data) {
events.trigger('status:add', data.text);
}
});
};
var NewStatusView = Backbone.View.extend({
initialize: function(options) {
this.statuses = options.statuses;
events.on('status:add', this.clearInput, this);
var add = $.proxy(this.addStatus, this);
this.$('form').submit(add);
},
addStatus: function(e) {
e.preventDefault();
this.statuses.add(this.$('textarea').val());
},
clearInput: function() {
this.$('textarea').val('');
}
});
var StatusesView = Backbone.View.extend({
initialize: function(options) {
events.on('status:add', this.appendStatus, this);
},
appendStatus: function(text) {
this.$('ul').append('<li>' + text + '</li>');
}
})
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({el: $('#new-statuses'), statuses: statuses});
new StatusesView({el: $('statuses')});
});
///////////////////////////////////////////////////////////////////////////
// Let's use a model
//
// The next step is introducing models, which are responsible for the network traffic,
// i.e. Ajax requests and responses. As Backbone nicely abstracts Ajax, we don't
// need to specify type, dataType and data anymore. Now we only need to specify
// the URL and call save on the model. The save method accepts the data we want
// to save as the first parameter, and options, such as the success callback,
// as the second parameter.
var events = _.clone(Backbone.Events);
var Status = Backbone.Model.extend({
url: '/status'
});
var Statuses = function() {};
Statuses.prototype.add = function(text) {
var status = new Status();
status.save({text: text}), {
success: function(data) {
events.trigger('status:add', data.text);
}
}
};
var NewStatusView = Backbone.View.extend({
initialize: function(options) {
this.statuses = options.statuses;
events.on('status:add', this.clearInput, this);
var add = $.proxy(this.addStatus, this);
this.$('form').submit(add);
},
addStatus: function(e) {
e.preventDefault();
this.statuses.add(this.$('textarea').val());
},
clearInput: function() {
this.$('textarea').val('');
}
});
var StatusesView = Backbone.View.extend({
initialize: function(options) {
events.on('status:add', this.appendStatus, this);
},
appendStatus: function(text) {
this.$('ul').append('<li>' + text + '</li>');
}
})
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({el: $('#new-statuses'), statuses: statuses});
new StatusesView({el: $('statuses')});
});
///////////////////////////////////////////////////////////////////////////
// Handling several models
//
// Now that we have introduced models, we need a concept for a list of models,
// such as the list of statuses in our application. In Backbone.js this
// concept is called a collection.
//
// One really cool thing about collections is that they have scoped events.
// Basically, this just means that we can bind and trigger events directly on
// a collection instead of using our events variable — our events will live
// on statuses instead of events. As we now start firing events directly on
// statuses there's no need for "status" in the event name,
// so we rename it from "status:add" to "add".
var Status = Backbone.Model.extend({
url: '/status'
});
var Statuses = Backbone.Collection.extend({
add: function(text) {
var that = this;
var status = new Status();
status.save({text: text}), {
success: function(model, data) {
events.trigger('status:add', data.text);
}
}
}
});
var NewStatusView = Backbone.View.extend({
initialize: function(options) {
this.statuses = options.statuses;
this.statuses.on('add', this.clearInput, this);
var add = $.proxy(this.addStatus, this);
this.$('form').submit(add);
},
addStatus: function(e) {
e.preventDefault();
this.statuses.add(this.$('textarea').val());
},
clearInput: function() {
this.$('textarea').val('');
}
});
var StatusesView = Backbone.View.extend({
initialize: function(options) {
this.statuses = options.statuses;
this.statuses.on('add', this.appendStatus, this);
},
appendStatus: function(text) {
this.$('ul').append('<li>' + text + '</li>');
}
})
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({el: $('#new-statuses'), statuses: statuses});
new StatusesView({el: $('statuses'), statuses: statuses});
});
///////////////////////////////////////////////////////////////////////////
// We can simplify this even more by using Backbone's create method.
// It creates a new model instance, adds it to the collection and saves it to the server.
// Therefore we must specify what type of model the collection should handle.
// There are two things we need to change to use Backbone collections:
//
// 1) When creating the status we need to pass in an options hash with the attributes
// we want to save, instead of only passing the text.
//
// 2) The built in create also triggers an "add" event, but rather than
// passing only the text, as we have done so far, it passes the newly created model.
// We can get the text from the model by calling model.get("text").
var Status = Backbone.Model.extend({
url: '/status'
});
var Statuses = Backbone.Collection.extend({
model: Status
});
var NewStatusView = Backbone.View.extend({
initialize: function(options) {
this.statuses = options.statuses;
this.statuses.on('add', this.clearInput, this);
var add = $.proxy(this.addStatus, this);
this.$('form').submit(add);
},
addStatus: function(e) {
e.preventDefault();
this.statuses.create({text: this.$('textarea').val())};
},
clearInput: function() {
this.$('textarea').val('');
}
});
var StatusesView = Backbone.View.extend({
initialize: function(options) {
this.statuses = options.statuses;
this.statuses.on('add', this.appendStatus, this);
},
appendStatus: function(status) {
this.$('ul').append('<li>' + status.get('text') + '</li>');
}
})
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({el: $('#new-statuses'), statuses: statuses});
new StatusesView({el: $('statuses'), statuses: statuses});
});
///////////////////////////////////////////////////////////////////////////
// As with el earlier, Backbone.js automatically sets this.collection when
// collection is passed. Therefore we rename statuses to collection in our view:
var Status = Backbone.Model.extend({
url: '/status'
});
var Statuses = Backbone.Collection.extend({
model: Status
});
var NewStatusView = Backbone.View.extend({
initialize: function() {
this.collection.on('add', this.clearInput, this);
var add = $.proxy(this.addStatus, this);
this.$('form').submit(add);
},
addStatus: function(e) {
e.preventDefault();
this.collection.create({text: this.$('textarea').val())};
},
clearInput: function() {
this.$('textarea').val('');
}
});
var StatusesView = Backbone.View.extend({
initialize: function() {
this.collections.on('add', this.appendStatus, this);
},
appendStatus: function(status) {
this.$('ul').append('<li>' + status.get('text') + '</li>');
}
})
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({el: $('#new-statuses'), collection: statuses});
new StatusesView({el: $('statuses'), collection: statuses});
});
///////////////////////////////////////////////////////////////////////////
// Evented views
//
// Now, let's get rid of that nasty $.proxy stuff. We can do this by letting
// Backbone.js delegate our events by specifying them in an events hash in the view.
// This hash is of the format {"event selector": "callback"}:
var Status = Backbone.Model.extend({
url: '/status'
});
var Statuses = Backbone.Collection.extend({
model: Status
});
var NewStatusView = Backbone.View.extend({
initialize: function() {
this.collection.on('add', this.clearInput, this);
},
events: {
'submit form': 'addStatus'
}
addStatus: function(e) {
e.preventDefault();
this.collection.create({text: this.$('textarea').val())};
},
clearInput: function() {
this.$('textarea').val('');
}
});
var StatusesView = Backbone.View.extend({
initialize: function() {
this.collections.on('add', this.appendStatus, this);
},
appendStatus: function(status) {
this.$('ul').append('<li>' + status.get('text') + '</li>');
}
})
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({el: $('#new-statuses'), collection: statuses});
new StatusesView({el: $('statuses'), collection: statuses});
});
///////////////////////////////////////////////////////////////////////////
// Escape it!
//
// As our last step we're going to prevent XSS-exploits.
// Instead of using model.get('text') let's use the built-in escape handling
// and write model.escape('text') instead. If you're using Handlebars,
// Mustache or similar templating engines, you might get this functionality
// out of the box.
var Status = Backbone.Model.extend({
url: '/status'
});
var Statuses = Backbone.Collection.extend({
model: Status
});
var NewStatusView = Backbone.View.extend({
initialize: function() {
this.collection.on('add', this.clearInput, this);
},
events: {
'submit form': 'addStatus'
}
addStatus: function(e) {
e.preventDefault();
this.collection.create({text: this.$('textarea').val())};
},
clearInput: function() {
this.$('textarea').val('');
}
});
var StatusesView = Backbone.View.extend({
initialize: function() {
this.collections.on('add', this.appendStatus, this);
},
appendStatus: function(status) {
this.$('ul').append('<li>' + status.escape('text') + '</li>');
}
})
$(document).ready(function() {
var statuses = new Statuses();
new NewStatusView({el: $('#new-statuses'), collection: statuses});
new StatusesView({el: $('statuses'), collection: statuses});
});
///////////////////////////////////////////////////////////////////////////
// And we're done!
// This is our final code:
//
// However, the code has increased from 16 lines to more than 40,
// so why do I think this is better? Because we are now working on a higher
// level of abstraction.
//
// This code is more maintainable, easier to reuse and extend, and easier to test.
// What I've seen is that Backbone.js helps improve the structure of my JavaScript
// applications considerably, and in my experience the end result is often less
// complex and has fewer lines of code than my "regular JavaScript".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment