can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
383 lines (274 loc) • 12.3 kB
Markdown
Control
canjs
can/route
can/route/test.html
can/control/test.html
can.Construct
widget factory with declarative event binding.
can.Control.plugins plugins
../docco/control/control.html docco
Create organized, memory-leak free, rapidly performing,
stateful controls with declarative event binding. Use `can.Control` to create UI
controls like tabs, grids, and context menus,
and organize them into higher-order business rules with
[can.route]. It can serve as both a traditional view and a traditional controller.
`can.Control( [staticProperties,] instanceProperties )`
Create a new, extended, control constructor
function. This functionality is inherited from [can.Construct] and is deprecated in favor of using
[can.Control.extend].
{Object} [staticProperties] An object of properties and methods that are added the control constructor
function directly. The most common property to add is [can.Control.defaults].
{Object} instanceProperties An object of properties and methods that belong to
instances of the `can.Control` constructor function. These properties are added to the
control's `prototype` object. Properties that
look like event handlers (ex: `"click"` or `"li mouseenter"`) are setup
as event handlers (see [Listening to events](#section_Listeningtoevents)).
{function(new:can.Construct,element,options)} A control constructor function that has been
extended with the provided `staticProperties` and `instanceProperties`.
`new can.Control( element, options )`
Create an instance of a control. [can.Control.prototype.setup] processes
the arguments and sets up event binding. Write your initialization
code in [can.Control.prototype.init]. Note, you never call `new can.Control()` directly,
instead, you call it on constructor functions extended from `can.Control`.
{HTMLElement|can.NodeList|CSSSelectorString} element Specifies the element the control
will be created on.
{Object} [options] Option values merged with [can.Control.defaults can.Control.defaults]
and set as [can.Control.prototype.options this.options].
{can.Control} A new instance of the constructor function extending can.Control.
## The Control Lifecycle
The following walks through a control's lifecycle
with an example todo list widget. It's broken up into the following
lifecycle events:
- Extending a control
- Creating a control instance
- Listening to events
- Destroying a control
## Extending a control
The following example builds up a basic todos widget for listing
and completing todo items. Start by creating a control constructor
function of your own by extending [can.Control] and defining an instance init method.
var Todos = can.Control.extend({
init: function( element, options ) { ... }
});
## Creating a control instance
Create an instance of the Todos control on the `todos` element with:
var todosControl = new Todos( '#todos', {} );
The control's associated [can.ejs EJS] template looks like:
<% todos.each(function( todo ) { %>
<li <%= (el) -> el.data( 'todo', todo ) %> >
<%= todo.attr( 'name' ) %>
<a href="javascript://" class="destroy">
</li>
<% }) %>
### `init(element, options)`
[can.Control.prototype.init] is called with the below arguments when new instances of [can.Control] are created:
- __element__ - The wrapped element passed to the
control. Control accepts a
raw HTMLElement, a CSS selector, or a NodeList. This is
set as `this.element` on the control instance.
- __options__ - The second argument passed to new Control, extended with
the can.Control's static __defaults__. This is set as
`this.options` on the control instance. Note that static is used
formally to indicate that _default values are shared across control instances_.
Any additional arguments provided to the constructor will be passed as normal. Use [can.view] to produce a document fragment
from your template and inject it in the passed element. Note that the `todos` parameter passed to [can.view] below
is an instance of [can.List]:
var Todos = can.Control.extend({
//defaults are merged into the options arg provided to the constructor
defaults : { view: 'todos.ejs' }
}, {
init: function( element , options ) {
//create a pointer to the control's scope
var self = this;
//run the Todo model's .findAll() method to produce a can.List
Todo.findAll( {}, function( todos ) {
//create a document fragment with can.view
//and inject it into the provided element's body
self.element.html( can.view(self.options.view, todos) );
});
}
});
// create a Todos Control with default options
new Todos( document.body.firstElementChild );
// overwrite the template default
new Todos( '#todos', { template: 'specialTodos.ejs' } );
### `this.element`
[can.Control::element] is the
NodeList consisting of the element the control is created on.
var todosControl = new Todos( document.body.firstElementChild );
todosControl.element[0] //-> document.body.firstElementChild
Each library wraps elements differently. If you are using jQuery, for example,
the element is wrapped with `jQuery( element )`.
### `this.options`
[can.Control::options] is the second argument passed to
`new can.Control()`, merged with the control's static __defaults__ property.
## Listening to events
Control automatically binds prototype methods that look
like event handlers. Listen to __click__'s on `<li>` elements like:
var Todos = can.Control.extend({
init: function( element , options ) {...},
'li click': function( li, event ) {
console.log( 'You clicked', li.text() );
// let other controls know what happened
li.trigger( 'selected' );
}
});
When an `<li>` is clicked, `"li click"` is called with:
- The library-wrapped __element__ that was clicked
- The __event__ data
Control uses event delegation, so you can add `<li>`s without needing to rebind
event handlers.
To destroy a todo when its `<a href="javascript://" class="destroy">` link
is clicked:
var Todos = can.Control.extend({
init: function( element, options ) {...},
'li click': function( li ) {...},
'li .destroy click': function( el, ev ) {
// get the li element that has todo data
var li = el.closest( 'li' );
// get the model
var todo = li.data( 'todo' );
//destroy it
todo.destroy();
}
});
When the todo is destroyed, EJS's live binding will remove its LI automatically.
### Templated Event Handlers Part 1 `"{eventName}"`
Customize event handler behavior with `"{NAME}"` in
the event handler name. The following allows customization
of the event that destroys a todo:
var Todos = can.Control.extend({
init: function( element , options ) { ... },
'li click': function( li ) { ... },
'li .destroy {destroyEvent}': function( el, ev ) {
// previous destroy code here
}
});
// create Todos with this.options.destroyEvent
new Todos( '#todos', { destroyEvent: 'mouseenter' } );
Values inside `{NAME}` are looked up on the control's `this.options` first,
and then the `window`. For example, we could customize it instead like:
var Todos = can.Control.extend({
init: function( element , options ) { ... },
'li click': function( li ) { ... },
'li .destroy {Events.destroy}': function( el, ev ) {
// previous destroy code here
}
});
// Events config
Events = { destroy: 'click' };
// Events.destroy is looked up on the window.
new Todos( '#todos' );
The selector can also be templated.
var Todos = can.Control.extend({
init: function( element , options ) { ... },
'{listElement} click': function( li ) { ... },
'{listElement} .destroy {destroyEvent}': function( el, ev ) {
// previous destroy code here
}
});
// create Todos with this.options.destroyEvent
new Todos( '#todos', {
destroyEvent: 'mouseenter',
listElement: 'li'
} );
### Templated Event Handlers Part 2 `"{objectName}"`
Control can also bind to objects other than `this.element` with
templated event handlers. This is _critical_
for avoiding memory leaks that are so common among MVC applications.
If the value inside `{NAME}` is an object, Control will bind to that
object to listen for events. For example, the following tooltip listens to
clicks on the window:
var Tooltip = can.Control.extend({
'{window} click': function( el, ev ) {
// hide only if we clicked outside the tooltip
if ( !this.element.has( ev.target ) ) {
this.element.remove();
}
}
});
// create a Tooltip
new Tooltip( $( '<div>INFO</div>' ).appendTo( el ) );
This is convenient when listening for model changes. If EJS were not
taking care of removing `<li>`s after their associated models were destroyed,
we could implement it in `Todos` like:
var Todos = can.Control.extend({
init: function( element, options ) {...},
'li click': function( li ) {...},
'li .destroy click': function( el, ev ) {
// get the li element that has todo data
var li = el.closest( 'li' );
// get the model
var todo = li.data( 'todo' );
//destroy it
todo.destroy();
},
'{Todo} destroyed': function( Todo, ev, todoDestroyed ) {
// find where the element
var index = this.todosList.indexOf( todoDestroyed );
this.element.children( ':nth-child(' + ( index + 1 ) + ')' )
.remove();
}
});
new Todos( '#todos' );
### `on()`
[can.Control::on] rebinds a control's event handlers. This is useful when you want
to listen to a specific model and change it:
var Editor = can.Control.extend({
todo: function( todo ) {
this.options.todo = todo;
this.on();
this.setName();
},
// a helper that sets the value of the input
// to the todo's name
setName: function() {
this.element.val( this.options.todo.name );
},
// listen for changes in the todo
// and update the input
'{todo} updated': function() {
this.setName();
},
// when the input changes
// update the todo instance
'change': function() {
var todo = this.options.todo;
todo.attr( 'name', this.element.val() );
todo.save();
}
});
var todo1 = new Todo({ id: 6, name: 'trash' }),
todo2 = new Todo({ id: 6, name: 'dishes' });
// create the editor;
var editor = new Editor( '#editor' );
// show the first todo
editor.todo( todo1 );
// switch it to the second todo
editor.todo( todo2 );
## Destroying a control
[can.Control::destroy] unbinds a control's
event handlers and releases its element, but does not remove
the element from the page.
var todo = new Todos( '#todos' );
todo.destroy();
When a control's element is removed from the page
__destroy__ is called automatically.
new Todos( '#todos' );
$( '#todos' ).remove();
All event handlers bound with Control are unbound when the control
is destroyed (or its element is removed).
_Brief aside on destroy and templated event binding. Taken
together, templated event binding, and control's automatic
clean-up make it almost impossible
to write leaking applications. An application that uses
only templated event handlers on controls within the body
could free up all
data by calling `$(document.body).empty()`._
## Tabs Example
Here is an example of how to build a simple tab widget using can.Control:
<iframe style="width: 100%; height: 300px"
src="//jsfiddle.net/donejs/kXLLt/embedded/result,html,js,css"
allowfullscreen="allowfullscreen"
frameborder="0">JSFiddle</iframe>
can.