18 min read

In this article by Abiee Echamea, author of the book Mastering Backbone.js, you will see that one of the best things about Backbone is the freedom of building applications with the libraries of your choice, no batteries included. Backbone is not a framework but a library. Building applications with it can be challenging as no structure is provided. The developer is responsible for code organization and how to wire the pieces of code across the application; it’s a big responsibility. Bad decisions about code organization can lead to buggy and unmaintainable applications that nobody wants to see.

In this article, you will learn the following topics:

  • Delegating the right responsibilities to Backbone objects
  • Splitting the application into small and maintainable scripts

(For more resources related to this topic, see here.)

The big picture

We can split application into two big logical parts. The first is an infrastructure part or root application, which is responsible for providing common components and utilities to the whole system. It has handlers to show error messages, activate menu items, manage breadcrumbs, and so on. It also owns common views such as dialog layouts or loading the progress bar.

A root application is responsible for providing common components and utilities to the whole system.

A root application is the main entry point to the system. It bootstraps the common objects, sets the global configuration, instantiates routers, attaches general services to a global application, renders the main application layout at the body element, sets up third-party plugins, starts a Backbone history, and instantiates, renders, and initializes components such as a header or breadcrumb.

However, the root application itself does nothing; it is just the infrastructure to provide services to the other parts that we can call subapplications or modules. Subapplications are small applications that run business value code. It’s where the real work happens.

Subapplications are focused on a specific domain area, for example, invoices, mailboxes, or chats, and should be decoupled from the other applications. Each subapplication has its own router, entities, and views. To decouple subapplications from the root application, communication is made through a message bus implemented with the Backbone.Events or Backbone.Radio plugin such that services are requested to the application by triggering events instead of call methods on an object.

Subapplications are focused on a specific domain area and should be decoupled from the root application and other subapplications.

Figure 1.1 shows a component diagram of the application. As you can see, the root application depends on the routers of the subapplications due to the Backbone.history requirement to instantiate all the routers before calling the start method and the root application does this. Once Backbone.history is started, the browser’s URL is processed and a route handler in a subapplication is triggered; this is the entry point for subapplications. Additionally, a default route can be defined in the root application for any route that is not handled on the subapplications.

Mastering Backbone.js

Figure 1.1: Logical organization of a Backbone application

When you build Backbone applications in this way, you know exactly which object has the responsibility, so debugging and improving the application is easier. Remember, divide and conquer. Also by doing this, you make your code more testable, improving its robustness.

Responsibilities of the Backbone objects

One of the biggest issues with the Backbone documentation is no clues about how to use its objects. Developers should figure out the responsibilities for each object across the application but at least you have some experience working with Backbone already and this is not an easy task.

The next sections will describe the best uses for each Backbone object. In this way, you will have a clearer idea about the scope of responsibilities of Backbone, and this will be the starting point of designing our application architecture. Keep in mind, Backbone is a library with foundation objects, so you will need to bring your own objects and structure to make an awesome Backbone application.

Models

This is the place where the general business logic lives. A specific business logic should be placed on other sites. A general business logic is all the rules that are so general that they can be used on multiple use cases, while specific business logic is a use case itself. Let’s imagine a shopping cart. A model can be an item in the cart. The logic behind this model can include calculating the total by multiplying the unit price by the quantity or setting a new quantity.

In this scenario, assume that the shop has a business rule that a customer can buy the same product only three times. This is a specific business rule because it is specific for this business, or how many stores do you know with this rule? These business rules take place on other sites and should be avoided on models.

Also, it’s a good idea to validate the model data before sending requests to the server. Backbone helps us with the validate method for this, so it’s reasonable to put validation logic here too.

Models often synchronize the data with the server, so direct calls to servers such as AJAX calls should be encapsulated at the model level. Models are the most basic pieces of information and logic; keep this in mind.

Collections

Consider collections as data repositories similar to a database. Collections are often used to fetch the data from the server and render its contents as lists or tables. It’s not usual to see business logic here.

Resource servers have different ways to deal with lists of resources. For instance, while some servers accept a skip parameter for pagination, others have a page parameter for the same purpose. Another case is responses; a server can respond with a plain array while other prefer sending an object with a data, list, or some other key, where an array of objects is placed. There is no standard way.

Collections can deal with these issues, making server requests transparent for the rest of the application.

Views

Views have the responsibility of handling Document Object Model (DOM). Views work closely with the template engines rendering the templates and putting the results in DOM. Listen for low-level events using a jQuery API and transform them into domain ones.

Views abstract the user interactions transforming his/her actions into data structures for the application, for example, clicking on a save button in a form view will create a plain object with the information in the input and trigger a domain event such as save:contact with this object attached. Then a domain-specific object can apply domain logic to the data and show a result.

Business logic on views should be avoided, but basic form validations are allowed, such as accepting only numbers, but complex validations should be done on the model.

Routers

Routers have a simple responsibility: listening for URL changes on the browser and transforming them into a call to a handler. A router knows which handler to call for a given URL and also decodes the URL parameters and passes them to the handlers. The root application bootstraps the infrastructure, but routers decide which subapplication will be executed. In this way, routers are a kind of entry point.

Domain objects

It is possible to develop Backbone applications using only the Backbone objects described in the previous section, but for a medium-to-large application, it’s not sufficient. We need to introduce a new kind of object with well-delimited responsibilities that use and coordinate the Backbone foundation objects.

Subapplication facade

This object is the public interface of a subapplication. Any interaction with the subapplication should be done through its methods. Direct calls to internal objects of the subapplication are discouraged. Typically, methods on this controller are called from the router but can be called from anywhere.

The main responsibility of this object is simplifying the subapplication internals, so its work is to fetch the data from the server through models or collections and in case an error occurs during the process, it has to show an error message to the user. Once the data is loaded in a model or collection, it creates a subapplication controller that knows the views that should be rendered and has the handlers to deal with its events.

The subapplication facade will transform the URL request into a Backbone data object. It shows the right error message; creates a subapplication controller, and delegates the control to it.

The subapplication controller or mediator

This object acts as an air traffic controller for the views, models, and collections. With a Backbone data object, it will instantiate and render the appropriate views and then coordinate them. However, the coordination task is not easy in complex layouts.

Due to loose coupling reasons, a view cannot call the methods or events of the other views directly. Instead of this, a view triggers an event and the controller handles the event and orchestrates the view’s behavior, if necessary. Note how the views are isolated, handling just their owned portion of DOM and triggering events when they need to communicate something.

Business logic for simple use cases can be implemented here, but for more complex interactions, another strategy is needed. This object implements the mediator pattern allowing other basic objects such as views and models to keep it simple and allow loose coupling.

The logic workflow

The application starts bootstrapping common components and then initializes all the routers available for the subapplications and starts Backbone.history. See Figure 1.2, After initialization, the URL on the browser will trigger a route for a subapplication, then a route handler instantiates a subapplication facade object and calls the method that knows how to handle the request. The facade will create a Backbone data object, such as a collection, and fetch the data from the server calling its fetch method. If an error is issued while fetching the data, the subapplication facade will ask the root application to show the error, for example, a 500 Internal Server Error.

Mastering Backbone.js

Figure 1.2: Abstract architecture for subapplications

Once the data is in a model or collection, the subapplication facade will instantiate the subapplication object that knows the business rules for the use case and pass the model or collection to it. Then, it renders one or more view with the information of the model or collection and places the results in the DOM. The views will listen for DOM events, for example, click, and transform them into a higher-level event to be consumed by the application object.

The subapplication object listens for events on models and views and coordinates them when an event is triggered. When the business rules are not too complex, they can be implemented on this application object, such as deleting a model. Models and views can be in sync with the Backbone events or use a library for bindings such as Backbone.Stickit.

In the next section, we will describe this process step by step with code examples for a better understanding of the concepts explained.

Route handling

The entry point for a subapplication is given by its routes, which ideally share the same namespace. For instance, a contacts subapplication can have these routes:

  • contacts: Lists all the available contacts
  • Contacts/page/:page: Paginates the contacts collection
  • contacts/new: Shows a form to create a new contact
  • contacts/view/:id: Shows an invoice given its ID
  • contacts/edit/:id: Shows a form to edit a contact

Note how all the routes start with the /contacts prefix. It’s a good practice to use the same prefix for all the subapplication routes. In this way, the user will know where he/she is in the application, and you will have a clean separation of responsibilities.

Use the same prefix for all URLs in one subapplication; avoid mixing routes with the other subapplications.

When the user points the browser to one of these routes, a route handler is triggered. The function handler parses the URL request and delegates the request to the subapplication object, as follows:

var ContactsRouter = Backbone.Router.extend({
  routes: {
    "contacts": "showContactList",
    "contacts/page/:page": "showContactList",
    "contacts/new": "createContact",
    "contacts/view/:id": "showContact",
    "contacts/edit/:id": "editContact"
  },

  showContactList: function(page) {
    page = page || 1;
    page = page > 0 ? page : 1;

    var region = new Region({el: '#main'});
    var app = new ContactsApp({region: region});
    app.showContactList(page);
  },

  createContact: function() {
    var region = new Region({el: '#main'});
    var app = new ContactsApp({region: region});
    app.showNewContactForm();
  },

  showContact: function(contactId) {
    var region = new Region({el: '#main'});
    var app = new ContactsApp({region: region});
    app.showContactById(contactId);
  },

  editContact: function(contactId) {
    var region = new Region({el: '#main'});
    var app = new ContactsApp({region: region});
    app.showContactEditorById(contactId);
  }
});

The validation of the URL parameters should be done on the router as shown in the showContactList method. Once the validation is done, ContactsRouter instantiates an application object, ContactsApp, which is a facade for the Contacts subapplication; finally, ContactsRouter calls an API method to handle the user request.

The router doesn’t know anything about business logic; it just knows how to decode the URL requests and which object to call in order to handle the request.

Here, the region object points to an existing DOM node by passing the application and tells us where the application should be rendered.

The subapplication facade

A subapplication is composed of smaller pieces that handle specific use cases. In the case of the contacts app, a use case can be see a contact, create a new contact, or edit a contact. The implementation of these use cases is separated on different objects that handle views, events, and business logic for a specific use case.

The facade basically fetches the data from the server, handles the connection errors, and creates the objects needed for the use case, as shown here:

function ContactsApp(options) {
  this.region = options.region;

  this.showContactList = function(page) {
    App.trigger("loading:start");

    new ContactCollection().fetch({
      success: _.bind(function(collection, response, options) {
        this._showList(collection);
        App.trigger("loading:stop");
      }, this),
      fail: function(collection, response, options) {
        App.trigger("loading:stop");
        App.trigger("server:error", response);
      }
    });
  };

  this._showList = function(contacts) {
    var contactList = new ContactList({region: this.region});
    contactList.showList(contacts);
  }

  this.showNewContactForm = function() {
    this._showEditor(new Contact());
  };

  this.showContactEditorById = function(contactId) {
    new Contact({id: contactId}).fetch({
      success: _.bind(function(model, response, options) {
        this._showEditor(model);
        App.trigger("loading:stop");
      }, this),
      fail: function(collection, response, options) {
        App.trigger("loading:stop");
        App.trigger("server:error", response);
      }
    });
  };

  this._showEditor = function(contact) {
    var contactEditor = new ContactEditor({region: this.region});
    contactEditor.showEditor(contact);
  }

  this.showContactById = function(contactId) {
    new Contact({id: contactId}).fetch({
      success: _.bind(function(model, response, options) {
        this._showViewer(model);
        App.trigger("loading:stop");
      }, this),
      fail: function(collection, response, options) {
        App.trigger("loading:stop");
        App.trigger("server:error", response);
      }
    });
  };

  this._showViewer = function(contact) {
    var contactViewer = new ContactViewer({region: this.region});
    contactViewer.showContact(contact);
  }
}

The simplest handler is showNewContactForm, which is called when the user wants to create a new contact. This creates a new Contact object and passes to the _showEditor method, which will render an editor for a blank Contact. The handler doesn’t need to know how to do this because the ContactEditor application will do the job.

Other handlers follow the same pattern, triggering an event for the root application to show a loading widget to the user while fetching the data from the server. Once the server responds successfully, it calls another method to handle the result. If an error occurs during the operation, it triggers an event to the root application to show a friendly error to the user.

Handlers receive an object and create an application object that renders a set of views and handles the user interactions. The object created will respond to the action of the users, that is, let’s imagine the object handling a form to save a contact. When users click on the save button, it will handle the save process and maybe show a message such as Are you sure want to save the changes and take the right action?

The subapplication mediator

The responsibility of the subapplication mediator object is to render the required layout and views to be showed to the user. It knows which views need to be rendered and in which order, so instantiate the views with the models if needed and put the results on the DOM.

After rendering the necessary views, it will listen for user interactions as Backbone events triggered from the views; methods on the object will handle the interaction as described in the use cases.

The mediator pattern is applied to this object to coordinate efforts between the views. For example, imagine that we have a form with contact data. As the user made some input in the edition form, other views will render a preview business card for the contact; in this case, the form view will trigger changes to the application object and the application object will tell the business card view to use a new set of data each time. As you can see, the views are decoupled and this is the objective of the application object. The following snippet shows the application that shows a list of contacts. It creates a ContactListView view, which knows how to render a collection of contacts and pass the contacts collection to be rendered:

var ContactList = function(options) {
  _.extend(this, Backbone.Events);
  this.region = options.region;

  this.showList = function(contacts) {
    var contactList = new ContactListView({
      collection: contacts
    });
    this.region.show(contactList);

    this.listenTo(contactList,
                  "item:contact:delete",
                  this._deleteContact);
  }

  this._deleteContact = function(contact) {
    if (confirm('Are you sure?')) {
      contact.collection.remove(contact);
    }
  }

  this.close = function() {
    this.stopListening();
  }
}

The ContactListView view will be responsible for transforming this into the DOM nodes and responding to collection events such as adding a new contact or removing one. Once the view is initialized, it is rendered on a specific region previously specified. When the view is finally on DOM, the application listens for the “item:contact:delete” event, which will be triggered if the user clicks on a delete button rendered for each contact.

To see a contact, a ContactViewer application is responsible for managing the use case, which is as follows:

var ContactViewer = function(options) {
  _.extend(this, Backbone.Events);
  this.region = options.region;

  this.showContact = function(contact) {
    var contactView = new ContactView({model: contact});
    this.region.show(contactView);
    this.listenTo(contactView,
                 "contact:delete",
                 this._deleteContact);
  },
  
  this._deleteContact = function(contact) {
    if (confirm("Are you sure?")) {
      contact.destroy({
        success: function() {
          App.router.navigate("/contacts", true);
        },
        error: function() {
          alert("Something goes wrong");
        }
      });
    }
  }
}

It’s the same situation, that is, the contact list creates a view that manages the DOM interactions, renders on the specified region, and listens for events. From the details view of a contact, users can delete them. Similar to a list, a _deleteContact method handles the event, but the difference is when a contact is deleted, the application is redirected to the list of contacts, which is the expected behavior. You can see how the handler uses the root application infrastructure by calling the navigate method of the global App.router.

The handler forms to create or edit contacts are very similar, so the same ContactEditor can be used for both the cases. This object will show a form to the user and will wait for the save action, as shown in the following code:

var ContactEditor = function(options) {
  _.extend(this, Backbone.Events)
  this.region = options.region;

  this.showEditor = function(contact) {
    var contactForm = new ContactForm({model: contact});
    this.region.show(contactForm);
    
    this.listenTo(contactForm,
                  "contact:save",
                  this._saveContact);
  },

  this._saveContact = function(contact) {
    contact.save({
      success: function() {
        alert("Successfully saved");
        App.router.navigate("/contacts");
      },
      error: function() {
        alert("Something goes wrong");
      }
    });
  }
}

In this case, the model can have modifications in its data. In simple layouts, the views and model can work nicely with the model-view data bindings, so no extra code is needed. In this case, we will assume that the model is updated as the user puts in information in the form, for example, Backbone.Stickit. When the save button is clicked, a “contact:save” event is triggered and the application responds with the _saveContact method. See how the method issues a save call to the standard Backbone model and waits for the result. In successful requests, a message will be displayed and the user is redirected to the contact list. In errors, a message will tell the user that the application found a problem while saving the contact.

The implementation details about the views are outside of the scope of this article, but you can abstract the work made by this object by seeing the snippets in this section.

Summary

In this article, we started by describing in a general way how a Backbone application works. It describes two main parts, a root application and subapplications. A root application provides common infrastructure to the other smaller and focused applications that we call subapplications.

Subapplications are loose-coupled with the other subapplications and should own resources such as views, controllers, routers, and so on. A subapplication manages a small part of the system and no more. Communication between the subapplications and root application is made through an event-driven bus, such as Backbone.Events or Backbone.Radio.

The user interacts with the application using views that a subapplication renders. A subapplication mediator orchestrates interaction between the views, models, and collections. It also handles the business logic such as saving or deleting a resource.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here