<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Bitovi Blog - UX and UI design, JavaScript and Front-end development

jQuery.Model - A jQuery Model Layer

Justin Meyer

jQuery.Model - A jQuery Model Layer

posted in Open Source, Development on October 13, 2010 by Justin Meyer


jQuery.Model - A jQuery Model Layer

Justin Meyer by Justin Meyer

For an updated take on a JavaScript model layer, check out can-connect.

Complex JavaScript applications are mostly about making it easy to create, read, update, and delete (CRUD) data. But being so close to the UI, most JavaScript developers ignore the data layer and focus on making animated drag-drop effects.

We're doing ourselves a disservice! A strong Model layer can make an architecture infinitely more robust, reusable, and maintainable.

jQuery.Model is designed to be a flexible and lightweight model layer for jQuery. The remainder of this article highlights the features, how to use them, and why they are important.

Downloads

Documentation

JavaScriptMVC's Model docs.

Features

  • Service / Ajax encapsulation
  • Type Conversion
  • Data Helper Methods
  • DOM Helper Functions
  • Events and Property Binding
  • Lists
  • Local Storage
  • Associations
  • Backup / Restore
  • Validations

Service / Ajax Encapsulation

Models encapsulate your application's raw data. The majority of the time, the raw data comes from services your server provides. For example, if you make a request to:

GET /contacts.json

The server might return something like:

[{
  'id': 1,
  'name' : 'Justin Meyer',
  'birthday': '1982-10-20'
},
{
  'id': 2,
  'name' : 'Brian Moschel',
  'birthday': '1983-11-10'
}]

In most jQuery code, you'll see something like the following to retrieve contacts data:

$.get('/contacts.json',
      {type: 'tasty'},
      successCallback,
      'json')

Instead, model encapsulates (wraps) this request so you call it like:

Contact.findAll({type: 'old'}, successCallback);

This might seem like unnecessary overhead, but by encapsulating your application's data, your application benefits in two significant ways:

Benefit 1: Localized Changes

Over the development lifecycle of an application, its very likely that your services will change. Models help localize your application's use of services to a single (TESTABLE!) location.

Benefit 2: Normalized Service Requests

Complex widgets, like Grids and Trees, need to make Ajax requests to operate correctly. Often these widgets need to be configured by a variety of options and callbacks. There's no uniformity, and sometimes you have to change your service to match the needs of the widget.

Instead, models normalize how widgets access your services, making it easy to use different models for the same widget.

Encapsulation Demo

The encapsulation demo shows using two different models with the same widget. The widgets are created like:

$("#recipes").grid({model: Recipe});
$("#workItems").grid({model: WorkItem});

How to Encapsulate

Think of models as a contract for creating, reading, updating, and deleting data. By filling out a model, you can pass that model to a widget and the widget will use the model as a proxy for your data.

The following chart shows the methods most models provide:

Create

Contact.create(attrs, success, error)

Read

Contact.findAll(params,success,error)
Contact.findOne(params, success, error)

Update

Contact.update(id, attrs, success, error)

Delete

Contact.destroy(id, success, error)

By filling out these methods, you get the benefits of encapsulation AND all the other magic Model provides. Lets see how we might fill out the Contact.findAll function:

$.Model.extend('Contact',
{
  findAll : function(params, success, error){

    // do the ajax request
    $.get('/contacts.json',
      params, 
      function( json ){ 

        // on success, create new Contact
        // instances for each contact
        var wrapped = [];

        for(var i =0; i< json.length;i++){
          wrapped.push( new Contact(json[i] ) );
        }

        //call success with the contacts
        success( wrapped );

      },
      'json');
  }
},
{
  // Prototype properties of Contact.
  // We'll learn about this soon!
});      

Well, that would be annoying to write out every time. Fortunately, models have the wrapMany method which will make it easier:

  findAll : function(params, success, error){
    $.get('/contacts.json',
      params,
      function( json ){
        success(Contact.wrapMany(json));
      },
      'json');
  }

Model is based off JavaScriptMVC's jQuery.Class. It's callback method allows us to pipe wrapMany into the success handler and make our code even shorter:

  findAll : function(params, success, error){
    $.get('/contacts.json',
    params,
    this.callback(['wrapMany', success]),
    'json')
  }

If we wanted to make a list of contacts, we could do it like:

Contact.findAll({},function(contacts){
  var html = [];
  for(var i =0; i < contacts.length; i++){
    html.push('<li>'+contacts[i].name + '</li>')
  }
  $('#contacts').html( html.join('') );
});

Read JavaScriptMVC's encapsulation documentation on how to fill out the other CRUD methods of the CRUD-Contract. Once this is done, you'll get all the following magic.

Type Conversion

By creating instances of Contact with the data from the server, it lets us wrap and manipulate the data into a more usable format.

You notice that the server sends back Contact birthdays like: '1982-10-20'. A string representation of dates is not terribly convient. We can use our model to convert it to something closer to new Date(1982,10,20). We can do this in two ways:

Way 1: Setters

In our Contact model, we can add a setBirthday method that will convert the raw data passed from the server to a format more useful for JavaScript:

$.Model.extend("Contact",
{
  findAll : function( ... ){ ... }
},
{
  setBirthday : function(raw){
    if(typeof raw == 'string'){
      var matches = raw.match(/(\d+)-(\d+)-(\d+)/)
      return new Date( matches[1],
                      (+matches[2])-1,
                       matches[3] )
    }else if(raw instanceof Date){
      return raw;
    }
  }
})

The setBirthday setter function takes the raw string date, parses it, and returns the JavaScript friendly date.

Way 2: Attributes and Converters

If you have a lot of dates, Setters won't scale well. Instead, you can set the type of an attribute and provide a function to convert that type.

The following sets the birthday attribute to "date" and provides a date conversion function:

$.Model.extend("Contact",
{
  attributes : {
    birthday : 'date'
  },
  convert : {
    date : function(raw){
      if(typeof raw == 'string'){
        var matches = raw.match(/(\d+)-(\d+)-(\d+)/)
        return new Date( matches[1],
                        (+matches[2])-1,
                         matches[3] )
      }else if(raw instanceof Date){
        return raw;
      }
    }
  },
  findAll : function( ... ){ ... }
},
{
  // No prototype properties necessary
})

This technique uses a Model's attributes and convert properties.

Now our recipe instances will have a nice Date birthday property. We can use it to list how old each person will be this year:

var age = function(birthday){
   return new Date().getFullYear() - 
          birthday.getFullYear()
}

Contact.findAll({},function(contacts){
  var html = [];
  for(var i =0; i < contacts.length; i++){
    html.push('<li>'+age(contacts[i].birthday) + '</li>')
  }
  $('#contacts').html( html.join('') );
});

But what if some other code wants to use age? Well, they'll have to use ...

Data Helper Methods

You can add domain specific helper methods to your models. The following adds ageThisYear to contact instances:

$.Model.extend("Contact",
{
  attributes : { ... },
  convert : { ... },
  findAll : function( ... ){ ... }
},
{
  ageThisYear : function(){
    return new Date().getFullYear() -
          this.birthday.getFullYear()
  }
})

Now we can write out the ages a little more cleanly:

Contact.findAll({},function(contacts){
  var html = [];
  for(var i =0; i < contacts.length; i++){
    html.push('<li>'+ contacts[i].ageThisYear() + '</li>')
  }
  $('#contacts').html( html.join('') );
});

Now that we are showing contacts on the page, lets do something with them. First, we'll need a way to get back our models from the page. For this we'll use ...

DOM Helper Functions

It's common practice with jQuery to put additional data 'on' html elements with jQuery.data. It's a great technique because you can remove the elements and jQuery will clean the data (letting the Garbage Collector do its work).

Model supports something similar with the model and models helpers. They let us set and retrieve model instances on elements.

For example, lets say we wanted to let the user delete contacts like in the Model DOM Demo.

First, we'll add a DELETE link like:

Contact.findAll({},function(contacts){
  var contactsEl = $('#contacts');
  for(var i =0; i < contacts.length; i++){
   $('<li>').model(contacts[i])
            .html(contacts[i].ageThisYear()+
                  " <a>DELETE</a>")
            .appendTo(contactsEl)
  }
});

When a model is added to an element's data, it also adds it's name a unique identifier to the element. For example, the first li element will look like:

<li class='contact contact_5'> ... </li>

When someone clicks on DELETE, we want to remove that contact. We implement it like:

$("#contacts a").live('click', function(){
  //get the element for this recipe
  var contactEl = $(this).closest('.contact')

  // get the conctact instance
  contactEl.model()
           // call destroy on the instance
           .destroy(function(){
                      // remove the element
                      contactEl.remove();
                    })

})

This assumes we've filled out Contact.destroy.

There's one more very useful DOM helper: contact.elements(). Elements returns the elements that have a particular model instance. We'll see how this helps us in the next section.

Events

Consider the case where we have two representations of the same recipe data on the page. Maybe when we click a contact, we show additional information on the page, like an input to change the contact's birthday.

See this in action in the events demo.

When the birthday is updated, we want the list's contact display to also update it's age. Model provides two ways of doing this.

Way 1 : Bind

You can bind to attribute changes in a model instance. The following listens for contact birthday changes. When birthday changes, it updates the item in the list:

Contact.findAll({},function(contacts){
  var contactsEl = $('#contacts');
  $.each(contacts, function(i, contact){
    var li = $('<li>')
              .model(contact)
              .html(contact.ageThisYear()+
                    " <a>Show</a>")
              .appendTo(contactsEl);
    contact.bind("birthday", function(){
      li.html(this.ageThisYear()+
              " <a>DELETE</a>");
    })
  })
});

Way 2 : Subscribe

If you include OpenAjax.hub in your project, Models will also publish OpenAjax messages that you can listen to. The following does roughly the same thing:

Contact.findAll({},function(contacts){
  var contactsEl = $('#contacts2');
  $.each(contacts, function(i, contact){
    var li = $('<li>')
              .model(contact)
              .html(contact.ageThisYear()+
                    " <a>Show</a>")
              .appendTo(contactsEl);
  });

  OpenAjax.hub.subscribe(
     "contact.updated", 
     function(called, contact){
       contact.elements(contactsEl)
              .html(contact.name+
                      " "+contact.ageThisYear()+
                      " <a>Show</a>");
     });
});

You might notice that we are using the elements method to retrieve all elements that represent the updated contact. This is an extra DOM query, and slower than than "Way 1". Why would we do this? We'll see in the next section.

Lists

In complex apps, we're often dealing with lists of data items. A user might want to select multiple contacts and delete them. The jQuery.Model.List plugin provides model list capabilities. Lists are useful in 2 ways:

Way 1: Faster Inserts

Remember how we originally inserted content into our page like:

Contact.findAll({},function(contacts){
  var html = [];
  for(var i =0; i < contacts.length; i++){
    html.push('<li>'+contacts[i].name + '</li>')
  }
  $('#contacts').html( html.join('') );
});

And, then we changed it to insert one element at a time. This is so we could use the model and models helpers. But, this makes the insert slower. For most use cases, this is going to be negligible. But, when performance matters, we've got you covered.

The following provides rapid insert at the cost of slightly more code and slower lookup.

Contact.findAll({},function(contacts){
  var contactsEl = $('#contacts'),
    html = [], 
    contact;

  // collect contact html
  for(var i =0; i < contacts.length;i++){
    contact = contacts[i]
    html.push("<li class='contact ",
        contact.identity(), // add the identity
        "'>",
        contact.name+" "+contact.ageThisYear()+
                " <a>Show</a>",
        "</li>")
  }
  // insert contacts html
  contactsEl.html(html.join(""))

  contactsEl.delegate("li","click", function(){
     // use the contacts list to get the
     // contact from the clicked element
     var contact = contacts.get(this)[0];

     makeAgeUpdater( contact );
  });

});

You can see this in action in the list insert demo.

There are two important things to notice in this example.

First, contacts is a Model.List and no longer a simple array. This allows us to call contacts.get(this)[0] to get a contact for a given element. We're using this technique because we can't use model().

Second, we used the identity method to provide a unique identifier that contacts.get uses to find the right contact.

Way 2 : List Helpers

We can use lists to add helper functions for multiple instances. Lets say we wanted to add checkboxes to each contact. And at the bottom of the list, we'll add a "DELETE ALL" button that will delete all checked instances. You can see this in the list helper demo.

The following makes a model list for contacts with a destroyAll helper:

$.Model.List.extend("Contact.List",{
  destroyAll : function(){
    $.post("/destroy",
      this.map(function(contact){
        return contact.id
      }),
      this.callback('destroyed'),
      'json')
  },
  destroyed : function(){
    this.each(function(){
      this.destroyed();
    })
  }
});

Now we can hook up our "DELETE ALL" button like this:

$("#destroyAll").click(function(){
  $("#contacts input:checked").closest(".contact")
    .models()
    .destroyAll();
})

The models helper returns a contact list with our destroyAll method on it.

Local Storage

Lists can also serialize and save themselves for local storage. Currently, there are two storage types - Cookie and Local. Local uses HTML5 localStorage and is not available in all browsers.

In the Cookie List demo , you can create contacts that are saved between page requests.

This is accomplished by creating a $.Model.List.Cookie class like the following

$.Model.List.Cookie.extend("Contact.List");

Then when the page is loaded, we use it to retrieve existing contacts. When the form is submitted, I add new contacts to the list and store the list again.

The code looks like:

var contacts = new Contact.List([]).retrieve("contacts");

// add each contact to the page
contacts.each(function(){
  // addContact is a helper that makes
  // and inserts html for a contact
  addContact(this);
});

// when a new cookie is created
$("#contact").submit(function(ev){
  ev.preventDefault();
  var data = $(this).formParams();

  // gives it a random id
  data.id = +new Date();
  var contact = new Contact(data);

  //add it to the list of contacts 
  contacts.push(contact);

  //store the current list
  contacts.store("contacts");

  //show the contact
  addContact(contact);
})

Associations

For efficiency, you often want to get data for related records at the same time. The jquery.model.assocations plugin lets you do this.

Lets say we wanted to list tasks for each of our contacts. When we request our contacts, the JSON data will come back like:

[
 {'id': 1,
  'name' : 'Justin Meyer',
  'birthday': '1982-10-20',
  'tasks' : [
    {'id': 1,
     'title': "write up model layer",
     'due': "2010-10-5" },
    {'id': 1,
     'title': "document models",
     'due': "2010-10-8"}]},
  ...
]

Like contacts, tasks have a due date we want to convert to a weeksPastDue helper. We can do this by adding a Task model.

$.Model.extend("Task",{
  convert : {
    date : function(date){ ... }
  },
  attributes : {
    due : 'date'
  }
},{
  weeksPastDue : function(){
    return Math.round( (new Date() - this.due) /
          (1000*60*60*24*7 ) );
  }
})

Now we just need to tell our Contact model that it will have many Tasks.

$.Model.extend("Contact",{
  associations : {
    hasMany : "Task"
  },
  ...
},{
  ...
});

Now we can output the contacts with their tasks:

Contact.findAll({},function(contacts){
  var contactsEl = $('#contacts');
  $.each(contacts, function(i, contact){
    var li = $('<li>')
              .model(contact)
              .html(contact.name+" "+contact.ageThisYear())
              .appendTo(contactsEl);
    var ul =$("<ul>");

    // add each task to the page
    contact.attr('tasks').each(function(){
      $('<li>').model(this)
               .html(this.title+" "+this.weeksPastDue())
               .appendTo(ul);
    })
    ul.appendTo(li)
  });
});

See this in action with the associations demo.

Backup / Restore

Sometimes you want to let a user make changes to data and then let them restore the original data. The jquery.model.backup plugin enables this functionality.

In the backup demo, we backup each contact like:

Contact.findAll({},function(contacts){
  var contactsEl = $('#contacts');
  $.each(contacts, function(i, contact){
    ...
    contact.backup()
    ...
  });
});

To restore the contacts, we listen for click and call restore on each contact:

$("#restore").click(function(){
  contacts.each(function(){
    this.restore()
  })
})

Validations

Finally, in many apps, it's important to validate data before sending it to the server. The jquery.model.validations plugin provides validations on models.

In the validations demo, we validate that the contact can not have a birthday in the future. This is done by adding validations in the Contact class's init method:

$.Model.extend("Contact",{
  init : function(){
    this.validate("birthday",function(){
      if(this.birthday > new Date){
        return "your birthday needs to be in the past"
      }
    })
  },
...
});

When setting a contact's birthday attribute, we can provide success and error callbacks that will show or hide an error message:

contact.attr("birthday", this.value ,
  function(){
    // on success, hide the error div
    $('#error').hide();
  },
  function(errors){
    // on error, show the error
    $('#error').html(errors.birthday[0]).show();
  })

Conclusion

Model is probably the least understood part of JavaScriptMVC's toolset. This is understandable. People get the need to unbind event handlers (Controller) and the utility of client side templates (view), but a model on the client ... that's crazy!

This is likely due to unfamiliarity with treating the server as a provider of raw data services - the idea behind Thin Server Architecture. Model is based around this concept. If you're unfamiliar with approach, please check out this video.

Assuming Thin Server Architecture makes sense to you, Model is awesome at what it does - flexibly encapsulating an Ajax application's data layer.

We often work alongside server teams and rarely have the luxury of a complete service API. Model insulates our widgets from the underlying AJAX requests. It is a contract we pass to our widgets, allowing them to manipulate services through a proxy.

If the services change, we only have to update the model layer.

Data Pipelining

There's one feature of model that deserves special attention - its ability to wrap service data with helper functions.

This provides what we call "data pipelining", which basically means avoiding unnecessary data transformations.

It's very common practice for a server to get data from a database and transform it for the client. A good example is converting a birthday into an age.

But, this adds unecessary complexity to the server. At some point, the client might want other values derived from a birthday. The server will need to constantly adjust to provide them.

Model makes it easy to avoid this craziness. Your server sends raw data, essentially a JSON dump of the database, and the client is left to extract the information it needs.

Conclusion's conclusion

Model has, of course, a lot of other benefits. It forms the backbone of most of our apps and enables the thinest of servers. We hope you will find it as useful as we have.

Create better web applications. We’ll help. Let’s work together.