MVC meet JavaScript, Javascript meet MVC

Brian Sam-Bodden
  • December 2011
  • jQuery
  • JavaScript

For years the software community has been pushing the MVC architectural pattern to organize and separate the concerns of our applications. So far we seem to have done a decent job of accomplishing that based on the enforcement of the pattern in the most successful web frameworks such as Rails, Grails, JSF, Struts and many others. The last frontier for MVC seems to be the sometimes convoluted world of JavaScript; the client tier of our web applications. Although frameworks like jQuery, Prototype, Scriptaculous, ExtJS, DOJO and others have greatly helped in cleaning up and structuring the client tier, there's still much to be desired. In recent years several micro-frameworks have appeared that aim to put an end to the madness of the JavaScript client tier world. In this article we'll explore the most prominent players and see how their usage impacts modern web development.

Blurting out a solution when so many do not understand the problem would not help. So, let’s start at the beginning… Before the web, but not that long ago, we had client-server applications in which the client was typically a desktop application and the server was a monolithic application that babysat a database. In those days, mastery of programming environments like Delphi, Visual Basic, Powerbuilder and Oracle Forms was in hot demand. Visual component environments allowed, to an extent, the separation of presentation logic from the database. I lived through that period and I remember the feeling that the user interface portions of the application seemed pretty orderly via the use of components but the server side was a disaster.

Then along came the web and, overwhelmed by the new environment, protocols, and stateless nature of the new beast, we somehow forgot everything we’ve learned in the past. The first applications were monolithic messes both in the front-end and the back (remember CGI scripts?). Then eventually we came to our senses and a myriad of web frameworks started to appear. Most of the frameworks implemented a version of the Model-View-Controller (MVC) pattern.

So while things got better at the web server level, the UI remained the wild frontier of web development, where JavaScript was the lingua franca and not many could speak it. JavaScript, an amazingly powerful but misused and misunderstood language, was caught smack in the middle of the so-called browser wars. These wars were centered around the differences and inconsistencies on the browser’s DOM (Document Object Model), which also made for a mess at development time. Slowly, the community reacted by creating frameworks like jQuery, Scriptaculous, Prototype, MooTools, Dojo, Ext JS and others that could hide the warts of the DOM implementations (and also mask some of the ugly parts of JavaScript).

These frameworks have simplified the way we deal with the DOM and JavaScript. They’ve given us the ability to encapsulate visual aspects of the DOM and deal with events in a uniform fashion. But in spite of our best efforts, complex client-side functionality still gets messy very fast. Applications typically end up as a spaghetti mess of callbacks between the DOM, your code and the server-side application. A more structured approach to building rich JavaScript applications is needed.

Model-View-Controller

The MVC pattern made its first appearance in 1979 when Trygve Reenskaug described a new architecture for developing interactive applications while working on Smalltalk at Xerox PARC. MVC breaks a functional slice of an application into three basic components: models, views and controllers.

In the MVC world, the model is responsible for maintaining an aspect of the state of the application. This “state” could be transient, lasting only while dealing with an interaction with the user or persistent and stored outside of the application’s realm. The model is responsible for encapsulating the operations relevant to the entity it represents. This encapsulation carries with it the responsibility of maintaining the integrity of the data that represents the state of the model.

The view encompasses a user interface element that typically reflects the state of the underlying model. Finally, the controller handles input events from the UI and determines a set of operations to be performed upon the model. Changes to the state of the model are then reflected on the view. How coupled the components of the MVC triumvirate are depends on implementation specific decisions.

A Simple Example: A Shopping Cart

To illustrate the problems the new breed of MVC frameworks aim to solve, I will first build a dynamic single page JavaScript application without them; a simple drag-n-drop jQuery based shopping cart.

Figure spine bsb 1

Figure BSB-1 – jQuery Shopping Cart Project

As shown in Figure BSB-1 the project consists of an HTML page called shopping_cart.html which uses the JavaScript files integrallis.commerce.js and integrallis.jquery.cart.js. These files are an attempt to separate the business objects from the visual aspects of the application. In integrallis.commerce.js I’ve implemented a simple shopping cart as shown in Listing BSB-1 (the full listing for this example can be found at https://github.com/bsbodden/jquery-shopping-cart)

var INTEGRALLIS = {};
INTEGRALLIS.commerce = {};
          
INTEGRALLIS.commerce.ShoppingCart = function(options) {
  // initializes the cart
  this.items = {};
             
  // callbacks
  var defaults = {
    removeAll: function() {},
    updateTotal: function() {},
    updateItemQuantity: function() {},
    removeItem: function() {}
  };
             
  // merge options with defaults
  for (property in defaults) { 
    if (!options.hasOwnProperty(property)) { 
      options[property] = defaults[property]
    } 
  }
             
  this.options = options;
}
          
// removes all items from the cart
INTEGRALLIS.commerce.ShoppingCart.prototype.clear = function() {
  this.items = {};
  this.options.removeAll();
  this.options.updateTotal();
}
          
// returns the value of all items in the cart
INTEGRALLIS.commerce.ShoppingCart.prototype.total = function() {
  var sum = 0.0;
  for (var index in this.items) {
    var item = this.items[index];
    sum = sum + (item.price * item.quantity);
  }
  return sum;
}
          
// returns the number of unique items in the cart
INTEGRALLIS.commerce.ShoppingCart.prototype.itemsCount = function() {
  var size = 0, key;
  for (key in this.items) {
    if (this.items.hasOwnProperty(key)) size++;
  }
          
  return size;
}
          
// returns whether the cart is empty or not
INTEGRALLIS.commerce.ShoppingCart.prototype.isEmpty = function() {
  return this.itemsCount() == 0;
}
             
// adds a new item to the cart or increases the quantity of an existing item
INTEGRALLIS.commerce.ShoppingCart.prototype.add = function(id, price, quantity) {
  var is_new;
  quantity = (typeof(quantity) != 'undefined') ? quantity : 1;
  if (this.items.hasOwnProperty(id)) {
    var item = this.items[id];
    item.quantity = item.quantity + quantity;
    this.options.updateItemQuantity(id);
    this.options.updateTotal();
    is_new = false;
  }
  else {
    this.items[id] = { quantity : quantity, price : price };
    this.options.updateTotal();
    is_new = true;
  }
  return is_new;
}
                 
// increases the quantity of an item in the cart
INTEGRALLIS.commerce.ShoppingCart.prototype.increase = function(id, quantity) {
  quantity = (typeof(quantity) != 'undefined') ? quantity : 1;
  if (this.items.hasOwnProperty(id)) {
    var item = this.items[id];
    item.quantity = item.quantity + quantity;
    this.options.updateItemQuantity(id);
    this.options.updateTotal();
  }
}
                 
// decreases the quantity of an item in the cart
INTEGRALLIS.commerce.ShoppingCart.prototype.decrease = function(id, quantity) {
  quantity = (typeof(quantity) != 'undefined') ? quantity : 1;
  if (this.items.hasOwnProperty(id)) {
    var item = this.items[id];
    if (item.quantity >= quantity) {
      item.quantity = item.quantity - quantity;
    }
    else {
      item.quantity = 0;
    }
    this.options.updateItemQuantity(id);
    if (item.quantity == 0) {
      delete this.items[id];
      this.options.removeItem(id);
    }
    this.options.updateTotal();
  }
}
                 
// returns the quantity of an item in the cart
INTEGRALLIS.commerce.ShoppingCart.prototype.quantityFor = function(id) {
  return this.items.hasOwnProperty(id) ? this.items[id].quantity : 0;
}

Listing BSB-1

The shopping cart provides the ability to maintain a list of items and their associated quantities as well as their total value. In order to reflect changes to the cart, I’ve created a set of callback functions (initialized to no-op functions) in the Constructor function of the cart. In integrallis.jquery.cart.js shown in Listing BOD-2, I wire the shopping cart object to the shopping_cart.html page. I start by instantiating the cart and passing the shopping cart UI callback functions. The callbacks use jQuery to update elements of the page to reflect changes in the shopping cart.

Interaction with the cart will occur when the user drops an HTML element representing an item into the cart “drop” area. To accomplish the drag-n-drop functionality I’m using jQuery’s draggable and droppable functions to enable the elements with class “product” to be dragged and dropped into the element with id “cart”.

// create a shopping cart instance, set all callbacks
var myShoppingCart = new INTEGRALLIS.commerce.ShoppingCart({ removeItem : removeItem,
                                                              removeAll : removeAll,
                                                            updateTotal : updateCartTotal,
                                                     updateItemQuantity : updateItemQuantity });
             
// shopping cart UI callbacks
          
function updateItemQuantity(id) {
  $('#cart #' + id)
    .children('#qty')
    .text(myShoppingCart.quantityFor(id))
    .effect("highlight", {}, 1500);     
}
          
function updateCartTotal() {
  $('#total').text(myShoppingCart.total()).effect("highlight", {}, 1500);
}
          
function removeItem(id) {
  $('#cart #' + id).effect("puff", {}, "slow", function(){ $(this).remove(); });
}
          
function removeAll() {
  $('#cart .product').effect("puff", {}, "slow", function(){ $('#cart').empty(); });
}
          
function decorateForCart(item, id) {
  item.append(' (1)')
    .append(' +')
    .append(' -');
  item.children('#add').click(function() {
    myShoppingCart.increase(id);
  });
  item.children('#remove').click(function() {
    myShoppingCart.decrease(id);
  });
}
          
$(document).ready(function() {
  // make products draggable
  $(".product").draggable({ helper: 'clone', opacity: "0.5" });
             
  // allow products to be dropped in the cart
  $("#cart").droppable({ accept: '.product', drop: function(ev, ui) {
      var item_dropped = ui.draggable;
      var id = item_dropped.attr('id');
      var price = item_dropped.attr('price');        
      if (myShoppingCart.add(id, price)) {
        var item = item_dropped.clone();
        decorateForCart(item, id);
        $(this).append(item);
      }
    }
  });
             
  // jquery-fy the 'clear shopping cart' button
  $('#dump').button().click(function() { myShoppingCart.clear() });
});

Listing BSB-2

Finally to put the cart to the test in shopping_cart.html where I provide a div representing the shopping cart and a list of “products” that can be dropped into the cart (Listing BOD-3). The “data” for the products is taken from the attributes of the <li> “product” elements.

<html>
  <head>
    <link href='http://fonts.googleapis.com/css?family=Muli' rel='stylesheet' type='text/css'>
    <link type="text/css" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/cupertino/jquery-ui.css" rel="stylesheet" />
    <link type="text/css" href="styles/shopping_cart.css" rel="stylesheet" />
             
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
    <script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js"></script>
    <script src="src/integrallis.commerce.js"></script>
    <script src="src/integrallis.jquery.cart.js"></script>
  </head>
  <body>
    <h2>jQuery Shopping Cart</h2>
          
    <h3>Available Products</h3>
             
    <ul class="products" id="products">
      <li class="product ui-state-default" id="item_0" price="100.0">Product 0 - $100</li>
      <li class="product ui-state-default" id="item_1" price="110.0">Product 1 - $110</li>
      <li class="product ui-state-default" id="item_2" price="120.0">Product 2 - $120</li>
    </ul>
          
    <h3>Your Shopping Cart</h3>
          
    <div id="cart">
    </div>
    <p>Total: $<span id="total">0.00</span></p>
    <button id="dump">Dump the cart!</button>
  </body>
</html>

Listing BSB-3

As the elements are dropped into the cart they are cloned and decorated with a couple of links to increase/decrease the quantity of the particular item in the cart.

In the callbacks we are dealing with dynamically updating the total value of the cart and increasing and decreasing the displayed quantities of an item. Also notice that if we drop an item that already exists in the cart, rather than creating a new item we simply increase the quantity of the existing item. Similarly, when an item’s quantity reaches zero we remove the item from the cart. All these operations are performed with jQuery’s animation capabilities to keep the user aware of changes to the cart.

Figure spine bsb 2 trans sm

Figure BSB-2 – jQuery Shopping Cart in Action

Shortcomings of the Jquery Shopping Cart

I am obviously taking some shortcuts for the sake of the example such as assuming that the available products magically appeared in the page (in a real application they would be retrieved from a server). But other than that, at first glance the implementation of the shopping cart seems to work as advertised and even seems well thought-out. That is, until we start asking the hard questions…

From the usability point of view, the first thing that jumps to mind is, what happens when the user clicks the refresh button? Since everything is just being kept in memory the answer is that it simply goes away. The user will end up with an empty shopping cart. The same applies to the back button; both will have the effect of dumping the cart!

One of the features that simple page applications try to achieve is the minimization of round trips to the server. Since the page is not to be refreshed by the user actions on the page, AJAX calls will have to be made to send the state of the cart back for the server for storage.

On the code organization front, even though I have taken steps to separate the business logic from the UI interaction code it is still obvious that for anything more complex we would end up with an unmanageable mess of UI callbacks all of which are currently living in the global JavaScript scope.

Another glaring problem is the coupling of the concepts of the shopping cart and the items in the shopping cart. The items are simple JavaScript objects with no behavior. Their reflection on the UI is controlled by the decorateForCart function as shown in Listing BSB-2. Ideally our cart items would be good object-oriented citizens and encapsulate both data and behavior.

Although it might seem that I’m being nitpicky about this implementation, the small issues above are representative of the types of problems that can rapidly turn a JavaScript application into an unbearable mess.

MVC to the Rescue: Introducing Spine

Spine is one of the many lightweight JavaScript micro MVC framework choices that have popped into the scene.

From the Spine Website:

"The library is written in CoffeeScript, but doesn't necessarily require CoffeeScript to develop applications. You can use CoffeeScript or JavaScript, whichever language you're most familiar with. Spine is tiny, the library comes in at around 500 lines of CoffeeScript, that's about 2K minified & compressed. Being lightweight and simple is fundamental to Spine."

Spine’s take on the MVC pattern might be slightly confusing for those accustomed to more traditional MVC implementations. Spine only provides Models and Controllers.

Figure BSB-3 shows the typical directory structure used for a Spine application. The application consists of 1 model, 2 controllers and a JavaScript file to bootstrap the application.

Figure spine bsb 3

Figure BSB-3 – jQuery/Spine Shopping Cart Project

Models

In Spine, a model represents an entity, which has state (data) and logic to manipulate said data. A model can be saved, retrieved and even queried for (to an extent). Models can be stored in memory (the default) or use any of the storage modules provided such as Ajax or HTML5 Local Storage. Listing BSB-4 shows the basic Item model that represents an item in our shopping cart application.

// Create the Item model.
var Item = Spine.Model.sub();
Item.configure("Item", "name", "pid", "price", "quantity");

Listing BSB-4

A Spine model “class” is created using the setup method of Spine.Model, which takes the name of the model and an array of properties. To make the model persist between page reloads we extend Item with the Spine.Model.Local module as shown in Listing BSB-5.

// Persist model between page reloads.
Item.extend(Spine.Model.Local);

Listing BSB-5

The extend method adds class properties to the model. We now have an object that can be created, saved and retrieved from the browser local storage.

To add behavior to our model we use the include method which adds instance properties. Listing BSB-6 shows the four methods that we need for our Item model; increase, decrease, total and label.

// Instance methods
Item.include({
  //
  total: function() {
    return (this.price * this.quantity);
  },
  //
  increase: function(quantity) {
    quantity = (typeof(quantity) != 'undefined') ? quantity : 1;
    this.quantity = this.quantity + quantity;
    this.trigger("quantityChanged");
  },
  //
  decrease: function(quantity) {
    quantity = (typeof(quantity) != 'undefined') ? quantity : 1;
    if (this.quantity >= quantity) {
      this.quantity = this.quantity - quantity;
    }
    else {
      this.quantity = 0;
    }
    this.trigger("quantityChanged");
  },
  // 
  label: function() {
    return (this.name + " - $" + this.price);
  }
});

Listing BSB-6

Controllers

Controllers in Spine are a combination of a traditional MVC controller and a view. Therefore controllers are in charge of rendering and manipulating one or more models in the context of the controller’s functionality.

Our first Spine controller will deal with the rendering and manipulation of an individual Item. The CartItem controller will deal with user interface events to increase and decrease the quantity of an Item while keeping the user abreast of the changes. Listing BSB-8 shows the CartItem controller contained in the file cart_item.js.

jQuery(function($){
  window.CartItem = Spine.Controller.sub({ 
                 
    init: function(){
      var cartItem = this;
                     
      this.item.bind("quantityChanged", function() { cartItem.updateQty() });
                     
      $('#item_' + this.item.pid + ' .add').live('click', function(e) { 
        cartItem.add(); 
        e.preventDefault(); 
      });
                     
      $('#item_' + this.item.pid + ' .remove').live('click', function(e) { 
        cartItem.remove(); 
        e.preventDefault(); 
      });
    },
                 
    render: function(){
      this.el = $.mustache($("#cartItem").html(), this.item);
                     
      return this;
    },
             
    // event handlers
    add: function(e) {
      this.item.increase();
    },
                     
    remove: function(e) {
      this.item.decrease();
    },
             
    // ui methods
    updateQty: function() {
      $('#item_' + this.item.pid + ' #qty')
      .text(this.item.quantity)
      .effect("highlight", {}, 1500);
    }
  });
})

Listing BSB-8

Spine controllers are created using the create method of Spine.Controller, which takes an object literal that sets a wide variety of properties. In Listing BSB-8 we see the first property passed is named proxied and takes an array of Strings. The values are the name of the controller methods that need to be guaranteed to execute in the controller’s context (otherwise you’ll noticed that sometimes the this variable points to something other than the controller).

Events

In the controller’s init method we bind a custom event called "quantityChanged" of the enclosing Item model to the controller’s updateQty method.

this.item.bind("quantityChanged", function() { cartItem.updateQty() });

Listing BSB-9.1

The model’s "quantityChanged" is triggered in both the increase and decrease methods of the Item model:

this.trigger("quantityChanged");

Listing BSB-9.2

Also, in the init method, we wire the ‘add’ and ‘remove’ links of the CartItem to the add and remove methods which invoke the underlying Item model add and remove methods.

Rendering

To render the associated view for the CartItem controller we use the render method. In the render method we use jQuery to locate the element with id cartItem, which is a template, embedded in the HTML page as shown in Listing BSB-10.

render: function(){
  this.el = $.mustache($("#cartItem").html(), this.item);
            
  return this;
}

Listing BSB-10

The cartItem template is a Mustache.js Template that enables the creation of markup templates containing binding expressions. The mustache method clones the template contents and replaces the binding expressions ({{exp}}) with the values of the object passed, in our case in Listing BSB-11 the controller’s enclosed Item model.

<!-- Mustache :-{)~ Template for CartItem -->
<script type="text/x-mustache-tmpl" id="cartItem">
  <li class="product ui-state-default" id="item_{{pid}}" price="{{price}}">
    {{label}}
    (<span id="qty">{{quantity}}</span>)
    <a href="#" class="add">+</a>
    <a href="#" class="remove">-</a>
  </li>
</script>

Listing BSB-11

To test our controller we need to instantiate an Item model and pass it to the controller. We can then render the controller on the DOM of an HTML page as shown in Listing BSB-12.

var item = new Item({name: "Product 1", pid: "0001" , price: 100.0, quantity: 1});
var view = new CartItem({item: item});
$("#item").html(view.render().el);

Listing BSB-12

A controller has an el (element) property that reflects the DOM element that will be the target of the rendering. The test page shown in Figure BSB-4 reveals that the CartItem controller accomplishes the functionality required to deal with a single Item model.

Figure spine bsb 4

Figure BSB-4 – CartItem Controller in Action

Shopping Cart Controller

With the CartItem controller in place we can now concentrate on the ShoppingCart controller (shown in Listing BSB-13), which will manage a collection of Item models.

Internally the ShoppingCart keeps the items dropped in the items property, which is initialized with an object literal in the init method. Also in the init method we set up the drop area for the drag-n-drop operation.

Persistence

In the init method we also query for any existing Item models that were previously saved by using the all method of the model (Item.all()). If any items are found then we simply add them to the cart using the addItem method.

Methods

The cart provides several aptly named methods to fulfill its functionality. The clear method iterates over all the contained Item models and invokes the destroy method which the cart listens to in order to animate the removal of rendered ItemCarts from the cart.

The methods total, isEmpty and itemsCount return the total dollar amount for the cart, whether the cart is empty and the count of all unique items in the cart respectively.

The drop method handles drop events similarly to the pure jQuery shopping cart implementation; it either creates a new item or increases the quantity of an existing item. The main difference being that the drop creates an Item model that is then rendered using the CartItem controller. Concerns separated!

jQuery(function($){
  window.ShoppingCart = Spine.Controller.sub({
    el: $("#theCart"),
                 
    init: function() {
      var cart = this;
      this.items = {};
      $.each(Item.all(), function(){ cart.addItem(this); });
      this.el.droppable({ accept: '.product', drop: this.proxy(this.drop) });
      $('#dump', this.el).live('click', function() { cart.clear(); });
    },
                 
    // removes all items from the cart
    clear: function() {
      $.each(this.items, function(){ this.destroy(); });
      this.items = {};
      this.updateCartTotal(); 
    },
                 
    total: function() {
      var sum = 0.0;
      $.each(this.items, function(){ sum += this.total(); });
          
      return sum;
    },
                 
    isEmpty: function() {
      return this.itemsCount() == 0;
    },
                 
    itemsCount: function() {
      var size = 0;
      var items = this.items;
      $.each(items, function(){ if (items.hasOwnProperty(this)) size++; });
          
      return size;
    },
                 
    drop: function(ev, ui) {
      var item_dropped = ui.draggable;
      var pid = item_dropped.attr('id');
      var price = item_dropped.attr('price');
      var name = item_dropped.attr('name');
          
      if (this.items.hasOwnProperty(pid)) {
        this.items[pid].increase();
      }
      else {
        var item = Item.create({name: name, pid: pid, price: price, quantity: 1});
        this.addItem(item);
        $(".items").append(CartItem.init({item: item}).render().el);
      }
    },
                 
    render: function() {
      this.el.html($.mustache($("#shoppingCart").html(), {}));
                     
      $('#dump').button();
          
      $.each(this.items, function(){ 
        $(".items").append(CartItem.init({item: this}).render().el);
      });
                     
      this.updateCartTotal();
    },
                 
    removeItem: function(item) {
      $('#item_' + item.pid).effect("puff", {}, "slow", function(){ $(this).remove(); });
    },
                 
    updateCartTotal: function() {
      $('#total').text(this.total()).effect("highlight", {}, 1500);
    },
                 
    removeIfQuantityZero: function(item) {
      if (item.quantity == 0) {
        this.removeItem(item);
        delete this.items[item.pid];
        item.destroy();
      }
    },
                 
    addItem: function(item) {
      this.items[item.pid] = item;
      item.bind("quantityChanged", this.proxy(this.updateCartTotal));
      item.bind("quantityChanged", this.proxy(this.removeIfQuantityZero));
      item.bind("quantityChanged", function() { item.save() });
      item.bind("destroy", this.proxy(this.removeItem));
      item.save();
      this.updateCartTotal();
    }
  });
})

Listing BSB-13

At the end of Listing BSB-13 we can see that the addItem method sets several listeners to various events on the enclosed Item models. One of the advantages of frameworks like Spine is the ability to deal with standard changes in related models without having to tightly couple their implementation to the controllers. Also, note that in addItem we save the created models so that we can later retrieve them in case of a page refresh.

Trying It All Together

Finally, to kick everything in motion the application.js file begins by fetching any previously stored Item records, making the sample products draggable, creating a cart and rendering it.

jQuery(function($){
  Item.fetch();
  $(".product").draggable({ helper: 'clone', opacity: "0.5" });
  var cart = new ShoppingCart();
  cart.render();
});

Listing BSB-14

Conclusions

In this article we’ve covered the basics of single page web applications. These applications are nothing new; we’ve attempted them in the past but now we have frameworks like jQuery to deal with browser disparities and Spine to inject the MVC sense of order.

The new breed of MVC frameworks allows us to create clean, simple, componentized and event-oriented applications. We can separate our applications’ logic into cleanly defined areas of concern, cache data on the client side, minimize AJAX calls and provide many features that we would otherwise have to implement ourselves. We can now build rich state of the art JavaScript applications that can rival their desktop counterparts.

References

  • Spine: http://spinejs.com/
  • jQuery: http://jquery.com
  • jQuery UI: http://jqueryui.com
  • Mustache Templates Plugin: https://github.com/janl/mustache.js
  • Example Code:
  • https://github.com/bsbodden/jquery-shopping-cart
  • https://github.com/bsbodden/jquery-spine-shopping-cart
Share