jQuery.Controller class
inherits: jQuery.Class
plugin: jquery/controller
download: jQuery.Controller
test: qunit.html
jQuery.Controller helps create organized, memory-leak free, rapidlyperforming jQuery widgets. Its extreme flexibility allows it to serve as both atraditional View and a traditional Controller.
This means it is used to create things like tabs, grids, and contextmenusas well as organizing them into higher-order business rules.
Controllers make your code deterministic, reusable, organized and can tearthemselves down auto-magically. Read about the theorybehind controller and a walkthroughof its features on Jupiter's blog. Get Startedwith jQueryMX also has a great walkthrough.
Controller inherits from $.Class andmakes heavy use of event delegation.Make sure you understand these concepts before using it.
Basic Example
Instead of
$(function(){
$('#tabs').click(someCallbackFunction1)
$('#tabs .tab').click(someCallbackFunction2)
$('#tabs .delete click').click(someCallbackFunction3)
});
do this
$.Controller('Tabs',{
click: function() {...},
'.tab click' : function() {...},
'.delete click' : function() {...}
})
$('#tabs').tabs();
Tabs Example
Demo
HTML
<ul id="tabs" class="ui-helper-clearfix"'="">
<li><a href="#tab1">Tab 1</a></li>
<li><a href="#tab2">Tab 2</a></li>
<li><a href="#tab3">Tab 3</a></li>
</ul>
<div id="tab1" class="tab">Tab 1 Content</div>
<div id="tab2" class="tab">Tab 2 Content</div>
<div id="tab3" class="tab">Tab 3 Content</div>
Source
steal("jquery/controller", function(){
// create a new Tabs class
$.Controller("Tabs",{
// initialize widget
init : function(el){
// activatethe first tab
$(el).children("li:first").addClass('active')
// hide theother tabs
var tab = this.tab;
this.element.children("li:gt(0)").each(function(){
tab($(this)).hide()
})
},
// helper function finds the tabfor a given li
tab : function(li){
return $(li.find("a").attr("href"))
},
// hides old active tab, showsnew one
"li click" : function(el, ev){
ev.preventDefault();
this.tab(this.find('.active').removeClass('active')).hide()
this.tab(el.addClass('active')).show();
}
})
// adds the controller to the element
$("#tabs").tabs();
})
Using Controller
Controller helps you build and organize jQuery plugins. It can be used tobuild simple widgets, like a slider, or organize multiple widgets intosomething greater.
To understand how to use Controller, you need to understand the typicallifecycle of a jQuery widget and how that maps to controller's functionality:
A controller class is created.
$.Controller("MyWidget",
{
defaults : {
message : "Remove Me"
}
},
{
init : function(rawEl, rawOptions){
this.element.append(
"<div>"+this.options.message+"</div>"
);
},
"div click" : function(div, ev){
div.remove();
}
})
This creates a $.fn.my_widget jQuery helper function that can be used to create a newcontroller instance on an element. Find more information hereabout the plugin gets created and the rules around its name.
An instance of controller is created on an element
$('.thing').my_widget(options)// calls new MyWidget(el, options)
This calls newMyWidget(el, options) on each '.thing' element.
When a new Classinstance is created, it calls the class's prototype setup and init methods.Controller's setupmethod:
- Sets this.element and adds the controller's name to element's className.
- Merges passed in options with defaults object and sets it as this.options
- Saves a reference to the controller in $.data.
- Binds all event handler methods.
The controller responds to events
Typically, Controller event handlers are automatically bound. However,there are multiple ways to listento events with a controller.
Once an event does happen, the callback function is always called with'this' referencing the controller instance. This makes it easy to use helperfunctions and save state on the controller.
The widget is destroyed
If the element is removed from the page, the controller's jQuery.Controller.prototype.destroymethod is called. This is a great place to put any additional teardownfunctionality.
You can also teardown a controller programatically like:
$('.thing').my_widget('destroy');
Todos Example
Lets look at a very basic example - a list of todos and a button you wantto click to create a new todo. Your HTML might look like:
<div id='todos'>
<ol>
<li class="todo">Laundry</li>
<li class="todo">Dishes</li>
<li class="todo">Walk Dog</li>
</ol>
<a class="create">Create</a>
</div>
To add a mousover effect and create todos, your controller might looklike:
$.Controller('Todos',{
".todo mouseover" : function( el, ev ) {
el.css("backgroundColor","red")
},
".todo mouseout" : function( el, ev ) {
el.css("backgroundColor","")
},
".create click" : function() {
this.find("ol").append("<li class='todo'>New Todo</li>");
}
})
Now that you've created the controller class, you've must attach the eventhandlers on the '#todos' div by creating anew controller instance. There are 2 ways of doing this.
//1. Create a new controller directly:
new Todos($('#todos'));
//2. Use jQuery function
$('#todos').todos();
Controller Initialization
It can be extremely useful to add an init method with setup functionalityfor your widget.
In the following example, I create a controller that when created, willput a message as the content of the element:
$.Controller("SpecialController",
{
init: function( el, message ) {
this.element.html(message)
}
})
$(".special").special("Hello World")
Removing Controllers
Controller removal is built into jQuery. So to remove a controller, youjust have to remove its element:
$(".special_controller").remove()
$("#containsControllers").html("")
It's important to note that if you use raw DOM methods (innerHTML, removeChild), the controllers won't be destroyed.
If you just want to remove controller functionality, call destroy on thecontroller instance:
$(".special_controller").controller().destroy()
Accessing Controllers
Often you need to get a reference to a controller, there are a few ways ofdoing that. For the following example, we assume there are 2 elements with className="special".
//creates 2 foo controllers
$(".special").foo()
//creates 2 bar controllers
$(".special").bar()
//gets all controllers on all elements:
$(".special").controllers() //-> [foo, bar,foo, bar]
//gets only foo controllers
$(".special").controllers(FooController) //->[foo, foo]
//gets all bar controllers
$(".special").controllers(BarController) //->[bar, bar]
//gets first controller
$(".special").controller() //-> foo
//gets foo controller via data
$(".special").data("controllers")["FooController"] //-> foo
Calling methods on Controllers
Once you have a reference to an element, you can call methods on it.However, Controller has a few shortcuts:
//creates foo controller
$(".special").foo({name: "value"})
//calls FooController.prototype.update
$(".special").foo({name: "value2"})
//calls FooController.prototype.bar
$(".special").foo("bar","something I want to pass")
These methods let you call one controller from another controller.
Listening To Events page
Controllers make creating and tearing down event handlersextremely easy. The tearingdown of event handlers is especially important inpreventing memory leaks in long lived applications.
Automatic Binding
When a newcontroller is created, contoller checks its prototype methods for functionsthat are named like event handlers. It binds these functions to thecontroller's elementwith event delegation. When the controller is destroyed (or it's element isremoved from the page), controller will unbind its event handlersautomatically.
For example, each of the following controller's functionswill automatically bound:
$.Controller(
"Crazy",{
// listens to all clicks on this element
"click"
:
function(el, ev){},
// listens to all mouseovers on
// li elements withing this controller
"li mouseover"
:
function(el, ev){}
// listens to the window being resized
"{window} resize"
:
function(window, ev){}
})
Controller will bind function names with spaces, standardDOM events, and event names in $.event.special.
In general, Controller will know automatically when to bindevent handler functions except for one case - event names without selectorsthat are not in $.event.special.
But to correct for this, you just need to add the functionto the listensToproperty. Here's how:
$.Controller(
"MyShow",{
listensTo: [
"show"]
},{
show:
function( el, ev ) {
el.show();
}
})
$(
'.show').my_show().trigger(
"show");
Callback parameters
Event handlers bound with controller are called back withthe element and the event as parameters. this refers to the controllerinstance. For example:
$.Controller(
"Tabs",{
// li - the list element that was clicked
// ev - the click event
"li click"
:
function(li, ev){
this
.tab(li).hide()
},
tab :
function(li){
return
$(li.find(
"a").attr(
"href"))
}
})
Templated Event Bindings
One of Controller's most powerful features is templatedevent handlers. You can parameterize the event name, the selector, or event theroot element.
Templating event names and selectors:
Often, you want to make a widget's behavior configurable. Acommon example is configuring which event a menu should show a sub-menu (ex: onclick or mouseenter). The following controller lets you configure when a menushould show sub-menus:
The following makes two buttons. One says hello on click,the other on a 'tap' event.
$.Controller(
"Menu",{
"li {showEvent}"
:
function(el){
el.children(
'ul').show()
}
})
$(
"#clickMe").menu({showEvent :
"click"});
$(
"#touchMe").menu({showEvent :
"mouseenter"});
$.Controller replaces value in {}
with valuesin a controller's options.This means we can easily provide a default showEvent
value andcreate a menu without providing a value like:
$.Controller(
"Menu",
{
defaults : {
showEvent :
"click"
}
},
{
"li {showEvent}"
:
function(el){
el.children(
'ul').show()
}
});
$(
"#clickMe").menu();
//defaults to using click
Sometimes, we might might want to configure our widget touse different elements. The following makes the menu widget's button
elements configurable:
$.Controller(
"Menu",{
"{button} {showEvent}"
:
function(el){
el.children(
'ul').show()
}
})
$(
'#buttonMenu').menu({button:
"button"});
Templating the root element.
Finally, controller lets you bind to objects outside of thecontroller'selement.
The following listens to clicks on the window:
$.Controller(
"HideOnClick",{
"{window} click"
:
function(){
this
.element.hide()
}
})
The following listens to Todos being created:
$.Controller(
"NewTodos",{
"{App.Models.Todo} created"
:
function(Todo, ev, newTodo){
this
.element.append(
"newTodos.ejs", newTodo)
}
});
But instead of making NewTodos only work with the Todomodel, we can make it configurable:
$.Controller(
"Newbie",{
"{model} created"
:
function(Model, ev, newItem){
this
.element.append(
this.options.view, newItem)
}
});
$(
'#newItems').newbie({
model: App.Models.Todo,
view:
"newTodos.ejs"
})
How Templated events work
When looking up a value to replace {}
,controller first looks up the item in the options, then it looks up the valuein the window object. It does not use eval to look up the object. Instead ituses jQuery.String.getObject.
Subscribing to OpenAjax messages and custom bindings
The jquery/controller/subscribe plugin allows controllersto listen to OpenAjax.hub messages like:
$.Controller(
"Listener",{
"something.updated subscribe"
:
function(called, data){
}
})
You can create your own binders by adding to jQuery.Controller.static.processors.
Manually binding to events.
The [jQuery.Controller.prototype.bind] and jQuery.Controller.prototype.delegatemethods let you listen to events on other elements. These event handlers willbe unbound when the controller instance is destroyed.
The generated jQuery plugin page
When you create a controller, it creates a jQuery plugin that can be usedto:
- Create controllers on an element or elements
- Call controller methods
- Update a controller
For example, the following controller:
$.Controller("My.Widget",{
say : function(){
alert(this.options.message);
}
})
creates a jQuery.fn.my_widget method that you can use like:
// create my_widget on each .thing
$(".thing").my_widget({message : "Hello"})
// alerts "Hello"
$(".thing").my_widget("say");
// updates the message option
$(".thing").my_widget({message : "World"});
// alerts "World"
$(".thing").my_widget("say");
Note that in every case, the my_widget plugin returns the original jQuerycollection for chaining ($('.thing')). If you want to get a valuefrom a controller, use the jQuery.fn.controllersor jQuery.fn.controller.
Creating controllers
When a controller's jQuery plugin helper is used on a jQuery collection,it goes to each element and tests if it has a controller instance on theelement. If it does not, it creates one.
It calls newYourController with the element and anyadditional arguments you passed to the jQuery plugin helper. So for example,say there are 2 elements in $('.thing').
This:
$(".thing").my_widget({message : "Hello"})
Does the exact same thing as:
var things = $('.thing'),
options = {message : "Hello"};
new My.Widget(things[0],options);
new My.Widget(things[1],options);
Note, when a new Class is created, it calls your class's prototype setup andinit methods. Read controller'ssetup for the details on what happens when a new controller is created.
Calling methods on controllers
Once a Controller is already on an element, you can call methods on itwith the same jQuery helper. The first param to the helper is the name of themethod, the following params are passed to the jQuery function. For example:
$.Controller("Adder",{
sum : function(first, second, third){
this.element.text(first+second+third);
}
})
// add an adder to the page
$("#myadder").adder()
// show the sum of 1+2+3
$("#myadder").adder("sum",1,2,3);
Naming
By default, a controller's jQuery helper is the controller name:
- underscored
- "." replaced with "_"
- with Controllers removed.
Here are some examples:
$.Controller("Foo") // -> .foo()
$.Controller("Foo.Bar") // -> .foo_bar()
$.Controller("Foo.Controllers.Bar") // ->.foo_bar()
You can overwrite the Controller's default name by setting a staticpluginName property:
$.Controller("My.Tabs",
{
pluginName: "tabs"
},
{ ... })
$("#tabs").tabs()
jQuery.Controller.prototype.delegate function
Delegate will delegate on an elememt and will be undelegated when thecontroller is removed. This is a good way to delegate on elements not in acontroller's element.
Example:
// calls function when the any 'a.foo' is clicked.
this.delegate(document.documentElement,'a.foo', 'click', function(ev){
//do something
})
API
controller.delegate(element,selector, eventName, func) -> Integer
element {optional:HTMLElement|jQuery.fn} defaults to this.element
the element to delegate from
selector {String}
the css selector
eventName {String}
the event to bind to
func {Function|String}
A callback function or the String name of a controller function. If acontroller function name is given, the controller function is called back withthe bound element and event as the first and second parameter. Otherwise thefunction is called back like a normal bind.
returns {Integer}
The id of the binding in this._bindings
jQuery.Controller.prototype.destroy function
Destroy unbinds and undelegates all event handlers on this controller, andprevents memory leaks. This is called automatically if the element is removed.You can overwrite it to add your own teardown functionality:
$.Controller("ChangeText",{
init : function(){
this.oldText = this.element.text();
this.element.text("Changed!!!")
},
destroy : function(){
this.element.text(this.oldText);
this._super(); //Always call this!
})
Make sure you always call _super when overwriting controller'sdestroy event. The base destroy functionality unbinds all event handlers thecontroller has created.
You could call destroy manually on an element with ChangeText added like:
$("#changed").change_text("destroy");
API
controller.destroy() ->undefined
returns {undefined}
jQuery.Controller.prototype.element attribute
The controller instance's delegated element. This is set by setup.It is a jQuery wrapped element.
For example, if I add MyWidget to a '#myelement' element like:
$.Controller("MyWidget",{
init : function(){
this.element.css("color","red")
}
})
$("#myelement").my_widget()
MyWidget will turn #myelement's font color red.
Using a different element.
Sometimes, you want a different element to be this.element. A very commonexample is making progressively enhanced form widgets.
To change this.element, overwrite Controller's setup method like:
$.Controller("Combobox",{
setup : function(el, options){
this.oldElement = $(el);
var newEl = $('<div/>');
this.oldElement.wrap(newEl);
this._super(newEl, options);
},
init : function(){
this.element //-> the div
},
".option click" : function(){
// eventhandler bound on the div
},
destroy : function(){
var div = this.element; //save reference
this._super();
div.replaceWith(this.oldElement);
}
}
jQuery.Controller.prototype.find function
Queries from the controller's element.
".destroy_all click" : function() {
this.find(".todos").remove();
}
API
controller.find(selector) ->jQuery.fn
selector {String}
selection string
returns {jQuery.fn}
returns the matched elements
jQuery.Controller.prototype.init function
Implement this.
API
controller.init()
jQuery.Controller.prototype.options attribute
Options are used to configure an controller. They are the 2nd argumentpassed to a controller (or the first argument passed to the controller'sjQuery plugin).
For example:
$.Controller('Hello')
var h1 = new Hello($('#content1'), {message: 'World'} );
equal( h1.options.message , "World" )
var h2 = $('#content2').hello({message: 'There'})
.controller();
equal( h2.options.message , "There" )
Options are merged with defaultsin setup.
For example:
$.Controller("Tabs",
{
defaults : {
activeClass: "ui-active-state"
}
},
{
init : function(){
this.element.addClass(this.options.activeClass);
}
})
$("#tabs1").tabs() // adds 'ui-active-state'
$("#tabs2").tabs({activeClass : 'active'}) // adds 'active'
Options are typically updated by calling update;
jQuery.Controller.prototype.setup function
Setup is where most of controller's magic happens. It does the following:
1. Sets this.element
The first parameter passed to new Controller(el, options) is expected tobe an element. This gets converted to a jQuery wrapped element and set as this.element.
2. Adds the controller's name to the element's className.
Controller adds it's plugin name to the element's className for easierdebugging. For example, if your Controller is named "Foo.Bar", itadds "foo_bar" to the className.
3. Saves the controller in $.data
A reference to the controller instance is saved in $.data. You can findinstances of "Foo.Bar" like:
$("#el").data("controllers")['foo_bar'].
Binds event handlers
Setup does the event binding described in ListeningTo Events.
API
controller.setup(element,options) -> Array
element {HTMLElement}
the element this instance operates on.
options {optional:Object}
option values for the controller. These get added to this.options andmerged with defaults.
returns {Array}
return an array if you wan to change what init is called with. By defaultit is called with the element and options passed to the controller.
jQuery.Controller.prototype.update function
Update extends this.optionswith the options argument and rebinds all events. It basicallyre-configures the controller.
For example, the following controller wraps a recipe form. When the formis submitted, it creates the recipe on the server. When the recipe is created, it resets the form with a new instance.
$.Controller('Creator',{
"{recipe} created" : function(){
this.update({recipe : new Recipe()});
this.element[0].reset();
this.find("[type=submit]").val("Create Recipe")
},
"submit" : function(el, ev){
ev.preventDefault();
var recipe = this.options.recipe;
recipe.attrs( this.element.formParams());
this.find("[type=submit]").val("Saving...")
recipe.save();
}
});
$('#createRecipes').creator({recipe : new Recipe()})
Demo
HTML
<form class="creator" action=""id="createRecipes">
<input name="name" type="text">
<input value="Create Recipe" type="submit">
</form>
Source
steal("jquery/controller",
"jquery/model",
"jquery/dom/form_params",
"jquery/dom/fixture",
function(){
$.fixture.delay = 2000;
$.fixture("POST /recipes",function(){
return {};
})
$.Model('Recipe',{
create: "/recipes"
},{});
$.Controller('Creator',{
"{recipe} created" : function(){
this.update({recipe : new Recipe()});
this.element[0].reset();
this.find("[type=submit]").val("Create Recipe")
},
"submit" : function(el, ev){
ev.preventDefault();
var recipe = this.options.recipe;
recipe.attrs( this.element.formParams());
this.find("[type=submit]").val("Saving...")
recipe.save();
}
});
$('#createRecipes').creator({recipe : new Recipe()})
})
Update is called if a controller's jQueryhelper is called on an element that already has a controller instance ofthe same type.
For example, a widget that listens for model updates and updates it's htmlwould look like.
$.Controller('Updater',{
// when the controller iscreated, update the html
init : function(){
this.updateView();
},
// update the html with atemplate
updateView : function(){
this.element.html( "content.ejs",
this.options.model);
},
// if the model is updated
"{model} updated" : function(){
this.updateView();
},
update : function(options){
// makesure you call super
this._super(options);
this.updateView();
}
})
// create the controller
// this calls init
$('#item').updater({model:recipe1});
// later, update that model
// this calls "{model} updated"
recipe1.update({name: "something new"});
// later, update the controller with a new recipe
// this calls update
$('#item').updater({model:recipe2});
// later, update the new model
// this calls "{model} updated"
recipe2.update({name: "something newer"});
NOTE: If you overwrite update, you probably need to call this._super.
Example
$.Controller("Thing",{
init: function( el, options ) {
alert( 'init:'+this.options.prop )
},
update: function( options ) {
this._super(options);
alert('update:'+this.options.prop)
}
});
$('#myel').thing({prop: 'val1'}); // alerts init:val1
$('#myel').thing({prop: 'val2'}); // alerts update:val2
API
controller.update(options) ->undefined
options {Object}
A list of options to merge with this.options.Often, this method is called by the jQueryhelper function.
returns {undefined}
jQuery.Controller.prototype.view function
tags: view
plugin: jquery/controller/view
Renders a View template with the controller instance. If the firstargument is not supplied, it looks for a view in /views/controllername/actionname.ejs.If data is not provided, it uses the controller instance as data.
TasksController =$.Controller.extend('TasksController',{
click: function( el ) {
// renderswith views/tasks/click.ejs
el.html( this.view() )
// renderswith views/tasks/under.ejs
el.after( this.view("under", [1,2]) );
// renderswith views/tasks/under.micro
el.after( this.view("under.micro", [1,2]) );
// renderswith views/shared/top.ejs
el.before( this.view("shared/top", {phrase: "hi"}) );
}
})
API
controller.view(view, data,myhelpers) -> String
view {optional:String}
The view you are going to render. If a view isn't explicity given thisfunction will try to guess at the correct view as show in the example codeabove.
data {optional:Object}
data to be provided to the view. If not present, the controller instanceis used.
myhelpers {optional:Object}
an object of helpers that will be available in the view. If not presentthis controller class's "Helpers" property will be used.
returns {String}
the rendered result of the view.
jQuery.Controller.static.defaults attribute
A object of name-value pairs that act as default values for a controller'soptions.
$.Controller("Message",
{
defaults : {
message : "Hello World"
}
},{
init : function(){
this.element.text(this.options.message);
}
})
$("#el1").message(); //writes "HelloWorld"
$("#el12").message({message: "hi"}); //writes hi
In setupthe options passed to the controller are merged with defaults. This is not a deepmerge.
jQuery.Controller.static.listensTo attribute
An array of special events this controller listens too. You only need toadd event names that are whole words (ie have no special characters).
$.Controller('TabPanel',{
listensTo : ['show']
},{
'show' : function(){
this.element.show();
}
})
$('.foo').tab_panel().trigger("show");
jQuery.Controller.static.pluginName attribute
Setting the pluginName property allows you to change the jQuery plugin helpername from its default value.
$.Controller("Mxui.Layout.Fill",{
pluginName: "fillWith"
},{});
$("#foo").fillWith();
jQuery.Controller.static.processors attribute
An object of {eventName : function} pairs that Controller uses to hook upevents auto-magically. A processor function looks like:
jQuery.Controller.processors.
myprocessor = function( el,event, selector, cb, controller ) {
//el - thecontroller's element
//event -the event (myprocessor)
//selector- the left of the selector
//cb - thefunction to call
//controller- the binding controller
};
This would bind anything like: "foo~3242 myprocessor".
The processor must return a function that when called, unbinds the eventhandler.
Controller already has processors for the following events:
- change
- click
- contextmenu
- dblclick
- focusin
- focusout
- keydown
- keyup
- keypress
- mousedown
- mouseenter
- mouseleave
- mousemove
- mouseout
- mouseover
- mouseup
- reset
- resize
- scroll
- select
- submit
Listen to events on the document or window with templated event handlers:
$.Controller('Sized',{
"{window} resize" : function(){
this.element.width(this.element.parent().width() / 2);
}
});
$('.foo').sized();
jQuery.Controller.static.processors.subscribe function
plugin: jquery/controller/subscribe
Adds OpenAjax.Hub subscribing to controllers.
$.Controller("Subscriber",{
"recipe.updatedsubscribe" : function(called, recipe){
},
"todo.* subscribe" : function(called, todo){
}
})
You should typically be listening to jQuery triggered events whencommunicating between controllers. Subscribe should be used for listening tomodel changes.
API
This is the call signiture for the processor, not the controllersubscription callbacks.
API
$.Controller.processors.subscribe(el,event, selector, cb, controller) -> undefined
el {HTMLElement}
the element being bound. This isn't used.
event {String}
the event type (subscribe).
selector {String}
the subscription name
cb {String}
the callback function's name
controller {}
returns {undefined}
jQuery.Controller.static.setup function
Does 2 things:
- Creates a jQuery helper for this controller.
- Calculates and caches which functions listen for events.
jQuery Helper Naming Examples
"TaskController" -> $().task_controller()
"Controllers.Task" -> $().controllers_task()
API
$.Controller.setup() ->undefined
returns {undefined}