jQuery.Model - A jQuery Model Layer
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
- jquery.model.js (2.8 K minimized and gzipped) [includes $.Class]
- jquery.model.associations.js
- jquery.model.backup.js
- jquery.model.list.js
- jquery.model.list.cookie.js
- jquery.model.validations.js
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.