Backbone.js Patterns: User notification system

BackboneJs User Notification

At Veriday, we have been using Backbone.js to build rich web applications for a few years now. During this period we developed different patterns to make us more efficient building apps using Backbone.js as well as to enforce certain user experience standards across our applications. In this post, we will talk about our “BaseModel” and how we use it to enforce the same user experience when it comes to messages to the end user.

A traditional Backbone.js model will usually extend Backbone.Model:

Person = Backbone.Model.extend({
        initialize: function(){
            alert("Welcome to this world");
        }
    });

The above is fine for learning and experimenting with Backbone.js, however, as your team and codebase grows you need to have a different pattern for all your application’s models.

var User = BaseModel.extend({
     defaults: {
     },
     initialize: function() {
         BaseModel.prototype.initialize.apply(this, arguments);
     }
});

In our applications, we have a model called BaseModel.js which serves exactly this purpose. When we declare a new model we extend our BaseModel. The above example shows how a “User” model is declared in Veriday’s JavaScript applications. Our BaseModel.js would extend the default Backbone.Model.

First, why should you care about this?

Before we elaborate on this, compare today’s web applications with the ones in the early 2000s. There is definitely richer experiences today across a variety of web applications. It wasn’t like that before, and the mere fact that you could do something online like pay your bills was revolutionary enough. Since the launch of Gmail on April 1st 2004, we started to see richer experiences on the web, we started seeing JavaScript toolkits and full blown frameworks to help us develop these rich experiences. This eventually led to the current JavaScript MVC style frameworks we see today from Backbone.js,  Angular.js, Ember.js, Knockout.js and many more. As JavaScript becomes more of a “first-class” citizen on the web you will start to have the need for implementing common design patterns that have, until recently, been the case only on the back-end. The front-end was an thought that got slapped on later and glued together through a myriad of tricks. So, here’s two reasons why you should care:

  1. Eventually the default Backbone.Model will no longer satisfy your needs and you will need to change it. Modifying the Backbone.js source code is not the right answer for that.
  2. Eventually you might have to introduce new behaviour to all your models. Copying and pasting this new behaviour across all your models is not the right answer for that.

How can we implement subclassing in JS?

Javascript’s inheritance model is prototypical and not class based (like Java). We can still achieve something similar through the pattern we will describe here, and some coding conventions that the team understands and most importantly follow. Even though our BaseModel.js could technically be instantiated, we never do that. The convention is that these Base*.js Models (and we have several of them) should never be instantiated, they just get extended by other instantiable models.

var User = BaseModel.extend({
     defaults: {
     },
     initialize: function() {
         BaseModel.prototype.initialize.apply(this, arguments);
     }
});

We accomplish this  through the JavaScript prototype. In the case for the “User” above, the Backbone.js initialize method for the model is responsible for calling the parent’s initialize method. This gives us the appearance of subclassing and inheritance in JavaScript. All of our models’ that initialize methods contain the:

BaseModel.prototype.initialize.apply(this,arguments)

This is so we can inherit behaviour from the BaseModel.

var BaseModel = Backbone.Model.extend({
	defaults: {
	},
	initialize: function() {
	}
});

Another real world advantage of this approach is when we introduced Backbone-Relational into our models.  We only had to modify our BaseModel and extend the RelationalModel instead like this:

var BaseModel = Backbone.RelationalModel.extend({
	defaults: {
	},
	initialize: function() {

	}
});

How to implement a global notification system in Backbone.js

By global, we mean that each developer should never have to worry about implementing this for their component.  Each component should behave the same way in terms of notifications for success and error messages and finally if/when we ever change how our notification system operates we can control that in one place across the application. This place is the BaseModel.js.

Notification

Above is an example of a success message in Digital Agent. Let’s take a look at how this works. In our BaseModel, we attach several listeners to the different Backbone. Events we would like to listen to and take an action on. Today these are:

this.on("error", this.defaultErrorHandler, this);
this.on("invalid", this.defaultValidationErrorHandler, this);
this.on("sync", this.defaultSuccessHandler, this);
this.on("saving", this.defaultPendingHandler, this);
this.on("deleting", this.defaultPendingHandler, this);

If you are familiar with Backbone.js you might be wondering about the saving and deleting events since these are not Backbone.js events. However, because we have our BaseModel in place, we are able to change some of this behaviour. For example, take a look at this snippet from our BaseModel.sync method.

sync: function(method, model, options){

				...
				var xhr = Backbone.sync(method, model, options);
				xhr.method = method;

				...

				if(method == "create" || method == "update"){
					model.trigger('saving', model, xhr, options);
				}

				else if(method == "delete") {
					model.trigger('deleting', model, xhr, options)
				}

				...

				return xhr;
			},

Basically, we overwrite the sync method with our own.  We still call the original Backbone.sync method but now we can do some other things before or after that. In this case, we trigger new events for when Backbone.js is in the process of saving or deleting something. This is more from a user experience perspective so that you can show different messages when models are being saved or deleted. Without this, you will not be able to differentiate between “sync” events which correspond to the model being synced with the server.

this.message = new Message();
...
this.messageView = new MessageView({
     model: this.message
});
...
this.message.on("change:uniqueId", this.messageView.render, this.messageView);

Also, in our BaseModel we make use of our Message view and model. These are responsible for handling messages that are returned by the server, or client side validation, or other error messages. Since we are in BaseModel.js, this.messageView is also available in all sub models for when we have a need to show the user a message.

Let’s look at the defaultSuccessHandler we wired up to the “sync” event above. We check what the method for the AJAX request was, and based on the method we show an appropriate message. Here, you also see that we use a “defaultMessages” object. This object contains some default text, however again, because it is in the BaseModel, another model is able to provide its own messages. Ex. in the BaseModel a successful save would show “Saved”, however, as you can see in the notification image above our Page model, it can provide its own message with more context around the action i.e. a page was saved.

defaultSuccessHandler: function(model, resp, options){
	...		
	//don't show a success message if we were just fetching from the server
	if(options.xhr.method == 'read'){
		return;
	}
	else if(options.xhr.method == 'create' || options.xhr.method == 'update') {
		this.message.set({
			type: 'success',
			text: this.defaultMessages.success
		});
	}
	else if(options.xhr.method == 'delete') {
		this.message.set({
			type: 'success',
			text: this.defaultMessages.deleteSuccess
		});
	}	
},

This works nicely with Backbone js validation as well since by default validation errors will trigger an “invalid” event which we will also listen to. Now we can show validation as well as errors returned from the back-end in the same way throughout the application.

This was a sneak peek into one of our favourite Backbone.js patterns at Veriday. To wrap this post up:

  1. Always extend your own base model instead of the Backbone.Model. Thank us when your code base crosses  30,000 lines of Javascript and you need to make a big change to all your models.
  2. If you need to overwrite Backbone.js behaviour, always do that in your BaseModel, BaseCollection, or BaseView.

Found this blog post useful? Leave us a note below!

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *