Get started with jQueryMx

Class         

JMVC's Controller and Model inherit from its Class helper - $.Class. To create a class, call $.Class(NAME, [classProperties, ] instanceProperties]).

$.Class("Animal",{
  breathe : function(){
     console.log('breathe'); 
  }
});

In the example above, instances of Animal have a breathe() method. We can create a new Animal instance and call breathe() on it like:

var man = new Animal();
man.breathe();

If you want to create a sub-class, simply call the the base class with the sub-class's name and properties:

Animal("Dog",{
  wag : function(){
    console.log('wag');
  }
})

var dog = new Dog;
dog.wag();
dog.breathe();

Instantiation

When a new class instance is created, it calls the class's init method with the arguments passed to the constructor function:

$.Class('Person',{
  init : function(name){
    this.name = name;
  },
  speak : function(){
    return "I am "+this.name+".";
  }
});

var payal = new Person("Payal");
assertEqual( payal.speak() ,  'I am Payal.' );

Calling base methods

Call base methods with this._super.  The following overwrites person to provide a more 'classy' greating:

Person("ClassyPerson", {
  speak : function(){
    return "Salutations, "+this._super();
  }
});

var fancypants = new ClassyPerson("Mr. Fancy");
assertEquals( fancypants.speak() , 'Salutations, I am Mr. Fancy.')

Proxies

Class's callback method returns a function that has 'this' set appropriately (similar to $.proxy).  The following creates a clicky class that counts how many times it was clicked:

$.Class("Clicky",{
  init : function(){
    this.clickCount = 0;
  },
  clicked: function(){
    this.clickCount++;
  },
  listen: function(el){
    el.click( this.callback('clicked') );
  }
})

var clicky = new Clicky();
clicky.listen( $('#foo') );
clicky.listen( $('#bar') ) ;

Static Inheritance

Class lets you define inheritable static properties and methods.  The following allows us to retrieve a person instance from the server by calling Person.findOne(ID, success(person) ).  Success is called back with an instance of Person, which has the speak method.

$.Class("Person",{
  findOne : function(id, success){
    $.get('/person/'+id, function(attrs){
      success( new Person( attrs ) );
    },'json')
  }
},{
  init : function(attrs){
    $.extend(this, attrs)
  },
  speak : function(){
    return "I am "+this.name+".";
  }
})

Person.findOne(5, function(person){
  assertEqual( person.speak(), "I am Payal." );
})

Introspection

Class provides namespacing and access to the name of the class and namespace object:

$.Class("Jupiter.Person");

Jupiter.Person.shortName; //-> 'Person'
Jupiter.Person.fullName;  //-> 'Jupiter.Person'
Jupiter.Person.namespace; //-> Jupiter

var person = new Jupiter.Person();

person.Class.shortName; //-> 'Person'

Model example

Putting it all together, we can make a basic ORM-style model layer.  Just by inheriting from Model, we can request data from REST services and get it back wrapped in instances of the inheriting Model.

$.Class("Model",{
  findOne : function(id, success){
    $.get('/'+this.fullName.toLowerCase()+'/'+id, 
      this.callback(function(attrs){
         success( new this( attrs ) );
      })
    },'json')
  }
},{
  init : function(attrs){
    $.extend(this, attrs)
  }
})

Model("Person",{
  speak : function(){
    return "I am "+this.name+".";
  }
});

Person.findOne(5, function(person){
  alert( person.speak() );
});

Model("Task")

Task.findOne(7,function(task){
  alert(task.name);
})

This is similar to how JavaScriptMVC's model layer works. Please continue to Model.

 

Model           

JavaScriptMVC's model and its associated plugins provide lots of tools around organizing model data such as validations, associations, lists and more.  But the core functionality is centered around service encapsulation, type conversion, and events.

Attributes and Observables

Of absolute importance to a model layer is the ability to get and set properties on the modeled data and listen for changes on a model instance.  This is the Observer pattern and lies at the heart of the MVC approach - views listen to changes in the model.

Fortunately, JavaScriptMVC makes it easy to make any data observable.  A great example is pagination.  It's very common that multiple pagination controls exist on the page.  For example, one control might provide next and previous page buttons.  Another control might detail the items the current page is viewing (ex "Showing items 1-20").  All pagination controls need the exact same data:

  • offset - the index of the first item to display
  • limit - the number of items to display
  • count - the total number of items

We can model this data with JavaScriptMVC's $.Model like:

var paginate = new $.Model({
  offset: 0,
  limit: 20,
  count: 200
});

The paginate variable is now observable.  We can pass it to pagination controls that can read from, write to, and listen for property changes.  You can read properties like normal or using the model.attr(NAME) method:

assertEqual( paginate.offset, 0 );
assertEqual( paginate.attr('limit') , 20 );

If we clicked the next button, we need to increment the offset.  Change property values with model.attr(NAME, VALUE).  The following moves the offset to the next page:

paginate.attr('offset',20);  

When paginate's state is changed by one control, the other controls need to be notified.  You can bind to a specific attribute change with model.bind(ATTR, success( ev, newVal ) ) and update the control:

paginate.bind('offset', function(ev, newVal){
  $('#details').text( 'Showing items ' + (newVal+1 )+ '-' + this.count )
})

You can also listen to any attribute change by binding to the 'updated.attr' event:

paginate.bind('updated.attr', function(ev, newVal){
  $('#details').text( 'Showing items ' + (newVal+1 )+ '-' + this.count )
})

The following is a next-previous jQuery plugin that accepts paginate data:

$.fn.nextPrev = function(paginate){
   this.delegate('.next','click', function(){
     var nextOffset = paginate.offset+paginate.limit;
     if( nextOffset < paginate.count){
       paginate.attr('offset', nextOffset );
     }
   })
   this.delegate('.prev','click', function(){
     var nextOffset = paginate.offset-paginate.limit;
     if( 0 < paginate.offset ){
       paginate.attr('offset', Math.max(0, nextOffset) );
     }
   });
   var self = this;
   paginate.bind('updated.attr', function(){
     var next = self.find('.next'),
         prev = self.find('.prev');
     if( this.offset == 0 ){
       prev.removeClass('enabled');
     } else { 
       prev.removeClass('disabled');
     }
     if( this.offset > this.count - this.limit ){
       next.removeClass('enabled');
     } else { 
       next.removeClass('disabled');
     }

   })
};

There are a few problems with this plugin. First, if the control is removed from the page, it is not unbinding itself from paginate.  We'll address this when we discuss controllers.

Second, the logic protecting a negative offset or offset above the total count is done in the plugin.  This logic should be done in the model. To fix this problem, we'll need to add additional constraints to limit what values limit, offset, and count can be.  We'll need to create a pagination class.

Extending Model

JavaScriptMVC's model inherits from $.Class.  Thus, you create a model class by inheriting from $.Model(NAME, [STATIC,] PROTOTYPE):

$.Model('Paginate',{
  staticProperty: 'foo'
},{
  prototypeProperty: 'bar'
})

There are a few ways to make the Paginate model more useful.  First, by adding setter methods, we can limit what values count and offset can be set to. 

Setters

Settter methods are model prototype methods that are named setNAME.  They get called with the val passed to model.attr(NAME, val) and a success and error callback.  Typically, the method should return the value that should be set on the model instance or call error with an error message.  Success is used for asynchronous setters.

The following paginate model uses setters to prevent negative counts the offset from exceeding the count by adding setCount and setOffset instance methods.

$.Model('Paginate',{
  setCount : function(newCount, success, error){
    return newCount < 0 ? 0 : newCount;
  },
  setOffset : function(newOffset, success, error){
    return newOffset < 0 ? 0 : Math.min(newOffset, !isNaN(this.count - 1) ? this.count : Infinity )
  }
})

Now the nextPrev plugin can set offset with reckless abandon:

this.delegate('.next','click', function(){
  paginate.attr('offset', paginate.offset+paginate.limit);
})
this.delegate('.prev','click', function(){
    paginate.attr('offset', paginate.offset-paginate.limit );
});

Defaults

We can add default values to Paginate instances by setting the static defaults property.  When a new paginate instance is created, if no value is provided, it initializes with the default value.

$.Model('Paginate',{
  defaults : {
    count: Infinity,
    offset: 0,
    limit: 100
  }
},{
  setCount : function(newCount, success, error){ ... },
  setOffset : function(newOffset, success, error){ ... }
})

var paginate = new Paginate({count: 500});
assertEqual(paginate.limit, 100);
assertEqual(paginate.count, 500);

This is getting sexy, but the Paginate model can make it even easier to move to the next and previous page and know if it's possible by adding helper methods.

Helper methods

Helper methods are prototype methods that help set or get useful data on model instances.  The following, completed, Paginate model includes a next and prev method that will move to the next and previous page if possible.  It also provides a canNext and canPrev method that returns if the instance can move to the next page or not.

$.Model('Paginate',{
  defaults : {
    count: Infinity,
    offset: 0,
    limit: 100
  }
},{
  setCount : function( newCount ){
    return Math.max(0, newCount  );
  },
  setOffset : function( newOffset ){
    return Math.max( 0 , Math.min(newOffset, this.count ) )
  },
  next : function(){
    this.attr('offset', this.offset+this.limit);
  },
  prev : function(){
    this.attr('offset', this.offset - this.limit )
  },
  canNext : function(){
    return this.offset > this.count - this.limit
  },
  canPrev : function(){
    return this.offset > 0
  }
})

Thus, our jQuery widget becomes much more refined:

$.fn.nextPrev = function(paginate){
   this.delegate('.next','click', function(){
     paginate.attr('offset', paginate.offset+paginate.limit);
   })
   this.delegate('.prev','click', function(){
     paginate.attr('offset', paginate.offset-paginate.limit );
   });
   var self = this;
   paginate.bind('updated.attr', function(){
     self.find('.prev')[paginate.canPrev() ? 'addClass' : 'removeClass']('enabled')
     self.find('.next')[paginate.canNext() ? 'addClass' : 'removeClass']('enabled');
   })
};

Service Encapsulation

We've just seen how $.Model is useful for modeling client side state.  However, for most applications, the critical data is on the server, not on the client.  The client needs to create, retrieve, update and delete (CRUD) data on the server.  Maintaining the duality of data on the client and server is tricky business.   $.Model is used to simplify this problem. 

$.Model is extremely flexible.  It can be made to work with all sorts of services types and data types.  This book covers only how $.Model works with the most common and popular type of service and data type: Representational State Transfer (REST) and JSON.

A REST service uses urls and the HTTP verbs POST, GET, PUT, DELETE to create, retrieve, update, and delete data respectively.  For example, a tasks service that allowed you to create, retrieve, update and delete tasks might look like:

 

ACTIONVERBURLBODYRESPONSE
Create a taskPOST/tasksname=do the dishes
{
  "id"       : 2,
  "name"     : "do the dishes",
  "acl"      : "rw" ,
  "createdAt": 1303173531164 // April 18 2011
}
Get a taskGET/task/2 
{
  "id"       : 2,
  "name"     : "do the dishes",
  "acl"      : "rw" ,
  "createdAt": 1303173531164 // April 18 2011
}
Get tasksGET/tasks 
[{
  "id"       : 1,
  "name"     : "take out trash",
  "acl"      : "r",
  "createdAt": 1303000731164 // April 16 2011
},
{
  "id"       : 2,
  "name"     : "do the dishes",
  "acl"      : "rw" ,
  "createdAt": 1303173531164 // April 18 2011
}]
Update a taskPUT/task/2name=take out recycling
{
  "id"       : 2,
  "name"     : "take out recycling",
  "acl"      : "rw" ,
  "createdAt": 1303173531164 // April 18 2011
}
Delete a taskDELETE/task/2 
{}

TODO: We can label the urls

The following connects to task services, letting us create, retrieve, update and delete tasks from the server:

$.Model("Task",{
  create  : "POST /tasks.json",
  findOne : "GET /tasks/{id}.json",
  findAll : "GET /tasks.json",
  update  : "PUT /tasks/{id}.json",
  destroy : "DELETE /tasks/{id}.json"
},{ });

The following table details how to use the task model to CRUD tasks.

 

ACTIONCODEDESCRIPTION
Create a task
new Task({ name: 'do the dishes'})
  .save( 
    success( task, data ), 
    error( jqXHR) 
  ) -> taskDeferred

To create an instance of a model on the server, first create an instance with new Model(attributes).  Then call save().

Save checks if the task has an id.  In this case it does not so save makes a create request with the task's attributes.  Save takes two parameters:

  • success - a function that gets called if the save is successful.  Success gets called with the task instance and the data returned by the server.
  • error - a function that gets called if there is an error with the request.  It gets called with jQuery's wrapped XHR object.
   Save returns a deferred that resolves to the created task.  
Get a task
Task.findOne(params, 
  success( task ), 
  error( jqXHR) 
) -> taskDeferred
Retrieves a single task from the server. It takes three parameters:  
  • params - data to pass to the server.  Typically an id like: {id: 2}.
  • success - a function that gets called if the request is succesful.  Success gets called with the task instance.
  • error - a function that gets called if there is an error with the request.
   findOne returns a deferred that resolves to the task.  
Get tasks
Task.findAll(params, 
  success( tasks ), 
  error( jqXHR) 
) -> tasksDeferred
Retrieves an array of tasks from the server. It takes three parameters:  
  • params - data to pass to the server.  Typically, it's an empty object ({}) or filters: {limit: 20, offset: 100}.
  • success - a function that gets called if the request is succesful.  Success gets called with an array of task instances.
  • error - a function that gets called if there is an error with the request.
   findOne returns a deferred that resolves to an array of tasks.  
Update a task
task.attr('name','take out recycling');
task.save( 
  success( task, data ), 
  error( jqXHR) 
) -> taskDeferred

To update the server, first change the attributes of a model instance with  attr.  Then call save().

Save takes the same arguments and returns the same deferred as the create task case.

Destroy a task
task.destroy( 
  success( task, data ), 
  error( jqXHR) 
) -> taskDeferred

Destroys a task on the server. Destroy takes two parameters:

  • success - a function that gets called if the save is successful.  Success gets called with the task instance and the data returned by the server.
  • error - a function that gets called if there is an error with the request.
   Destroy returns a deferred that resolves to the destroyed task.  

The Task model has essentially become a contract to our services!

Type Conversion

Did you notice how the server responded with createdAt values as numbers like 1303173531164.  This number is actually April 18th, 2011.  Instead of getting a number back from task.createdAt, it would be much more useful if it returns a JavaScript date created with new Date(1303173531164).  We could do this with a setCreatedAt setter.  But, if we have lots of date types, this will quickly get repetitive. 

To make this easy, $.Model lets you define the type of an attribute and a converter function for those types.  Set the type of attributes on the static attributes object and converter methods on the static convert object.

$.Model('Task',{
  attributes : {
    createdAt : 'date'
  },
  convert : {
    date : function(date){
      return typeof date == 'number' ? new Date(date) : date;
    }
  }
},{});

Task now converts createdAt to a Date type.  To list the year of each task, write:

Task.findAll({}, function(tasks){
  $.each(tasks, function(){
    console.log( "Year = "+this.createdAt.fullYear() )
  })
});

CRUD Events

Model publishes events when an instance has been created, updated, or destroyed. You can listen to these events globally on the Model or on an individual model instance. Use MODEL.bind(EVENT, callback( ev, instance ) ) to listen for created, updated, or destroyed events.

Lets say we wanted to know when a task is created and add it to the page. After it's been added to the page, we'll listen for updates on that task to make sure we are showing its name correctly.  We can do that like:

Task.bind('created', function(ev, task){
  var el = $('<li>').html(todo.name);
  el.appendTo($('#todos'));

  task.bind('updated', function(){
    el.html(this.name)
  }).bind('destroyed', function(){
    el.remove()
  })

})

Please continue to View.

 

View     page       

JavaScriptMVC's views are really just client side templates. Client side templates take data and return a string.  Typically, the strings are HTML intended to be inserted into the DOM.

$.View is a templating interface that takes care of complexities using templates:

  • Convenient and uniform syntax
  • Template loading from html elements or external files
  • Synchronous or asynchronous template loading
  • Template preloading
  • Caching of processed templates
  • Bundling of processed templates in production builds
  • $.Deferred support

JavaScriptMVC comes pre-packaged with 4 different template engines:

  • EJS
  • JAML
  • Micro
  • Tmpl

This tutorial uses EJS templates, but all the following techniques will work with any template engine with minor syntax differences.

Basic Use

When using views, you almost always want to insert the results of a rendered template into the page. jQuery.View overwrites the jQuery modifiers so using a view is as easy as:

$("#foo").html('mytemplate.ejs',{message: 'hello world'})

This code:

  1. Loads the template in file 'mytemplate.ejs'. It might look like:

    <h2><%= message %></h2>
    
  2. Renders it with {message: 'hello world'}, resulting in:

    <h2>hello world</h2>
    
  3. Inserts the result into the foo element. Foo might look like:

    <div id='foo'><h2>hello world</h2></div>
    

jQuery Modifiers

You can use a template with the following jQuery modifier methods:

$('#bar').after('temp.ejs',{});
$('#bar').append('temp.ejs',{});
$('#bar').before('temp.ejs',{});
$('#bar').html('temp.ejs',{});
$('#bar').prepend('temp.ejs',{});
$('#bar').replaceWith('temp.ejs',{});
$('#bar').text('temp.ejs',{});

Loading from a script tag

View can load from script tags or from files. To load from a script tag, create a script tag with a type attribute set to the template type (text/ejs) and an id to label the template:

<script type='text/ejs' id='recipesEJS'>
<% for(var i=0; i < recipes.length; i++){ %>
  <li><%=recipes[i].name %></li>
<%} %>
</script>

Render with this template like:

$("#foo").html('recipesEJS', recipeData)

Notice we passed the id of the element we want to render.

$.View and Sub-templates

Sometimes, you simply want the rendered string.  In this case, you can use $.View(TEMPLATE , data ) directly.  Pass $.View the path to the template and the data you want to render:

var html = $.View("template/items.ejs", items );

The most common use case is sub templates.  It's common practice to separate out an individual item's template from the items template.  We'll make template/items.ejs render an <LI> for each item, but use the template in template/item.ejs for the content of each item.

<% for( var i = 0; i < this.length; i++){ %>
  <li>
    <%= $.View("template/item.ejs", this[i]);  
  </li>
< % } %>

Notice, in the template this refers to the data passed to the template. In the case of template/items.ejs, this is the array of items.  In template/item.ejs it will be the individual item.

Deferreds

It's extremely common behavior to make an Ajax request and use a template to render the result.  Using the Task model from the previous $.Model section, we could render tasks like:

Task.findAll({}, function(tasks){
  $('#tasks').html("views/tasks.ejs" , tasks )
})

$.View supports $.Deferred allowing very powerful, terse, and high performance syntax.  If a deferred is found in the render data passed to $.View or the jQuery modifiers, $.View will load the template asynchronously and wait until all deferreds and the template are loaded before rendering the template.

The Model methods findAll, findOne, save or destroy return deferreds.  This allows us to rewrite the rendering of tasks into a one liner!

$('#tasks').html("views/tasks.ejs" , Task.findAll() )

This works with multiple deferreds too:

$('#app').html("views/app.ejs" , {
  tasks: Task.findAll(),
  users: User.findAll()
})

Packaging, Preloading, and Performance

By default, $.View loads templates synchronously.  This is because it's expected that you are either:

  • Putting templates in script tags,
  • Packaging templates with your JavaScript build, or
  • Preloading templates

JavaScriptMVC does not recommend putting templates in script tags.  Script tag templates make it hard to reuse templates across different JavaScript applications.  They can also reduce load performance if your app doesn't need the templates immediately.

JavaScriptMVC recommends packaging initially used templates with your application's JavaScript and preloading templates that will be used later.

StealJS, JavaScriptMVC's build system, can process and package templates, adding them to a minified production build.  Simply point steal.views(PATH, ...) to your template. 

steal.views('tasks.ejs','task.ejs');

Later, when $.View looks for that template it will use a cached copy, saving an extra Ajax request.

For templates that are not used immediately, preload and cache template them with jQuery.get.  Simply provide the url to the template and provide a dataType of 'view'.  It's best to do this a short time after the inital page has loaded:

$(window).load(function(){
  setTimeout(function(){
    $.get('users.ejs',function(){},'view');
    $.get('user.ejs',function(){},'view');
  },500)
})

Please continue to Controller.

 

Controller     page       

JavaScriptMVC's controllers are many things.  They are a jQuery plugin factory.  They can be used as a traditional view, making pagination widgets and grid controls.  Or, they can be used as a traditional controller, initializing and controllers and hooking them up to models.  Mostly, controller's are a really great way of organizing your application's code.

Controllers provide a number of handy features such as:

  • jQuery plugin creation
  • automatic binding
  • default options
  • automatic determinism

But controller's most important feature is not obvious to any but the most hard-core JS ninjas.  The following code creates a tooltip like widget that displays itself until the document is clicked.

$.fn.tooltip = function(){
  var el = this[0];

  $(document).click(function(ev){
    if(ev.target !== el){
      $(el).remove()
    }
  })

  $(el).show();
  return this;
})

To use it, you'd add the element to be displayed to the page, and then call tooltip on it like:

$("<div class='tooltip'>Some Info</div>")
    .appendTo(document.body)
    .tooltip()

But, this code has a problem.  Can you spot it?  Here's a hint. What if your application is long lived and lots of these tooltip elements are created? 

The problem is this code leaks memory!  Every tooltip element, and any tooltip child elements, are kept in memory forever.  This is because the click handler is not removed from the document and has a closure reference to the element. 

This is a frighteningly easy mistake to make.  jQuery removes all event handlers from elements that are removed from the page so developers often don't have to worry about unbinding event handlers.  But in this case, we bound to something outside the widget's element, the document, and did not unbind the event handler.

But within a Model-View-Controller architecture, Controllers listen to the View and Views listen to the Model.  You are constantly listening to events outside the widget's element.  For example, the nextPrev widget from the $.Model section listens to updates in the paginate model:

paginate.bind('updated.attr', function(){
  self.find('.prev')[this.canPrev() ? 'addClass' : 'removeClass']('enabled')
  self.find('.next')[this.canNext() ? 'addClass' : 'removeClass']('enabled');
})

But, it doesn't unbind from paginate!  Forgetting to remove event handlers is potentially a source of errors.  However, both the tooltip and nextPrev would not error.  Instead both will silently kill an application's performance.  Fortunately, $.Controller makes this easy and organized.  We can write tooltip like:

$.Controller('Tooltip',{
  init: function(){
    this.element.show()
  },
  "{document} click": function(el, ev){
    if(ev.target !== this.element[0]){
      this.element.remove()
    }
  }
})

When the document is clicked and the element is removed from the DOM, $.Controller will automatically unbind the document click handler. 

$.Controller can do the same thing for the nextPrev widget binding to the the paginate model:

$.Controller('Nextprev',{
  ".next click" : function(){
    var paginate = this.options.paginate;
    paginate.attr('offset', paginate.offset+paginate.limit);
  },
  ".prev click" : function(){
    var paginate = this.options.paginate;
    paginate.attr('offset', paginate.offset-paginate.limit );
  },
  "{paginate} updated.attr" : function(ev, paginate){
    this.find('.prev')[paginate.canPrev() ? 'addClass' : 'removeClass']('enabled')
    this.find('.next')[paginate.canNext() ? 'addClass' : 'removeClass']('enabled');
  }
})

// create a nextprev control
$('#pagebuttons').nextprev({ paginate: new Paginate() })

If the element #pagebuttons is removed from the page, the Nextprev controller instance will automatically unbind from the paginate model.

Now that your appetite for error free code is properly whetted, the following details how $.Controller works.

Overview

$.Controller inherits from $.Class.  To create a Controller class, call $.Controller( NAME, classProperties, instanceProperties ) with the name of your controller, static methods, and instance methods.  The following is the start of a reusable list widget:

$.Controller("List", {
  defaults : {}
},{
  init : function(){  },
  "li click" : function(){  }
})

When a controller class is created, it creates a jQuery helper method of a similar name.  The helper method is primarily use to create new instances of controller on elements in the page.  The helper method name is the controller's name underscored, with any periods replaced with underscores.  For example, the helper for $.Controller('App.FooBar') is $(el).appfoobar().

Controller Instantiation

To create a controller instance, you can call new Controller(element, options) with a HTMLElment or jQuery-wrapped element and an optional options object to configure the controller.  For example:

new List($('ul#tasks'), {model : Task});

You can also use the jQuery helper method to create a List controller instance on the #tasks element like:

$('ul#tasks').list({model : Task})

When a controller is created, it calls the controller's prototype init method with:

  • this.element set to the jQuery-wrapped HTML element
  • this.options set to the options passed to the controller merged with the class's defaults object.

The following updates the List controller to request tasks from the model and render them with an optional template passed to the list:

$.Controller("List", {
  defaults : {
    template: "items.ejs"
  }
},{
  init : function(){
    this.element.html( this.options.template, this.options.model.findAll() ); 
  },
  "li click" : function(){  }
})

We can now configure Lists to render tasks with a template we provide.  How flexible!

$('#tasks').list({model: Task, template: "tasks.ejs"});
$('#users').list({model: User, template: "users.ejs"})

If we don't provide a template, List will default to using items.ejs.

Event Binding

As mentioned in $.Controller's introduction, it's most powerful feature is it's ability to bind and unbind event handlers. 

When a controller is created, it looks for action methods.  Action methods are methods that look like event handlers.  For example, "li click".  These actions are bound using jQuery.bind or jQuery.delegate.  When the controller is destroyed, by removing the controller's element from the page or calling destroy on the controller, these events are unbound, preventing memory leaks.

The following are examples of actions with descriptions of what the listen for:

  • "li click" - clicks on or within li elements within the controller element.
  • "mousemove" - mousemoves within the controller element.
  • "{window} click" - clicks on or within the window.

Action functions get called back with the jQuery-wrapped element or object that the event happened on and the event.  For example:

"li click": function( el, ev ) {
  assertEqual(el[0].nodeName, "li" )
  assertEqual(ev.type, "click")
}

Templated Actions

$.Controller supports templated actions.  Templated actions can be used to bind to other objects, customize the event type, or customize the selector.

Controller replaces the parts of your actions that look like {OPTION} with a value in the controller's options or the window.

The following is a skeleton of a menu that lets you customize the menu to show sub-menus on different events:

$.Controller("Menu",{
  "li {openEvent}" : function(){
    // show subchildren
  }
});

//create a menu that shows children on click
$("#clickMenu").menu({openEvent: 'click'});

//create a menu that shows children on mouseenter
$("#hoverMenu").menu({openEvent: 'mouseenter'});

We could enhance the menu further to allow customization of the menu element tag:

$.Controller("Menu",{
  defaults : {menuTag : "li"}
},{
  "{menuTag} {openEvent}" : function(){
    // show subchildren
  }
});

$("#divMenu").menu({menuTag : "div"})

Templated actions let you bind to elements or objects outside the controller's element.  For example, the Task model from the $.Model section produces a "created" event when a new Task is created.  We can make our list widget listen to tasks being created and automatically add these tasks to the list like:

$.Controller("List", {
  defaults : {
    template: "items.ejs"
  }
},{
  init : function(){
    this.element.html( this.options.template, this.options.model.findAll() ); 
  },
  "{Task} created" : function(Task, ev, newTask){
    this.element.append(this.options.template, [newTask])
  }
})

The "{Task} create" gets called with the Task model, the created event, and the newly created Task. The function uses the template to render a list of tasks (in this case there is only one) and add the resulting html to the element.

But, it's much better to make List work with any model.  Instead of hard coding tasks, we'll make controller take a model as an option:

$.Controller("List", {
  defaults : {
    template: "items.ejs",
    model: null
  }
},{
  init : function(){
    this.element.html( this.options.template, this.options.model.findAll() ); 
  },
  "{model} created" : function(Model, ev, newItem){
    this.element.append(this.options.template, [newItem])
  }
});

// create a list of tasks
$('#tasks').list({model: Task, template: "tasks.ejs"});

Putting it all together - an abstract CRUD list.

Now we will enhance the list to not only add items when they are created, but update them and remove them when they are destroyed.  To do this, we start by listening to updated and destroyed:

"{model} updated" : function(Model, ev, updatedItem){
  // find and update the LI for updatedItem
},
"{model} destroyed" : function(Model, ev, destroyedItem){
  // find and remove the LI for destroyedItem
}

You'll notice here we have a problem.  Somehow, we need to find the element that represents particular model instance.  To do this, we need to label the element as belonging to the model instance.  Fortunately, $.Model and $.View make labeling an element with an instance and finding that element very easy. 

To label the element with a model instance within an EJS view, you simply write the model instance to the element.  The following might be tasks.ejs

<% for(var i =0 ; i < this.length; i++){ %>
  <% var task = this[i]; %>
  <li <%= task %> > <%= task.name %> </li>
<% } %>

tasks.ejs iterates through a list of tasks.  For each task, it creates an li element with the task's name.  But, it also adds the task to the element's jQuery data with: <li <%= task %> >.

To later get that element given a model instance, you can call modelInstance.elements([CONTEXT]).  This returns the jQuery-wrapped elements the represent the model instance. 

Putting it together, list becomes:

$.Controller("List", {
  defaults : {
    template: "items.ejs",
    model: null
  }
},{
  init : function(){
    this.element.html( this.options.template, this.options.model.findAll() ); 
  },
  "{model} created" : function(Model, ev, newItem){
    this.element.append(this.options.template, [newItem])
  },
  "{model} updated" : function(Model, ev, updatedItem){
    updatedItem.elements(this.element)
      .replaceWith(this.options.template, [updatedItem])
  },
  "{model} destroyed" : function(Model, ev, destroyedItem){
    destroyedItem.elements(this.element)
      .remove()
  }
});

// create a list of tasks
$('#tasks').list({model: Task, template: "tasks.ejs"});

It's almost frighteningly easy to create abstract, reusable, memory safe widgets with JavaScriptMVC.

Boo!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值