📜 ⬆️ ⬇️

Writing complex interfaces with Backbone.js

image

Backbone.js is the framework for creating RIA JavaScript applications, its author is Jeremy Ashkenas, the creator of CoffeeScript, Backbone is part of the Document Cloud company she also owns Underscrore.js. Backbone is a very lightweight library that helps you create interfaces. It can work with any libraries you are used to.
Backbone is a set of classes, less than 4Kb in size, that form the structure of your code and help you create high-quality MVC web applications.
Backbone forms the structure of heavy JavaScript applications, adding models with key-value storage and similar events, collections with rich API, views (orig. Views) with declarative event handling and combines all of this into one application that supports the RESTful JSON interface.

Backbone cannot work without Underscore.js . To support REST API and work with DOM elements in Backbone.View it is strongly recommended to connect json2.js and jQuery-like library: jQuery or Zepto
')
The article will examine the structure of Backbone.js, will create a simple Todo application in stages.

These are the levers Backbone gives us.

Key type and value events


When the model content changes, all objects that have been subscribed to the model changes receive notifications and may take further action. For example, views (original views) listen to changes in the model and update their state instead of the model changing the state of the species. The loose coupling pattern is applied.

Rich API of enumerated functions


Backbone comes with a set of very useful functions for processing your data. Unlike other languages, in JavaScript the arrays are “unfinished”, which causes a lot of trouble if you work with a large amount of data.

Views with declarative event handling


Those days when you wrote spaghetti-like code come to an end. You can programmatically determine which callback is associated with which object.

RESTful JSON interface


If you need to communicate with the server, you have to execute an AJAX request in the "view" code and get what you need. This is partially simplified using various xhr adapters, WebSockets and localStorage, but you can make it easier. This entity should be divided into several smaller ones; Backbone will help us with this:
Backbone gives us the opportunity to separate the data from the presentation. A model that works with data and synchronizes with the server, then when the view listens to changes in the model and changes its state (renders the data in HTML)
Immediately list the answers to questions that may arise now:

Does it replace jQuery?

Not. They are very different in their goals. Backbone works with high-level abstractions, while jQuery or similar libraries work with the DOM, normalizes events, and so on.

They have different applications, if you know one library, this does not mean that you need to know another. But as a JavaScript developer, you need to know how to work with both effectively.

Why should I use it?

Because most often the interface code is a dirty set of nested callbacks, DOM manipulations, HTML templates or functions that generate HTML for data presentation. Backbone offers a great tool for managing this chaos.

Where should I use it?

Backbone is ideal for creating heavy interfaces and data-driven applications. For example, the interface GMail, new Twitter or other related applications. Backbone makes creating complex applications easier.

With it, you can create simple JavaScript pages that are rich in JavaScript, but for the most part, Backbone is designed to create web applications.

Does it look like Cappuccino or Sproutcore?

Yes and no. Yes, because, as in the above frameworks, the main goal is to create complex interfaces for web applications. They differ in that Backbone is very light, no one of the aforementioned libraries can match it.
Backbone is extremely light, less than 4kb.
In fact, about 4kb is not true: Backbone 4kb + Underscore.js 3kb + jQuery 31kb = 38kb

Cappuccino forces you to write code in Objective-J, while Sproutcore views must be declared in JavaScript code. But I can’t call any of these approaches a valid one, but with Backbone you write plain JavaScript and use plain HTML and CSS, you practically don’t need to change anything.

Can I use other libraries with Backbone?

Of course. Not only DOM calls, AJAX wrappers, but also templates and script loaders. Backbone has a very, very loosely coupled - this means that you can use almost all of your tools in combination with Backbone.

Backbone structure


Initially, Backbone consisted only of models, types and collections - controllers were not included. Over time, controllers have been added. Now Backbone consists of 4 main classes:
Let's briefly go over the main classes, then on the basis of this knowledge we will create a simple application.

Model

Models are called different things in different MVC frameworks. In Backbone, a model is a separate entity, the closest analogue is an entry in the database. But there are no hard and fast rules. From the framework site:
Models are the heart of all JavaScript applications that contain interactive data as well as most of the logic that surrounds them: transformations, validation, computed properties, and access rights.
A model is a way to read and write properties or attributes in a data set. Here is an example of a model:
var Game = Backbone.Model.extend({}); 

Let's complicate things a bit:
 var Game = Backbone.Model.extend({ initialize: function(){ alert("Oh hey! "); }, defaults: { name: 'Default title', releaseDate: 2011, } }); 

The initialize method will be executed when an object is created. In this method, I do not do anything useful. I also define several variables by default, in case some data is not transferred.

Let's see how to read and write attributes. But first, create an object:
 //    var portal = new Game({ name: "Portal 2", releaseDate: 2011}); //     releaseDate var release = portal.get('releaseDate'); // 2011 //    portal.set({ name: "Portal 2 by Valve"}); 

You cannot work with attributes directly (object.attribute) you must call a method to change or retrieve data (I think the situation will change with the advent of Proxy).
Now all data is in the application memory. Let's save them to the server:
 portal.save(); 

Did you expect something more? AJAX? One line we send a request to the server. Remember that the request type changes: if you create a new object, a POST request will be sent, otherwise there will be a PUT.
I briefly touched the model. Backbone gives much more opportunities for working with models. Details can be found in the documentation.

Collections

Collections in Backbone is just a collection of models. By analogy with the collection database, these are the results of database queries containing strings (Models).
Create a collection of games:
 var GamesCollection = Backbone.Collection.extend({ model : Game }); 

The first thing to notice is that we determine which models the collection consists of. We have created a collection of games consisting of models of the game.

Now you can work with data. For example, let's expand the collection by adding a method that defines old games.
 var GamesCollection = Backbone.Collection.extend({ model : Game, old : function() { return this.filter(function(game) { return game.get('releaseDate') < 2009; }); } }); 

Simple, isn't it? We simply check the games that were created before 2009 and return them. You can also manage the collection directly:
 var games = new GamesCollection games.get(0); 

The example above creates a new collection and gets a model with ID 0. You can find an element in a certain position through a link to the object's position: games.at(0);

And finally, you can dynamically replenish your collection like this:
 var GamesCollection = Backbone.Collection.extend({ model : Game, url: '/games' }); var games = new GamesCollection games.fetch(); 


We notify Backbone with the help of which url to manage the data. Next, we simply create a new object and call the fetch method, which leads to an asynchronous request for data from the server and fills the collection.

Now you know the basics of the Backbone collections. As I noted above, there are still tons of useful things that Backbone can do, for example, all Underscore library methods. Read the documentation before starting the experiments.

View

At first glance, the species may seem confusing. Unlike pure MVC, views in Backbone have some controller functions.

The duties of the species include:
Let's create a simple view:
 var GameView = Backbone.View.extend({ tagName: "div", className: "game", render: function() { //    } }); 

It's pretty simple. I simply specify which HTML element should be used for the wrapper view (tagName and className).

The following code renders the view:
  render : function() { this.el.innerHTML = this.model.get('name'); //   jQuery: $(this.el).html(this.model.get('name')); } 

el refers to the DOM element that represents this view. We just put the name of the game in the HTML element. Obviously, using jQuery is a bit easier.

In more complex markup, using HTML code inside JavaScript is tedious and pointless. In these cases it is worth applying templates.

Backbone has its own template engine (part of Underscore.JS) but you are free to use any kind of template.

Finally, let's see how species listen to events. First DOM events.
 events: { 'click .name': 'handleClick' }, handleClick: function(){ alert('In the name of science... you monster'); // Other actions as necessary } 

Of course, not as familiar as jQuery, but also simple. We define which events we listen through the Events object. As can be seen from the code above, the first part refers to the event, and the following defines the function that is associated with the event.

Now let's associate our view with the model:
 var GameView = Backbone.View.extend({ initialize: function (args) { _.bindAll(this, 'changeName'); this.model.bind('change:name', this.changeName); } }); 

The first thing to note is how we place our binding code in the initialize function. I believe that initialize is the best place to bind events.

bindAll is an Underscore method that binds this context to a function. This is especially useful in events.

Now, as soon as the model name is changed, the changeName function is changeName . You can use other prefixes instead of change: to poll the state of the model.

Controller

Controllers allow you to create an application that stores its status in the url hash (hashbangs).
 var Hashbangs = Backbone.Controller.extend({ routes: { "!/": "root", "!/games": "games", }, root: function() { //      }, games: function() { //      } }); 

This is very similar to routes in traditional server MVC frameworks. For example !/games will be associated with the function while the URL in the browser will be domain/#!/games .

Using hashbangs , you can create web applications that remember their state and can be indexed by Google .

If you fear that it will break the Back button, then Backbone can take care of that.
 //   var ApplicationController = new Controller; Backbone.history.start(); 

Backbone monitors the #hash change and alerts controllers.

Now you know the basics of Backbone, try to create a test application.

Example: Todo List


This application was created by Jérôme Gravel-Niquet . It uses a simple localStorage adapter to save your data in the browser (I don’t stop at the adapter, look at its code - everything is extremely simple). See what happens at the end to better understand the code: Todo List

Model Tudu

Our base model describes an element of the list list, which has the attributes content , order and done .
  window.Todo = Backbone.Model.extend({ //     ,    //    ,       default EMPTY: "empty todo...", //     `content`,    initialize: function() { if (!this.get("content")) { this.set({"content": this.EMPTY}); } }, //   `done` toggle: function() { this.save({done: !this.get("done")}); }, //   localStorage    clear: function() { this.destroy(); this.view.remove(); } }); 


Tudu collection

Tudu collection is stored in localStorage
  window.TodoList = Backbone.Collection.extend({ //       Todo model: Todo, //      "todos"  localStorage localStorage: new Store("todos"), //     ,   done: function() { return this.filter(function(todo){ return todo.get('done'); }); }, //     ,    remaining: function() { return this.without.apply(this, this.done()); }, //     ,          . //      GUID   .      . nextOrder: function() { if (!this.length) return 1; return this.last().get('order') + 1; }, //        comparator: function(todo) { return todo.get('order'); } }); //     window.Todos = new TodoList; 


View - Tudu Element

  // DOM    window.TodoView = Backbone.View.extend({ //    tagName: "li", //   //      template: _.template($('#item-template').html()), //  DOM,     events: { "click .check" : "toggleDone", "dblclick div.todo-content" : "edit", "click span.todo-destroy" : "clear", "keypress .todo-input" : "updateOnEnter" }, // TodoView      . //     ---,     //    . initialize: function() { _.bindAll(this, 'render', 'close'); this.model.bind('change', this.render); this.model.view = this; }, //   render: function() { $(this.el).html(this.template(this.model.toJSON())); this.setContent(); return this; }, //   XSS   `jQuery.text`     setContent: function() { var content = this.model.get('content'); this.$('.todo-content').text(content); this.input = this.$('.todo-input'); this.input.bind('blur', this.close); this.input.val(content); }, //   "done"   toggleDone: function() { this.model.toggle(); }, //      edit: function() { $(this.el).addClass("editing"); this.input.focus(); }, //   ,   close: function() { this.model.save({content: this.input.val()}); $(this.el).removeClass("editing"); }, //   `enter`,    updateOnEnter: function(e) { if (e.keyCode == 13) this.close(); }, //  DOM  remove: function() { $(this.el).remove(); }, //     clear: function() { this.model.clear(); } }); 


View - Application

This is the basic view of our application.
  window.AppView = Backbone.View.extend({ //  ,        HTML  el: $("#todoapp"), //    //      statsTemplate: _.template($('#stats-template').html()), //       ,   events: { "keypress #new-todo": "createOnEnter", "keyup #new-todo": "showTooltip", "click .todo-clear a": "clearCompleted" }, //        : //  , , .     ,   //   localStorage initialize: function() { _.bindAll(this, 'addOne', 'addAll', 'render'); this.input = this.$("#new-todo"); Todos.bind('add', this.addOne); Todos.bind('refresh', this.addAll); Todos.bind('all', this.render); Todos.fetch(); }, //   -  .   . render: function() { var done = Todos.done().length; this.$('#todo-stats').html(this.statsTemplate({ total: Todos.length, done: Todos.done().length, remaining: Todos.remaining().length })); }, //   .      `<ul>` addOne: function(todo) { var view = new TodoView({model: todo}); this.$("#todo-list").append(view.render().el); }, //    addAll: function() { Todos.each(this.addOne); }, //      newAttributes: function() { return { content: this.input.val(), order: Todos.nextOrder(), done: false }; }, //   return      -   . //            createOnEnter: function(e) { if (e.keyCode != 13) return; Todos.create(this.newAttributes()); this.input.val(''); }, //    ,   . clearCompleted: function() { _.each(Todos.done(), function(todo){ todo.clear(); }); return false; }, //      . showTooltip: function(e) { var tooltip = this.$(".ui-tooltip-top"); var val = this.input.val(); tooltip.fadeOut(); if (this.tooltipTimeout) clearTimeout(this.tooltipTimeout); if (val == '' || val == this.input.attr('placeholder')) return; var show = function(){ tooltip.show().fadeIn(); }; this.tooltipTimeout = _.delay(show, 1000); } }); //  -    window.App = new AppView; 

The original with annotations can be found here.

HTML templates and CSS

  <!--  --> <div id="todoapp"> <div class="title"> <h1>Todos</h1> </div> <div class="content"> <div id="create-todo"> <input id="new-todo" placeholder="What needs to be done?" type="text" /> <span class="ui-tooltip-top" style="display:none;">Press Enter to save this task</span> </div> <div id="todos"> <ul id="todo-list"></ul> </div> <div id="todo-stats"></div> </div> </div> <!--  --> <script type="text/template" id="item-template"> <div class="todo <%= done ? 'done' : '' %>"> <div class="display"> <input class="check" type="checkbox" <%= done ? 'checked="checked"' : '' %> /> <div class="todo-content"></div> <span class="todo-destroy"></span> </div> <div class="edit"> <input class="todo-input" type="text" value="" /> </div> </div> </script> <!--  --> <script type="text/template" id="stats-template"> <% if (total) { %> <span class="todo-count"> <span class="number"><%= remaining %></span> <span class="word"><%= remaining == 1 ? 'item' : 'items' %></span> left. </span> <% } %> <% if (done) { %> <span class="todo-clear"> <a href="#"> Clear <span class="number-done"><%= done %></span> completed <span class="word-done"><%= done == 1 ? 'item' : 'items' %></span> </a> </span> <% } %> </script> 

CSS is not attached, it is very long and completely off topic.

Everything, our test application is ready, the result can be found here .

What is useful you can learn from Backbone


Here are a few lessons you can learn from the Backbone application architecture:
Suffice it to say that Backbone is changing the way that interfaces are created, at least for me. If you have questions - ask.

Sources that have been used in writing


Create JavaScript Apps with Backbone.js (Jim Hoskins)
Getting Started with Backbone.js (Siddharth)
Backbone-locaLstorage adapter
Todos Example (Jérôme Gravel Niquet)
Source Todos.js with annotations

What else to read


Backbone GitHub Documentation
Underscore Documentation on GitHub
Node Tutorial Part 19: Backbone.js (Alex Young)
Creating a large Javascript application (original Addy Osmani, translated by trurl123 )

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


All Articles