can-control
Create organized, memory-leak free, rapidly performing, stateful controls with declarative event binding. Use 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.
Control( [staticProperties,] instanceProperties )
Create a new, extended, control constructor function. This functionality is inherited from can-construct and is deprecated in favor of using extend.
Parameters
- staticProperties
{Object}
:An object of properties and methods that are added the control constructor function directly. The most common property to add is defaults.
- instanceProperties
{Object}
:An object of properties and methods that belong to instances of the
Control
constructor function. These properties are added to the control'sprototype
object. Properties that look like event handlers (ex:"{element} click"
or"{element} li mouseenter"
) are setup as event handlers (see Listening to events).
Returns
{constructor(element, options) => can.Construct}
:
A control constructor function that has been
extended with the provided staticProperties
and instanceProperties
.
new 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 Control()
directly,
instead, you call it on constructor functions extended from Control
.
Parameters
- element
{HTMLElement|can-view-nodelist|CSSSelectorString}
:Specifies the element the control will be created on.
- options
{Object|can-map|can-define/map/map}
:Option values merged with Control.defaults and set as this.options. If options is an observable (CanMap / DefineMap), any values from defaults that do not exist on the observable will be set. The observable will then be set as this.options.
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 = 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 = 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', { view: 'specialTodos.ejs' } );
this.element
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
options is the second argument passed to
new 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 within this.element like:
var Todos = Control.extend({
init: function( element , options ) {...},
'{element} 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, "{element} 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 ) {...},
'{element} li click': function( li ) {...},
'{element} 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 = Control.extend({
init: function( element , options ) { ... },
'{element} li click': function( li ) { ... },
'{element} 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 = Control.extend({
init: function( element , options ) { ... },
'{element} li click': function( li ) { ... },
'{element} 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 = Control.extend({
init: function( element , options ) { ... },
'{element} {listElement} click': function( li ) { ... },
'{element} {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 = 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 = Control.extend({
init: function( element, options ) {...},
'{element} li click': function( li ) {...},
'{element} 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()
on rebinds a control's event handlers. This is useful when you want to listen to a specific model and change it:
var Editor = 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
'{element} 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
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 Control:
See the Pen CanJS 5.0 Hello World by Daniel Tian (@danieltian) on CodePen.