-
Notifications
You must be signed in to change notification settings - Fork 1
controls
@page Controls Controls @parent Tutorial 6
@body Controls made with can.Control are organized, memory-leak free, performant, stateful UI controls. can.Control lets you create controls like tabs, grids, context menus, and forms, and helps you organize them into higher-order business units, tying them all together with can.route. Controls fill the traditional MVC controller role, managing data through Models made with can.Model and directing it to be displayed through views made with can.view.
Because Controls are Constructs, you can create control constructors and instances just like with can.Construct. Here's what the constructor for a Control that manages a Todo list might look like:
@codestart var Todos = can.Control({ init: function(el, options) { var self = this; Todo.findAll({}, function(todos) { self.element.html(can.view('todoList', todos)); }); } }); @codeend
When you instantiate a control, pass it a selector, element, or library-wrapped
NodeList that corresponds to the DOM element you want the Control to set to
this.element for that Control. Also pass the control an object with any options
for that particular instance. These options will be extended off of the Control's
constructor's static defaults and set as this.options for that Control.
Here we'll initiate a Todos controller to hang off of the element with ID todos and with no options supplied:
@codestart var todosList = new Todos('#todos', {}); @codeend
If you specify a method called init when creating your Control's constructor,
that method will be called when a new instance of that Control is created. The
init method gets passed a library-wrapped NodeList containing this.element
as the first parameter and this.options as the second parameter. Any other
parameters you passed to the constructor during instantiation will also be passed
to init.
To demonstrate this, here is another version of the Todo list Control constructor that can have its view overridden, and the instantiation of that Control:
@codestart var Todos = can.Control({ defaults: { view: 'todos.ejs' } },{ init: function(el, options) { var self = this; Todo.findAll({}, function(todos) { self.element.html(can.view(this.options.view, todos)); }); } });
// this Control will use todos.ejs new Todos(document.body.firstChild);
// this Control will use todos2.ejs new Todos('#todoList', {view: 'todos2.ejs'}); @codeend
Controls will automatically bind instance methods that look like event handlers.
On this Control, click events on &lit;li> elements inside this.element will
trigger the console log to be written to:
@codestart var Todos = can.Control({ init: function(el, options) { var self = this; Todo.findAll({}, function(todos) { self.element.html(can.view('todoList', todos)); }); }, 'li click': function(el, ev) { console.log('You clicked ' + el.text()); } }); @codeend
The event handlers are passed a library-wrapped NodeList containing the element that was clicked, and the event. can.Control uses event delegation, so you don't need to rebind handlers when you add or remove elements.
One of the things we want to do with our to-do list is delete Todos. This is made easy with event handling in can.Control. Let's say that our view template looks like this:
@codestart <script type="text/ejs" id="todoList"> <% this.each(function(todo) { %> <li <%= (el) -> el.data('todo', todo) %>> <%= todo.attr('description'); %> <a class="destroy">X </li> <% }) %> </script> @codeend
We should put an event listener on our Todos Control to remove a Todo when its destruction link is clicked:
@codestart var Todos = can.Control({ init: function(el, options) { var self = this; Todo.findAll({}, function(todos) { self.element.html(can.view('todoList', todos)); }); }, 'li click': function(el, ev) { console.log('You clicked ' + el.text()); }, 'li .destroy click': function(el, ev) { var li = el.closest('li'), todo = li.data('todo');
todo.destroy();
}
}); @codeend
Destroying the Todo will take it out of the list of Todos being rendered (because the list of Todos passed into the template is a Model List), which will cause the template to re-render itself. This means that EJS's live binding will remove the appropriate <li> automatically.
If a variable is placed in braces in the event handler key, can.Control will
look up that key in the Control's options, and then on window. You can use
this to customize the events that cause handlers to fire:
@codestart var Todos = can.Control({ defaults: { destroyEvent: 'click' } },{ init: function(el, options) { var self = this; Todo.findAll({}, function(todos) { self.element.html(can.view(this.options.view, todos)); }); }, 'li .destroy {destroyEvent}': function(el, ev) { var li = el.closest('li'), todo = li.data('todo');
todo.destroy();
}
});
new Todos('#todos', {destroyEvent; 'mouseenter'}); @codeend
You can also use this to bind events to objects other that this.element
within Controls. This is critical for avoiding memory leaks that are
commonplace with other MVC applications and frameworks because it ensures that
these handlers get unbound when the control is destroyed:
@codestart var Tooltip = can.Control({ '{window} click': function(el, ev) { // hide only if we clicked outside the tooltip if(! this.element.has(ev.target).length) { this.element.remove(); } } }); @codeend
This is useful for listening to changes on models. Say that our live-binding did not take care of removing <li>s after the corresponding Model was destroyed. In that case, we could implement that functionality by listening to when Todos are destroyed:
@codestart var Todos = can.Control({ defaults: { destroyEvent: 'click' } },{ init: function(el, options) { var self = this; self.todosList = todos; Todo.findAll({}, function(todos) { self.element.html(can.view(this.options.view, todos)); }); }, 'li .destroy {destroyEvent}': function(el, ev) { var li = el.closest('li'), todo = li.data('todo');
todo.destroy();
},
'{Todo} destroyed': function(Todo, ev, destroyed) {
// find where the element is in the list
var index = this.todosList.indexOf(destroyed);
this.element.children(':nth-child(' + (index + 1) + ')').remove();
this.todosList.splice(index, 1);
}
}); @codeend
You can unbind and rebind all a Control's event handlers by calling on on it.
This is useful when a Control starts listening to a specific Model, and you want
to change which model it is listening to.
In the example below, an Editor Control keeps a reference to the specific Todo
it is editing. Its todo method calls on when the Todo being edited switches,
because it needs to rebind {todo} updated.
@codestart var Editor = can.Control({ setDesc: function() { this.element.val(this.options.todo.description); }, // change what Todo this Control points at todo: function(todo) { this.options.todo = todo; this.on(); this.setDesc(); }, // listen for changes in the Todo '{todo} updated': function() { this.setDesc(); }, // when the input changes, update the Todo ' change': function(el, ev) { this.options.todo.attr('description', el.val()); this.options.todo.save(); } });
var todo1 = new Todo({id: 7, description: 'Take out the trash.'}), todo2 = new Todo({id: 8, description: 'Wash the dishes.'}), editor = new Editor('#editor');
// start editing the first Todo editor.todo(todo1);
// switch to editing the second Todo editor.todo(todo2); @codeend
Calling destroy on a Control unbinds the Control's event handlers and removes
its association with its element, but it does not remove the element from the
page.
@codestart
var list = new Todos('#todos');
However, when a Control's element is removed from the page, destroy is called
on the Control.
Taken together, templated event binding and Control's automatic cleanup make it
nearly impossible to write applications with memory leaks. An application that
uses only templated event handlers on the controls within the body could free
up all the data it uses by calling $(document.body).empty().