From jQuery to Backbone

imageThis article will show how you can reorganize code written in a "simple" jQuery style into Backbone code using representations, models, collections and events. The reorganization will be gradual so that this process gives a clear understanding of the main abstractions in Backbone. The article is intended for those who use jQuery and would like to get acquainted with the MVC scheme for client code.

This article is a translation 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><divid="new-status"><h2>New monolog</h2><formaction=""><textarea></textarea><br><inputtype="submit"value="Post"></form></div><divid="statuses"><h2>Monologs</h2><ul></ul></div></body>

Here you can see 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 form submission, in which all the logic is located. But what is the problem? This code does a lot of things at the same time. It listens for page events, user events, network events, processes user input, analyzes the response, and manipulates the DOM. And that's all in 16 lines of code. Next, we will reorganize this code so that it meets the principle of single responsibility , so that it is easy to test, maintain, reuse and expand.
Here are three beliefs we want to achieve:
  • We want to fetch as much code as possible from $ (document) .ready . In its current state, the code is almost impossible to test.
  • We want to adhere to a single principle of responsibility, and make the code more usable and verifiable.
  • We want to break the connection between the DOM and Ajax.

Separation of DOM and Ajax


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

Change No. 1

+functionaddStatus(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, the “+” sign marks the added lines, and the “-” marks the deleted lines.

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
functionaddStatus(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 “class” Statuses :

Change No. 3
-functionaddStatus(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('');
             }
         });
     });
 });


Create view


Our form handler now has one dependency on the statuses variable , and everything inside the handler works only with the DOM. Let's move the form handler and everything inside into a separate “class” 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 that we have taken so far have identified two components from the code that are easier to test and have clearer responsibilities. But still there is something to work on. Let's start by taking the form handler out of the view into 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 we start in Chrome, we see an error:
Uncaught TypeError: Cannot call method 'add' of undefined

We get this error because this has different meanings in the constructor and the addStatus method . (If you do not fully understand why this is happening, I recommend reading 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 during its development. We have also become closer to using Backbone.

Add Events


For the next step, we will need to use our first Backbone module - events. Events are just a way of saying: “Hello, I want to know when some action will happen” and “Hello, do you know that the 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 submit.
The Backbone documentation explains about Backbone.Events this way: "Events is a module that can be mixed with any object, which gives the object the ability to bind and trigger custom events." The documentation also tells us how we can use Underscore.js to create an event dispatcher:
var events = _.clone(Backbone.Events);

With this little functionality, we can let success 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. AddStatus' sole responsibility is feedback, not DOM manipulation.

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 });
 });


Obligations of submissions


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 the responsibility of working with #statuses from NewStatusView into a separate StatusesView . This separation does not require much effort from us, since now we use the event dispatcher, and with hard function callbacks it would be much more complicated.

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 presence of an HTML element. In order to fix this, we will set 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 the views. Instead of initializing the view with 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, as there are no DOM manipulations.

Our next step will be 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 a 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) {
+    returnthis.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) {
+    returnthis.el.find(selector);
+};
 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ el: $('#new-status'), statuses: statuses });
     new StatusesView({ el: $('#statuses') });
 });


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

Getting started with views


In the current state of our code, you need to write just 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) {
     returnthis.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) {
     returnthis.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 heir, we can specify methods, such as initialization, which is the 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) {
+        returnthis.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) {
-    returnthis.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) {
-    returnthis.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) {
+        returnthis.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 this. $ Helper function because it already exists in Backbone. Also, we no longer need to save this.el , since Backbone does this automatically when the view is initialized with an HTML element.

Change No. 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) {
-        returnthis.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) {
-        returnthis.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 the introduction of 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 request type, data type, and data. Now we only need to specify the URL and cause the model to be saved. The save method accepts the data that 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') });
 });


Processing multiple models


Now that we have introduced the models, we need the concept of a list of models, such as a list of statuses in our application. In Backbone, this concept is called collection.
One of the great things about collections is that they have an area of ​​events. Basically, it just means that we can bind and trigger events directly on the collection, instead of using our event variables. If we now start to generate events directly on the 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 saves it on the server. Therefore, we must indicate what type of model we will use for the collection. There are two things that we need to change in order to use the Backbone collection:
  • When creating a new status, we must also pass the attribute name that we want to save, and not just the text
  • When creating, the “add” event is automatically triggered, but instead of passing only the text, as it was until now, the whole model will be sent. We can get the text from the model by calling model.get ("text")


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 submitted. Therefore, we will rename statuses to 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 last step is to prevent XSS attacks. Instead of using model.get ('text') we will use the built-in escaping 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 see the application after the reorganization. Yes, of course, from a user point of view, it looks exactly the same as the first version. And besides, the code has grown from 16 lines to more than 40, so why do I think the code got better? Yes, because now we are working at a higher level of abstraction. This code is easier to maintain, easier to reuse and extend, and easier to test.
As we saw, Backbone helped significantly improve the structure of the application code, and in my experience the end result is less complex and has less code than my “vanilla JavaScript”.

UPD for convenience hid the source of changes to spoilers

Also popular now: