Build A Web Application with Backbone by Listening for Events

In this post you’ll learn about a built-in feature of Backbone called Events. To provide a basic understanding, let’s take a brief look on models, views and other concepts and then get started on the work with events. I’m excited to show you how mastering events is one of the quickest ways to become more productive with Backbone.

My goal is to keep this as simple as possible. The concepts that you’ll learn here are a great way to reduce coupling. In addition, it’s a helpful approach for building extensible, flexible, and maintainable code.

We’ll be building the following:

pubsub-backbone-events

Overview

Before walking through any code, let’s do a quick overview of two other subjects covered in this article. If you’re already comfortable with DBaaS and Graph concepts, feel free to skip the next two sections.

Database as a Service (DBaaS)

DBaaS is a cloud service that offers the option to use a database through a service API, without physically launching a virtual machine instance. In this configuration, application owners do not have to install and maintain the database on their own. Instead, the database service provider takes responsibility to handle your database backend and do the dirty work of database management for you.

DBaaS uses a REST (representational state transfer) API that can be used from any platform. The REST API basically acts as a two-way street to get data in and out.

Graph Database

Several popular databases can be classified as below:

Relational (SQL) Databases

  • SQLite
  • PostgreSQL

NoSQL Databases

Graph Database is built on top of a graph schema, where objects are called Vertices (a graph term for entities), and the links (relationships) between the objects are called Edges.

We can not actually compare these concepts with other kinds of databases, but it helps to understand if we compare the usage practices. For instance, SQL has concepts for table, row, column, relation and MongoDB has collection, Document, and fields, here are the key concepts:

  • Vertex — is similar to a Document in MongoDB, it stores data.
  • Namespace — is similar to a Collection in MongoDB or type in ElasticSearch. A vertex always belongs to a namespace and security rules can be applied on namespaces.
  • Edge — is a directional link to another vertex.

Backbone

Backbone is a library easy to use with structure to build single-page applications by providing models, views and custom events and connects it all to your existing service API. The single most important thing that Backbone can help you with is keeping your business logic separate from your user interface.

Let’s dive into Backbone and see how this flexible library can bring order to your Javascript code.

Models

Backbone models contain data for an application as well as the logic around this data. For example, we can use a model to represent the concept of a node in a graph including its attributes like color and label. Models can be created by extending Backbone.Model as follows:


var MyBackboneModel = Backbone.Model.extend({
  initialize: function( options ) {
    this.on('change:id',    this.notifyEachIdChange,    this);
    this.on('change:color', this.notifyEachColorChange, this);
    this.on('change:label', this.notifyEachLabelChange, this);
  },
  notifyEachIdChange: function() {
    console.log( 'id was changed' );
  },
  notifyEachColorChange: function() {
    console.log( 'color was changed' );
  },
  notifyEachLabelChange: function() {
    console.log( 'label was changed' );
  }
});

Views

Backbone views don’t contain HTML markup, they contain the logic behind the presentation of the model’s data to the user. The general idea is to organize the user interface into logical views, backed by models. To create a view is relatively straightforward and similar to creating new models. To create a new View, simply extend Backbone.View as follows:


var MyBackboneView = Backbone.View.extend({
  el: 'body',
  events: {
    'click button.add-node': 'addNode',
    'click #editNodeModal button.btn.btn-primary': 'editNode',
    'change #textNode': 'textChanged',
    'changeColor #textColorNode': 'colorChanged'
  },
  initialize: function( options ) {
    this.model = new MyBackBoneModel();
  },
  textChanged: function() {
    this.model.set( 'label', 'new label' );
  },
  colorChanged: function() {
    this.model.set( 'color', '#333333' );
  },
  editNode: function() {
    // edit the selected node
  },
  addNode: function() {
    // add new node
  }
});

Backbone Events

Events in Backbone is a module that can be mixed in to any object, giving the object the ability to bind and trigger custom named events. It gives us a feature that provides us to avoid coupling. Coupling is when a group of code is highly dependent on one another. Which means if a piece of code changes, then it’s needed to update everything that uses this piece of code.

In the following example you’ll see a pattern that has synchronization decoupling. It uses an event system like the way a radio works: a radio station broadcasts (publishes) and anyone can listen (subscribes). And rather than talking to other objects directly, you can publish messages on a shared radio station. This event system allows to define events that can pass arguments containing values needed by the subscriber. Backbone makes this event system implementation pretty straightforward, to do that just mix Backbone.Events into an empty object like so:


var EventChannel = _.extend({}, Backbone.Events);

Then you can just use the standard trigger and on methods to publish and subscribe to messages. See an implementation below:


var channel = $.extend( {}, Backbone.Events );
channel.on('remove-node', function(msg) {
   // code to remove the node
});
channel.trigger( 'remove-node', msg );
// 'msg' can be everything: String, number, object and so forth
// also we can pass more than one message like the example below
channel.on('add-node', function(node, callback) {
  // code to add a new node
  callback();
} );
channel.trigger('add-node', {
  label: 'I am a new node',
  color: 'black'
}, function() {
  console.log( 'I am a callback' );
});

The goal is to avoid dependencies between modules. Each modules can have a channel like a radio station firing the events (the publishers) and any other modules can listen to their events wishing to receive notifications (subscribers).

A Quick Example

This example uses two modules:

  1. One for drawing directed
  2. Another for storing and fetching data from a DBaaS

Both modules work as a radio station. The first module uses a graph visualization tool, named Force Editor, and it enables the user to position the nodes of a graph in two-dimensional space in a simple, intuitive, and aesthetically pleasing way. The second module uses AppBase as a realtime database. Take a look on the images below to see which events are being emitted from each modules:

DBaaS radio station

graph visualization radio station

You can see that none of these objects know about any one of the others. Their behavior can all be considered in isolation. If they do the right thing given the events firing, the system will work as a whole.

Using this implementation we can easily make a view completely unaware of existing modules. It just publish an event with a new graph update, and another different module is responsible for updating the graph from it. Also, if the graph visualization needs to be updated, that is broadcast from DBaaS module, it’s responsible to communicate to ForceView about new updates.

The view is a mediator between the ForceView and DBaaS. Doing that means decomposing everything and making it usable in small pieces and then making those small pieces work beautifully together.

So if you want to customize it just a bit to suit your particular taste, you can easily pick up any module and change it any way you like. For instance, you can replace the graph visualization by other graph tools like jqPlot, Dracula, ArborJS, sigmajs, RaphaelJS and so forth.

Or you can use any other DBaaS like Neo4j, TitanDB, etc. The good news, you just need to change a single file in order to migrate to another library. The image below illustrates the interaction between the View and these two modules.

backbone view

Feel free to drop a line right below, if you wanna see a new blog post covering any other libraries mentioned above. I’ll be glad to hear from everybody.

Implementation

Notice we don’t know how the detail on how the graph tool will be updated, or even when the graph will have to be updated, only that it will happen. As you can see, this event system is coupled to the name and data payload of a message. The only dependency is on each radio channel that has a very small API, you can send or receive messages. What matters is which message the channel is triggering, then you just need to ensure your system reacts to the vocabulary of events correctly.

users see update on realtime

For instance, the ForceView channel always triggers an event for each change made by the user. On another hand, ForceView also listens for changes that comes from other users remotely. See below a code snapshot for publishing a message and another for subscribing for messages:


function mouseup() {
  // ...
  ForceViewEventChannel.trigger('node-and-link-added', {
    node: node,
    link: link
  });
  // ...
  redraw();
  // ...
}
/** more code here **/
ForceViewEventChannel.on('add-node', function(node) {
  var selected_node = searchNode(node);
  if (!selected_node) {
    nodes.push(node);
    redraw();
  }
});

ForceView only handles things regarding the graph visualization, once it needs to deal with different matter, it publishes and subscribes for messages. The DBaaS module handles things regarding to storing and fetching data. The code below shows how DBaaS module listens for messages and how it sets up listeners.


retrieveEdges = function() {
  config.streamingClient.streamSearch({
    type: config.type.edge,
    body: {
      query: {
        match_all: {}
      }
    }
  }).on('data', function(res, err) { /* ... */ });
};
// ...
retrieveVertex = function(callback) {
  config.streamingClient.streamSearch({
    type: config.type.vertex,
    body: {
      query: {
        match_all: {}
      }
    }
  }).on('data', function(res, err) {
    _.each(res.hits && res.hits.hits, function(node) {
      Channel.trigger( 'node-added', $.extend( node._source || {}, {
        id: node._id
      } ) );
    });
    callback && callback();
  });
};
// ...
Channel.on('retrieve-all-nodes', function() {
  retrieveVertex( retrieveEdges );
});
// ...
Channel.on('add-node', function( node, cb ) {
  config.client.index({
    index: config.appname,
    type: config.type.vertex,
    body: node
  }).then(function(response) {
    node.id = response._id;
    if (cb) {
      cb(node);
    } else {
      Channel.trigger( 'node-added', node );
    }
  }, function(error) {
    console.error(error);
  });
});

You have seen how these two modules work individually so far. Lets take a look on them working together. To do that the view uses a new channel to mediate these modules.


define(['...'], function($, Backbone, ForceView) {
  Backbone.View.extend({
    initialize: function( options ) {
      var view = this;
      ForceView.trigger('init', function() {
        view.sync();
      });
    },
    sync: function() {
      var view = this;
        view.mediatorChannel = $.extend( {}, Backbone.Events );
      view.syncWithForceView();
      view.syncWithDBaaS();
    }
  });
});

syncWithDBaaS function


syncWithDBaaS: function() {
  var view = this;
  require(['dbaas'], function(dbaas) {
    view.mediatorChannel.on('add-node', function(node, cb) {
      dbaas.trigger( 'add-node', node || {}, cb );
    } );
    dbaas.on( 'node-added', function(node) {
      ForceView.trigger('add-node', node );
      ForceView.trigger('remove-node', {});
    } );
    dbaas.trigger('retrieve-all-nodes');
  });
}

syncWithForceView function


syncWithForceView: function() {
  var view = this;
  ForceView.on('node-and-link-added', function(data) {
    view.mediatorChannel.trigger( 'add-node', data.node, function( node ) {
      data.node.id = node.id;
      ForceView.trigger('link-added', data.link);
    } );
  });
}

Regardless of the language or framework, tightly coupled systems are a common problem in modern web applications. We, as developers, should be able to create modular and reusable code. As you learned this pattern plays an important role in creating better applications, loosely coupled, flexible and scalable.

Running the Example Locally

The entire code is available on Github, just clone the repository git clone git@github.com:igorlima/pubsub-with-backbone-events.git (or download the code), execute npm install from a console to install all dependencies, then execute node server.js to start the application! Head to http://localhost:5000 to see the application in action!

NOTE: you are using my application: pubsub_with_backbone_events. Appbase is completely free up to 100 thousand API calls per month. Feel free to use mine while you’re learning. After that, create your own application’s name, then new learners can use my API calls remaining. Thanks.

What’s next?

If you wanna go further, go ahead and learn how to deploy this example on Heroku by following the steps described on this amazing article. Thanks for reading! See you in the next blog post.

Igor Ribeiro Lima

A lucky guy who loves his wife, family, friends and coding. Enjoy learning new things, working on personal projects and contributing on open source.