Skip to content

Instantly share code, notes, and snippets.

@mindscratch
Forked from jupiterjs/$.Controller.md
Created May 31, 2011 09:47
Show Gist options
  • Save mindscratch/1000244 to your computer and use it in GitHub Desktop.
Save mindscratch/1000244 to your computer and use it in GitHub Desktop.
$.Controller for Alex MacCaw's Book

TODOS:

  • show .models() method and hookup

$.Controller - jQuery plugin factory

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).app_foo_bar().

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 our 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.

Binding

When a controller is created, before calling init, it looks for action methods. Action methods are methods that look like event handlers. For example: "li click". It binds these actions using event delegation.

You create a controller by calling this jQuery.fn method on any jQuery collection. You can pass in options, which are used to set this.options in the controller.

$(".thing").my_widget({message : "Hello"})

Determinism

Controllers provide automatic determinism for your widgets. This means you can look at a controller and know where in the DOM they operate, and vice versa.

First, when a controller is created, it adds its underscored name as a class name on the parent element.

<div id='historytab' class='history_tabs'></div>

You can look through the DOM, see a class name, and go find the corresponding controller.

Second, the controller saves a reference to the parent element in this.element. On the other side, the element saves a reference to the controller instance in jQuery.data.

$("#foo").data('controllers')

A helper method called controller (or controllers) using the jQuery.data reference to quickly look up controller instance on any element.

$("#foo").controller() // returns first controller found
$("#foo").controllers() // returns an array of all controllers on this element

Finally, actions are self labeling, meaning if you look at a method called ".foo click", there is no ambiguity about what is going on in that method.

Responding to Actions

If you name an event with the pattern "selector action", controllers will set these methods up as event handlers with event delegation. Even better, these event handlers will automatically be removed when the controller is destroyed.

".todo mouseover" : function( el, ev ) {}

The el passed as the first argument is the target of the event, and ev is the jQuery event. Each handler is called with "this" set to the controller instance, which you can use to save state.

Removing Controllers

Part of the magic of controllers is their automatic removal and cleanup. Controllers bind to the special destroy event, which is triggered whenever an element is removed via jQuery. So if you remove an element that contains a controller with el.remove() or a similar method, the controller will remove itself also. All events bound in the controller will automatically clean themselves up.

Defaults

Controllers can be given a set of default options. Users creating a controller pass in a set of options, which will overwrite the defaults if provided.

In this example, a default message is provided, but can is overridden in the second example by "hi".

$.Controller("Message", {
  defaults : {
    message : "Hello World"
  }
},{
  init : function(){
    this.element.text(this.options.message);
  }
})

$("#el1").message(); //writes "Hello World"
$("#el12").message({message: "hi"}); //writes "hi"

Parameterized Actions

Controllers provide the ability to set either the selector or action of any event via a customizable option. This makes controllers potentially very flexible. You can create more general purpose event handlers and instantiate them for different situations.

The following listens to li click for the controller on #clickMe, and "div mouseenter" for the controller on #touchMe.

$.Controller("Hello", {
  defaults: {item: “li”, helloEvent: “click”}
}, {
  “{item} {helloEvent}" : function(el, ev){
    alert('hello')�    el // li, div
  }
})

$("#clickMe").hello({item: “li”, helloEvent : "click"});
$("#touchMe").hello({item: “div”, helloEvent : "mouseenter"});

Pub / Sub

JavaScriptMVC applications often use OpenAjax event publish and subscribe as a good way to globally notify other application components of some interesting event. The jquery/controller/subscribe method lets you subscribe to (or publish) OpenAjax.hub messages:

$.Controller("Listener",{
  "something.updated subscribe" : function(called, data){}
})

// called elsewhere
this.publish("something.updated", data);

Special Events

Controllers provide support for many types of special events. Any event that is added to jQuery.event.special and supports bubbling can be listened for in the same way as a DOM event like click.

$.Controller("MyHistory",{
  "history.pagename subscribe" : function(called, data){
    //called when hash = #pagename
  }
})

Drag, drop, hover, and history and some of the more widely used controller events. These events will be discussed later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment