Replacing jQuery enrichment with Backbone on a server-side rendered website.

My task was adding a newsletter signup form to a website. Simple enough task. The site is rendered on the server-side using Node.js, Express and Handlebars. Performing a full page refresh to post a bit of data on a single-page marketing site seemed a bit cludgey! So off to the races we go with a bit of AJAXy enrichment.

For starters, the server has the following endpoint for performing the newsletter subscription:

POST /api/mail  

and is expecting a JSON payload:

{
  "email": "email@test.com",
  "firstname": "Brian",
  "lastname": "Mancini",
  "lists" [
    "eab123",
    "fea343"
  ]
}

The markup on the client side looks like this:

<div class="row">  
  <div class="col-lg-6 col-lg-offset-3 text-center">
    <h4>Newsletter Signup</h4>
    <div class="signup-form-notifications"></div>
    <form class="signup-form form-horizontal">
      <div class="form-group">
        <label for="lastname" class="col-md-12">Name</label>            
        <div class="col-md-6 sm-pad-right">
          <input type="text" class="form-control" id="firstname" name="firstname" placeholder="First name">          
        </div>
        <div class="col-md-6 sm-pad-left">
          <input type="text" class="form-control" id="lastname" name="lastname" placeholder="Last name">
        </div>
      </div>  
      <div class="form-group">
        <label for="email" class="col-md-12">Email</label>
        <div class="col-md-12">
          <input type="email" class="form-control" id="email" name="email" placeholder="Email address">      
        </div>
      </div>  
      <div class="form-group">    
        <label class="col-md-12">Subscribe to:</label>
        <div class="col-md-12">
          <input type="checkbox" checked="checked" id="list-eab123" class="list" name="lists" value="eab123">
          <label for="list-eab123">New Comics and Previews</label>
        </div>
        <div class="col-md-12">
          <input type="checkbox" checked="checked" id="list-2fc7e5" class="list" name="lists" value="2fc7e5">
          <label for="list-2fc7e5">Store News</label>
        </div>
      </div>
      <div class="form-group">
        <div class="col-md-12">
          <input type="submit" class="btn btn-primary pull-right submit" value="SIGN UP">
        </div>
      </div>
    </form>      
  </div>        
</div>  

In the first, quick, get-it-working version I resorted to doing some simple jQuery coding. It's been a while since I've used jQuery directly, so it was a fun exercise to see what has changed in the library. Some of the interfaces have been polished, but in general it's stayed the same. After getting things working however, there were some things that I just don't like.

Having been working with ExtJS, Angular, and Backbone for the last few years, the traditional jQuery code soup leaves a bit to be desired.

The biggest thing I noticed is that the AJAX library is not designed for working with JSON entities. It required a number of attempts to get JSON serialization working correctly, blame that on my rusty jQuery as well.

Here's what the first pass looked like:

(function() {
  'use strict';

  $(function init() {
    $('body')      
      .on('click', '.signup-form input[type="submit"]', subscribeClick);
  });

  function subscribeClick(e) {
    e.preventDefault();

    var $form = $('.signup-form')
      , data
      ; 

    data = {
      firstname: $form.find('#firstname').val(),
      lastname: $form.find('#lastname').val(),
      email: $form.find('#email').val(),
      lists: $form.find('.list:checked').map(function(idx, ele) {
        return $(ele).val();
      }).get()
    };

    $.ajax({
        url: '/api/mail',
        method: 'post',
        dataType: 'json',
        data: JSON.stringify(data),  
        contentType: 'application/json; charset=utf-8'      
      })
      .then(subscribeSuccess, subscribeFailure);
  }

  function subscribeSuccess(data) {
    var $container = $('.signup-form-notifications')
      , $form = $('.signup-form')
      ;
    $form.trigger('reset');
    $container.html('<div class="alert alert-success" role="alert">You have been successfully subscribed. Thanks!</div>');
  }

  function subscribeFailure(data) {
    var $container = $('.signup-form-notifications');
    $container.html('<div class="alert alert-danger" role="alert">You were not subscribed. Ensure all information is filled in correctly and try again.</div>') ;
  }
}());

The above code is pretty straightforward. We're essentially plucking a few values from the form and submitting a POST request. Then we display a response.

However, haven't been in the SPA world for a while, the above approach seemed... poor.

Converting to Backbone

Switching to a full-on SPA framework like Angular, Ember, or ExtJS is a heavy handed approach. Those frameworks are designed for working with the entire stack. In this case, because everything is rendered for us, I was just looking for a better way to interact with forms and submit data to a JSON endpoing. Basically this boils down to an entity model.

Backbone seemed like a good fit. It's light weight and doesn't really care what or how we render things. It also has a Model concept that is perfect for what we need.

We'll allow the server to render the form HTML and use Backbone to sort out the eventing and server interaction.

To start off, I simply created a Model object that maps to the server endpoint:

var Subscriber = Backbone.Model.extend({  
  urlRoot: '/api/mail'        
});

Next I needed to wrap the form in a Backbone View so that we can use Backbone's eventing to track the click event.

var Signup = Backbone.View.extend({  
  el: '.signup-form',        
  events: {
    'click .submit': 'subscribe'
  },
  render: function() {
    return this;
  },
  subscribe: function subscribe(e) {
    e.preventDefault();                      

    var subscriber = new Subscriber({ 
      firstname: this.$el.find('#firstname').val(),
      lastname: this.$el.find('#lastname').val(),
      email: this.$el.find('#email').val(),
      lists: this.$el.find('.list:checked').map(function(idx, ele) {
        return $(ele).val();
      }).get()
    });

    subscriber.save();
  }
});

You'll see that the form uses the jQuery selector for the .signup-form to attach itself to that element when render is called.

We have to create a render method for our view even though it's not going to do anything.

Finally, this binds the click event for the submit button to the subscribe method. You'll notice this looks extremely similar to the jQuery method. Functionally it is. Backbone doesn't have the concept of two-way databinding, so we're forced to pluck the values from the form the old school way.

Now keen observers may have noticed that we do not have callback handling. That's because we're going to create another view for displaying notification info. We'll handle the rendering of that view at an application layer.

Lets refactor a bit and create a few application "controller" methods to handle startup and the server interaction that occur outside the purview of a single view.

var app = {  
  start: function() {
    this.signupView = new Signup();
    this.notificationView = new Notification();
    this.signupView.render();
  },
  subscribe: function(subscriber) {
    subscriber.save(null, { success: this.subscribeSuccess, error: this.subscribeFailure });
  },
  subscribeSuccess: function() {
    app.signupView.clear();
    app.notificationView.render(true);            
  },  
  subscribeFailure: function() {
    app.notificationView.render(false);
  }
};

The start method gets called when we start the application and it initialize the two views.

The subscribe method will perform the data persistance and then handle the interaction between the two views.

We then change the existing Signup view to use the new application subscribe method to perform the server interaction:

var Signup = Backbone.View.extend({  
  el: '.signup-form',        
  events: {
    'click .submit': 'subscribe'
  },
  render: function() {
    return this;
  },
  subscribe: function subscribe(e) {
    e.preventDefault();                      

    var subscriber = new Subscriber({ 
      firstname: this.$el.find('#firstname').val(),
      lastname: this.$el.find('#lastname').val(),
      email: this.$el.find('#email').val(),
      lists: this.$el.find('.list:checked').map(function(idx, ele) {
        return $(ele).val();
      }).get()
    });

    app.subscribe(subscriber);                        
  },
  clear: function() {
    this.$el.trigger('reset');
  }
});

We also create a new view for rendering the notification information:

var Notification = Backbone.View.extend({  
  el: '.signup-form-notifications',        
  successTemplate: '<div class="alert alert-success" role="alert">You have been successfully subscribed. Thanks!</div>',
  errorTemplate:   '<div class="alert alert-danger" role="alert">You were not subscribed. Ensure all information is filled in correctly and try again.</div>',
  render: function(status) {
    this.$el.html(status ? this.successTemplate : this.errorTemplate);
    return this;
  }
});   

Finally, a little restructure to create one nice package for our code. We end up with the final product looking like this:

(function() {
  'use strict';  

  var app = {

    // CONTROLLER METHODS
    start: function() {
      this.signupView = new this.views.Signup();
      this.notificationView = new this.views.Notification();
      this.signupView.render();
    },
    subscribe: function(subscriber) {
      subscriber.save(null, { success: this.subscribeSuccess, error: this.subscribeFailure });
    },
    subscribeSuccess: function() {
      app.signupView.clear();
      app.notificationView.render(true);            
    },  
    subscribeFailure: function() {
      app.notificationView.render(false);
    },

    // MODELS
    models: {
      Subscriber: Backbone.Model.extend({
        urlRoot: '/api/mail'        
      })
    },

    // VIEWS
    views: {

      Notification: Backbone.View.extend({ 
        el: '.signup-form-notifications',        
        successTemplate: '<div class="alert alert-success" role="alert">You have been successfully subscribed. Thanks!</div>',
        errorTemplate:   '<div class="alert alert-danger" role="alert">You were not subscribed. Ensure all information is filled in correctly and try again.</div>',
        render: function(status) {
          this.$el.html(status ? this.successTemplate : this.errorTemplate);
          return this;
        }
      }),          

      Signup: Backbone.View.extend({
        el: '.signup-form',        
        events: {
          'click .submit': 'subscribe'
        },
        render: function() {
          return this;
        },
        subscribe: function subscribe(e) {
          e.preventDefault();                      

          var subscriber = new app.models.Subscriber({ 
            firstname: this.$el.find('#firstname').val(),
            lastname: this.$el.find('#lastname').val(),
            email: this.$el.find('#email').val(),
            lists: this.$el.find('.list:checked').map(function(idx, ele) {
              return $(ele).val();
            }).get()
          });

          app.subscribe(subscriber);                        
        },
        clear: function() {
          this.$el.trigger('reset');
        }
      })
    }
  };  

  $(function init() {
    app.start();
  });

}());

Afterthoughts

In comparing the two bits of code, the jQuery code is a good bit shorter (994 characters versus 1419 characters). However, the Backbone code gives us a pipeline for client-side validation and better capabilities for rendering custom messages. That combined with the separation of concerns and code organization make the Backbone solution more appealing to me.

For me, this was an interesting exercise. It's been a while since I had had the chance to compare different client-side techniques on the same code. What do you think?

comments powered by Disqus