📜 ⬆️ ⬇️

From jQuery to Backbone

image This article will show you how to reorganize code written in a “simple” jQuery style into Backbone code, using views, models, collections, and events. The reorganization will be gradual, so that this process gives a clear understanding of the basic abstractions in Backbone. The article is designed for those who use jQuery and would like to get acquainted with the MVC scheme for client code.

This article is a translation of material from github .


Let's start with the application code, which we will actually reorganize.
$(document).ready(function() { $('#new-status form').submit(function(e) { e.preventDefault(); $.ajax({ url: '/status', type: 'POST', dataType: 'json', data: { text: $('#new-status').find('textarea').val() }, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); $('#new-status').find('textarea').val(''); } }); }); }); 

 <body> <div id="new-status"> <h2>New monolog</h2> <form action=""> <textarea></textarea><br> <input type="submit" value="Post"> </form> </div> <div id="statuses"> <h2>Monologs</h2> <ul></ul> </div> </body> 

Here you can look at the code in action. The application allows you to enter text, when you click on "Post", this text is sent to the server and displayed below in the history.
The application waits for the page to load, adds a wait for the submission of the form in which all the logic is located. But what's the problem? This code does many things at once. It listens to page events, user events, network events, processes user input, analyzes the response, and manipulates the DOM. And this is all in 16 lines of code. Next, we reorganize this code so that it meets the principle of common responsibility , so that it is easy to test, maintain, reuse and extend.
Here are three beliefs we want to achieve:

DOM and Ajax separation


DOM manipulations need to be separated from working with Ajax, and the first step is to create the addStatus function:

Change No. 1

 +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 textarea').val(''); + } + }); +} + $(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 textarea').val(''); - } - }); + addStatus(); }); }); 


Hereinafter, “+” marks added lines, and “-” marks deleted.

Of course, in both data and success, we still work with the DOM. We must break this connection by passing them as arguments:
')
Change No. 2
 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 textarea').val(''); - } + data: { text: options.text }, + success: options.success }); } $(document).ready(function() { $('#new-status form').submit(function(e) { e.preventDefault(); - addStatus(); + addStatus({ + text: $('#new-status textarea').val(), + success: function(data) { + $('#statuses ul').append('<li>' + data.text + '</li>'); + $('#new-status textarea').val(''); + } + }); }); }); 


Next, you need to wrap these statuses in an object, so that you can write statuses.add instead of addStatus .
To do this, use the constructor pattern with the prototype, and create a Statuses "class":

Change number 3
 -function addStatus(options) { +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(); - addStatus({ + statuses.add({ text: $('#new-status textarea').val(), success: function(data) { $('#statuses ul').append('<li>' + data.text + '</li>'); $('#new-status textarea').val(''); } }); }); }); 


Creating a view


Our form handler now has one dependency on the statuses variable, and everything inside the handler only works with the DOM. Let's move the form handler and everything inside to a separate “class” of NewStatusView :

Change No. 4
 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 textarea').val(''); + } + }); + }); +}; + $(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 ul').append('<li>' + data.text + '</li>'); - $('#new-status textarea').val(''); - } - }); - }); + new NewStatusView({ statuses: statuses }); }); 


Now we only initialize the application when the DOM is loaded, and everything else is rendered for $ (document) .ready . The steps we have done so far have separated from the code two components that are easier to test and have clearer responsibilities. But still there is work to do. Let's start by removing the form handler from the view to a separate addStatus method:

Change No. 5
  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; + this.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 textarea').val(''); - } - }); - }); + $('#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 textarea').val(''); + } + }); +}; $(document).ready(function() { var statuses = new Statuses(); new NewStatusView({ statuses: statuses }); }); 


But when running in Chrome, we will see an error:
Uncaught TypeError: Cannot call method 'add' of undefined

We get this error because this has different values ​​in the constructor and the addStatus method. (If you do not fully understand why this is happening, I recommend reading the Understanding JavaScript Function Invocation and “this” ). To solve this problem, we can use $ .proxy , which creates a function in which this has the context we need.

Change No. 6
 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); + 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 textarea').val(''); } }); }; $(document).ready(function() { var statuses = new Statuses(); new NewStatusView({ statuses: statuses }); }); 


Let's make success a separate method that will work with the DOM, which will make the code more readable and flexible:

Change No. 7
 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) { - $('#statuses ul').append('<li>' + data.text + '</li>'); - $('#new-status textarea').val(''); + that.appendStatus(data.text); + that.clearInput(); } }); }; +NewStatusView.prototype.appendStatus = function(text) { + $('#statuses ul').append('<li>' + text + '</li>'); +}; +NewStatusView.prototype.clearInput = function() { + $('#new-status textarea').val(''); +}; $(document).ready(function() { var statuses = new Statuses(); new NewStatusView({ statuses: statuses }); }); 


It is much easier to test and support the project as it develops. We have also become closer to using Backbone.

Adding events


For the next step, we will need to use our first Backbone module - events. Events are just a way of saying “Hi, I want to know when some action will happen” and “Hi, do you know what action you were waiting for just happened?”. This is the same idea as jQuery events when working with the DOM, such as waiting for a click or submission.
The Backbone documentation explains about Backbone.Events like this: “Events are a module that can be mixed with any object, which gives an object the ability to associate and trigger custom events.” The documentation also tells us how we can use Underscore.js to create an event manager:
 var events = _.clone(Backbone.Events); 

With this little functionality, we can allow success to notify instead of calling functions. We can also declare in the constructor which methods we want to notify when the event occurs:

Change No. 8
 +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(); - var that = this; - this.statuses.add({ text: $('#new-status textarea').val(), success: function(data) { - that.appendStatus(data.text); - that.clearInput(); + events.trigger('status:add', data.text); } }); }; NewStatusView.prototype.appendStatus = function(text) { $('#statuses ul').append('<li>' + text + '</li>'); }; NewStatusView.prototype.clearInput = function() { $('#new-status textarea').val(''); }; $(document).ready(function() { var statuses = new Statuses(); new NewStatusView({ statuses: statuses }); }); 


Now in the constructor we can declare what we want to call when a new status is added, instead of addStatus calling the desired function. The only responsibility of addStatus is feedback, not manipulation of the DOM.

Change No. 9
 var events = _.clone(Backbone.Events); var Statuses = function() { }; -Statuses.prototype.add = function(options) { +Statuses.prototype.add = function(text) { $.ajax({ url: '/status', type: 'POST', dataType: 'json', - data: { text: options.text }, - success: options.success + 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({ - text: $('#new-status textarea').val(), - success: function(data) { - events.trigger('status:add', data.text); - } - }); + this.statuses.add($('#new-status textarea').val()); }; NewStatusView.prototype.appendStatus = function(text) { $('#statuses ul').append('<li>' + text + '</li>'); }; NewStatusView.prototype.clearInput = function() { $('#new-status textarea').val(''); }; $(document).ready(function() { var statuses = new Statuses(); new NewStatusView({ statuses: statuses }); }); 


Submission Duties


Looking at appendStatus and clearInput in NewStatusView , we see that these methods work with two different DOM elements, #statuses and # new-status, respectively. This is not consistent with the principle of shared responsibility Let's take responsibility for working with #statuses into a separate StatusesView view from NewStatusView . This separation does not require much effort from us, since now we are using the event dispatcher, and with hard callbacks of functions this would be much more difficult.

Change No. 10
 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 textarea').val(''); }; +var StatusesView = function() { + events.on('status:add', this.appendStatus, this); +}; +StatusesView.prototype.appendStatus = function(text) { + $('#statuses ul').append('<li>' + text + '</li>'); +}; + $(document).ready(function() { var statuses = new Statuses(); new NewStatusView({ statuses: statuses }); + new StatusesView(); }); 


Now, since views are responsible for only one HTML element, we can specify them in the constructor:

Change No. 11
 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); - $('#new-status form').submit(add); + this.el.find('form').submit(add); }; NewStatusView.prototype.addStatus = function(e) { e.preventDefault(); - this.statuses.add($('#new-status textarea').val()); + this.statuses.add(this.el.find('textarea').val()); }; NewStatusView.prototype.clearInput = function() { - $('#new-status textarea').val(''); + this.el.find('textarea').val(''); }; var StatusesView = function() { + this.el = $('#statuses'); + events.on('status:add', this.appendStatus, this); }; StatusesView.prototype.appendStatus = function(text) { - $('#statuses ul').append('<li>' + text + '</li>'); + 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 the availability of the HTML element. To fix this, we will define these elements when creating views:

Change No. 12
 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'); + 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() { - this.el = $('#statuses'); +var StatusesView = function(options) { + this.el = options.el; events.on('status:add', this.appendStatus, this); }; StatusesView.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(); + new NewStatusView({ el: $('#new-status'), statuses: statuses }); + new StatusesView({ el: $('#statuses') }); }); 


Now the code is easy to test. With this change, we can use the following jQuery trick to test views. Instead of initializing the view using for example $ ('# new-status') , we can pass the necessary jQuery HTML wrapper, for example $ (' …' ) . jQuery will create the necessary elements on the fly. This provides incredibly fast tests, since there are no DOM manipulations.

Our next step is to introduce an assistant to clear our views a bit. Instead of writing this.el.find we can create a simple helper function so that we can write this. $ . With this small change, it looks like we are saying, “I want to use jQuery to search for something locally in the view, and not globally in all HTML”. And it is so easy to add:

Change No. 13
  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); + this.$('form').submit(add); }; NewStatusView.prototype.addStatus = function(e) { e.preventDefault(); - this.statuses.add(this.el.find('textarea').val()); + this.statuses.add(this.$('textarea').val()); }; NewStatusView.prototype.clearInput = function() { - this.el.find('textarea').val(''); + 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); }; StatusesView.prototype.appendStatus = function(text) { - this.el.find('ul').append('<li>' + text + '</li>'); + this.$('ul').append('<li>' + text + '</li>'); }; +StatusesView.prototype.$ = function(selector) { + return this.el.find(selector); +}; $(document).ready(function() { var statuses = new Statuses(); new NewStatusView({ el: $('#new-status'), statuses: statuses }); new StatusesView({ el: $('#statuses') }); }); 


However, adding this function to each view looks silly. This is one of the reasons to use Backbone views - to reuse functionality in views.

Getting started with views


In the current state of our code, you only need to write a couple of lines to add Backbone views:

Change No. 14
  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); -}; +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); }; StatusesView.prototype.appendStatus = function(text) { this.$('ul').append('<li>' + text + '</li>'); }; StatusesView.prototype.$ = function(selector) { return this.el.find(selector); }; $(document).ready(function() { var statuses = new Statuses(); new NewStatusView({ el: $('#new-status'), statuses: statuses }); new StatusesView({ el: $('#statuses') }); }); 


As you can see from the code, we use Backbone.View.extend to create a new Backbone view. In the successor we can specify methods, such as initialization, which is a constructor.
Now that we have started using Backbone views, let's translate the second view to Backbone:

Change No. 15
 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); + } }); -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); -}; -StatusesView.prototype.appendStatus = function(text) { - this.$('ul').append('<li>' + text + '</li>'); -}; -StatusesView.prototype.$ = 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-status'), statuses: statuses }); new StatusesView({ el: $('#statuses') }); }); 


Now, since we only use Backbone views, we can remove the function helper this. $ Because it already exists in Backbone. We also no longer need to save this.el , since Backbone does this automatically when the view is initialized by the HTML element.

Change №16
 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-status'), statuses: statuses }); new StatusesView({ el: $('#statuses') }); }); 


Using models


The next step is to introduce models that are responsible for communicating with the server, that is, for Ajax requests and responses. Since Backbone abstracts Ajax well, we no longer need to specify the type of request, data type and data. Now we only need to specify the URL and call the save model. The save method takes the data we want to save as the first parameter and parameters, such as callback, as the second parameter.

Change No. 17
 var events = _.clone(Backbone.Events); +var Status = Backbone.Model.extend({ + url: '/status' +}); + 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 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; 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-status'), statuses: statuses }); new StatusesView({ el: $('#statuses') }); }); 


Handling multiple models


Now that we have introduced the models, we need the concept of a list of models, such as the list of statuses in our application. In Backbone, this concept is called a collection.
One of the great things about collections is that they have an area of ​​events. Basically, this simply means that we can bind and trigger events, directly on the collection, instead of using our event variables. If we now start generating events directly on statuses, then there is no need for the word “status” in the name of the event, so we will rename it from “status: add” to “add”.

Change No. 18
 -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(model, data) { - events.trigger("status:add", data.text); - } - }); -}; +var Statuses = Backbone.Collection.extend({ + add: function(text) { + var that = this; + var status = new Status(); + status.save({ text: text }, { + success: function(model, data) { + that.trigger("add", data.text); + } + }); + } +}); var NewStatusView = Backbone.View.extend({ initialize: function(options) { this.statuses = options.statuses; - events.on("status:add", this.clearInput, this); + 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; + - events.on("status:add", this.appendStatus, this); + 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-status'), statuses: statuses }); - new StatusesView({ el: $('#statuses') }); + new StatusesView({ el: $('#statuses'), statuses: statuses }); }); 


We can simplify this code using the Backbone creation method. He creates a new instance of the model, adds it to the collection and stores it on the server. Therefore, we must specify which type of model we will use for the collection. There are two things we need to change to use the Backbone collection:


Change No. 19
  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) { - that.trigger("add", data.text); - } - }); - } + 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.add(this.$('textarea').val()); + 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(text) { + appendStatus: function(status) { - this.$('ul').append('<li>' + text + '</li>'); + this.$('ul').append('<li>' + status.get("text") + '</li>'); } }); $(document).ready(function() { var statuses = new Statuses(); new NewStatusView({ el: $('#new-status'), statuses: statuses }); new StatusesView({ el: $('#statuses'), statuses: statuses }); }); 


As with el earlier, Backbone will automatically set this.collection when the collection is transferred. Therefore, we will rename the statuses in the collection in our views:

Change No. 20
 var Status = Backbone.Model.extend({ url: '/status' }); var Statuses = Backbone.Collection.extend({ model: Status }); var NewStatusView = Backbone.View.extend({ - initialize: function(options) { + initialize: function() { - this.statuses = options.statuses; - - this.statuses.on('add', this.clearInput, this); + this.collection.on('add', this.clearInput, this); var add = $.proxy(this.addStatus, this); this.$('form').submit(add); }, addStatus: function(e) { e.preventDefault(); - this.statuses.add({ text: this.$('textarea').val() }); + this.collection.create({ text: this.$('textarea').val() }); }, clearInput: function() { this.$('textarea').val(''); } }); var StatusesView = Backbone.View.extend({ - initialize: function(options) { + initialize: function() { - this.statuses = options.statuses; - - this.statuses.on('add', this.appendStatus, this); + this.collection.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-status'), statuses: statuses }); + new NewStatusView({ el: $('#new-status'), collection: statuses }); - new StatusesView({ el: $('#statuses'), statuses: statuses }); + new StatusesView({ el: $('#statuses'), collection: statuses }); }); 


Events in views


Now, let's finally get rid of $ .proxy . We can do this by delegating Backbone event management. It looks like this: {'event selector': 'callback'} :

Change No. 21
 var Status = Backbone.Model.extend({ url: '/status' }); var Statuses = Backbone.Collection.extend({ model: Status }); var NewStatusView = Backbone.View.extend({ + events: { + 'submit form': 'addStatus' + }, + 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.collection.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-status'), collection: statuses }); new StatusesView({ el: $('#statuses'), collection: statuses }); }); 


Screen it


Our final step is to prevent XSS attacks. Instead of using model.get ('text'), we will use the built-in screening function, it looks like model.escape ('text') . If you use Handlebars, Mustache, or other template engines, you can get protection out of the box.

Change No. 22
  var Status = Backbone.Model.extend({ url: '/status' }); var Statuses = Backbone.Collection.extend({ model: Status }); var NewStatusView = Backbone.View.extend({ events: { "submit form": "addStatus" }, initialize: function(options) { this.collection.on("add", this.clearInput, this); }, addStatus: function(e) { e.preventDefault(); this.collection.create({ text: this.$('textarea').val() }); }, clearInput: function() { this.$('textarea').val(''); } }); var StatusesView = Backbone.View.extend({ initialize: function(options) { this.collection.on("add", this.appendStatus, this); }, appendStatus: function(status) { - this.$('ul').append('<li>' + status.get("text") + '</li>'); + this.$('ul').append('<li>' + status.escape("text") + '</li>'); } }); $(document).ready(function() { var statuses = new Statuses(); new NewStatusView({ el: $('#new-status'), collection: statuses }); new StatusesView({ el: $('#statuses'), collection: statuses }); }); 


We are done!


This is the final version of the code:
 var Status = Backbone.Model.extend({ url: '/status' }); var Statuses = Backbone.Collection.extend({ model: Status }); var NewStatusView = Backbone.View.extend({ events: { 'submit form': 'addStatus' }, initialize: function() { this.collection.on('add', this.clearInput, this); }, 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.collection.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-status'), collection: statuses }); new StatusesView({ el: $('#statuses'), collection: statuses }); }); 

Here you can look at the application after the reorganization. Yes, of course from the user's point of view, it looks exactly like the first version. And besides, the code has grown from 16 lines to more than 40, so why do I think that the code is better? Yes, because now we are working at a higher level of abstraction. This code is easier to maintain, easier to reuse and expand, and easier to test.
As we have seen, Backbone has significantly improved the code structure of the application, and in my experience the end result is less complex and has less code than my “vanilla javascript”.

UPD for convenience, hid the sources of changes in spoilers

Source: https://habr.com/ru/post/170563/


All Articles