CanJS - Control

canjs

What is can.Control?

can.Control is used to 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 is a widget factory used to organize event handlers and create stateful UI controls.

var Tabs = can.Control({
  init: function( el ) {
    // show first tab
  },
  'li  click': function( el, ev ) {
    // hide other tabs
    // show selected tab
  }
});

new Tabs('#tabs');

It can also be used with can.route to organize higher order business rules.

Controls 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 a subclass of 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:

var Todos = can.Control({
    init: function(el, options) {
        var self = this;
        Todo.findAll({}, function(todos) {
            self.element.html(can.view('todoList', todos));
        });
    }
});

Should we use can.Control() or should we use can.Control.extend()?

can.Control(…) is deprecated. We should use can.Control.extend(…) instead.

var Todos = can.Control.extend({
    init: function( element, options ) { ... }
});

var todosControl = new Todos( '#todos', {} );

How can we create a control?

To instantiate a control, pass it a selector, element, or library-wrapped NodeList that corresponds to the DOM element you want the Control to use as the containing element (which will be 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:

var todosList = new Todos('#todos', {});

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:

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'});

How can we listen to events?

Controls will automatically bind instance methods that look like event handlers. On this Control, click events on elements inside this.element will trigger the console log to be written to:

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());
    }
});

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.

How can we delete a model instance?

One of the things that 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:

<script type="text/ejs" id="todoList">
    <% this.each(function(todo) { %>
        <li <%= (el) -> el.data('todo', todo) %>>
            <%= todo.attr('description'); %>
            <a class="destroy">X</a>
        </li>
    <% }) %>
</script>

We should put an event listener on our Todos Control to remove a Todo when its destruction link is clicked:

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();
    }
});

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 live binding will remove the appropriate <li> automatically.

What is a templating event handler?

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:

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'});

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:

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();
        }
    }
});

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:

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);
    }
});

How can we unbind and rebind all events?

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.

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);

How can we destroy a control?

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.

var list = new Todos('#todos');
$('#todos').length; // 1
list.destroy();
$('#todos').length; // 1

However, when a Control's element is removed from the page, destroy is called on the Control.

How should we implement the init function for our control?

The init method is called with the following parameters:

init(element, options)
  1. 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.
  2. 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.

How can we define a view for our control?

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' } );

What is the purpose of this.element from inside a control?

It is the NodeList consisting of the element the control is created on. Each library wraps elements differently. If you are using jQuery, for example, the element is wrapped with jQuery( element ).

What is the purpose of this.options from inside a control?

It is the second argument passed to new can.Control(), merged with the control's static defaults property.

How can we define event handlers for our controls?

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.

How can we destroy a control?

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();
        }
    }
);

The control's associated EJS template looks like:

<% todos.each(function( todo ) { %>
    <li <%= (el) -> el.data( 'todo', todo ) %> >
        <%= todo.attr( 'name' ) %>
            <a href="javascript://" class="destroy">
    </li>
<% }) %>

When the todo is destroyed, EJS's live binding will automatically remove the <li>.

What is templated event handlers?

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' } );

In the above code, the user can customize or specify the event type that trigger the destroyEvent.

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' 
});

Control can also bind to objects other than this.element with templated event handlers. If the value inside {NAME} is an object, Control will bind to that object to listen for events.

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 ) );

The above code listen for clicks on the window object.

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' );

What is the purpose of the on method of a control?

It 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' });
var 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 );

How can we implement a tab widget using can.Control?

HTML:
<ul id="tabs" class="tabs ui-helper-clearfix">
  <li><a href="#tab1">Model</a></li>
  <li><a href="#tab2">View</a></li>
  <li><a href="#tab3">Controller</a></li>
</ul>
<div id="tab1" class="tab">
    Model Content
</div>
<div id="tab2" class="tab">
    View Content
</div>
<div id="tab3" class="tab">
    Controller Content
</div>

CSS:
Not relevant

var Tabs = can.Control.extend(
    {
        init: function( el ) {
            // activate the first tab
            $( el ).children( 'li:first' ).addClass( 'active' );
            // hide the other tabs
            var tab = this.tab;
            this.element.children( 'li:gt(0)' ).each(function() {
                tab( $( this ) ).hide();
            });
        },

        // helper function finds the tab for a given li
        tab: function( li ) {
            return $( li.find( 'a' ).attr( 'href' ) );
        },

        // hides old active tab, shows new one
        'li click': function( el, ev ) {
            ev.preventDefault();
            this.tab( this.element.find( '.active' ).removeClass( 'active' ) ).hide();
            this.tab( el.addClass( 'active' ) ).show();
        }
    }
);

// adds the controller to the element
new Tabs( '#tabs' );

In what circumstances do we want to override the destroy method of a control?

Sometimes, you want to reset a controlled element back to its original state when the control is destroyed. Overwriting destroy lets you write teardown code of this manner. When overwriting destroy, make sure you call Control's base functionality. The following example changes an element's text when the control is created and sets it back when the control is removed:

Changer = can.Control.extend({
    init: function() {
        this.oldText = this.element.text();
        this.element.text( "Changed!!!" );
    },
    destroy: function() {
        this.element.text( this.oldText );
        can.Control.prototype.destroy.call( this );
    }
});

// create a changer which changes #myel's text
var changer = new Changer( '#myel' );

// destroy changer which will reset it
changer.destroy();

Inside a control, what is the purpose of this.element?

HelloWorld = can.Control({
    init: function(){
        this.element.text( 'Hello World' );
    }
});

// create the controller on the element
new HelloWorld( document.getElementById( '#helloworld' ) );

It represents the controlled element that is passed to the constructor:

var Todos = can.Control.extend({
    init: function( element, options ) { ... }
});

var todosControl = new Todos( '#todos', {} );

this.element is a wrapped NodeList of one HTMLELement (or window). This is for convenience in libraries like jQuery where all methods operate only on a NodeList. To get the raw HTMLElement, write:

this.element[0] //-> HTMLElement

Inside our control, can we change the meaning of this.element?

Yes. Sometimes you don't want what's passed to new can.Control to be this.element. You can change this by overwriting setup or by unbinding, setting this.element, and rebinding.

move: function( newElement ) {
    this.off();
        this.element = $( newElement );
    this.on();
}

How can we override can.Control setup?

The following Combobox overwrites setup to wrap a select element with a div. That div is used as this.element. Notice how destroy sets back the original element.

Combobox = can.Control({
    setup: function( el, options ) {
        this.oldElement = $( el );
        var newEl = $( '<div/>' );
        this.oldElement.wrap( newEl );
        can.Control.prototype.setup.call( this, newEl, options );
    },
    init: function() {
        this.element //-> the div
    },
    ".option click": function() {
        // event handler bound on the div
    },
    destroy: function() {
        var div = this.element; //save reference
        can.Control.prototype.destroy.call( this );
        div.replaceWith( this.oldElement );
    }
});

What is the purpose of the on method?

Bind an event handler to a Control, or rebind all event handlers on a Control.

What is the signature for the on method?

control.on([el,] selector, eventName, func)
  1. el: The element to be bound. If no element is provided, the control's element is used instead.
  2. selector: A CSS selector for event delegation.
  3. eventName: The name of the event to listen for.
  4. func: A callback function or the String name of a control function. If a control function name is given, the control function is called back with the bound element and event as the first and second parameter. Otherwise the function is called back like a normal bind.

The on method returns the id of the binding in this._bindings.

on(el, selector, eventName, func) binds an event handler for an event to a selector under the scope of the given element.

What is the purpose of the on method when called without any parameters?

Rebind all of a control's event handlers. This function returns the number of handlers bound to this Control.

this.on() is used to rebind all event handlers when this.options has changed. It can also be used to bind or delegate from other elements or objects.

What do we mean when we rebind events?

By using templated event handlers, a control can listen to objects outside this.element. This is extremely common in MVC programming. For example, the following control might listen to a task model's completed property and toggle a strike className like:

TaskStriker = can.Control({
    "{task} completed": function(){
        this.update();
    },
    update: function(){
        if ( this.options.task.completed ) {
            this.element.addClass( 'strike' );
        } else {
            this.element.removeClass( 'strike' );
        }
    }
});

var taskstriker = new TaskStriker({
    task: new Task({ completed: 'true' })
});

To update the taskstriker's task, add a task method that updates this.options and rebinds the event handlers for the new task like:

TaskStriker = can.Control({
    "{task} completed": function(){
        this.update();
    },
    update: function() {
        if ( this.options.task.completed ) {
            this.element.addClass( 'strike' );
        } else {
            this.element.removeClass( 'strike' );
        }
    },
    task: function( newTask ) {
        this.options.task = newTask;
        this.on();
        this.update();
    }
});
var taskstriker = new TaskStriker({
    task: new Task({ completed: true })
});

// Now, add a new task that is not yet completed
taskstriker.task(new Task({ completed: false }));

How can we add new events?

If events need to be bound to outside of the control and templated event handlers are not sufficient, you can call this.on to bind or delegate programmatically:

init: function() {
    // calls somethingClicked( el, ev )
    this.on( 'click', 'somethingClicked' );
    // calls function when the window is clicked
    this.on( window, 'click', function( ev ) {
        // do something
    });
},
somethingClicked: function( el, ev ) {
    // ...
}

Inside a control, what is the purpose of this.options?

The this.options property is an Object that contains configuration data passed to a control when it is created (new can.Control(element, options)).

var Greeting = can.Control.extend({
    init: function(){
        this.element.text( this.options.message )
    }
});
new Greeting("#greeting",{message: "I understand this.options"});

The options argument passed when creating the control is merged with defaults in setup.

var Greeting = can.Control.extend(
    {
        defaults: {
            message: "Defaults merged into this.options"
        }
    },{
        init: function(){
            this.element.text( this.options.message )
        }
    }
);
new Greeting("#greeting");

In the above code, if no message property is provided, the defaults' message property is used.

What is the purpose of the setup method?

Perform pre-initialization logic for control instances and classes.

control.setup(element, options)
  1. element: {HTMLElement | NodeList | String}. The element as passed to the constructor.
  2. options: {Object}. option values for the control. These get added to this.options and merged with [can.Control.static.defaults defaults].

This setup function returns an array if you want to change what init is called with. By default it is called with the element and options passed to the control.

setup when called, does the following:

  1. Sets this.element: The first parameter passed to new Control( el, options ) is expected to be an element. This gets converted to a Wrapped NodeList element and set as this.element.
  2. Adds the control's name to the element's className: Control adds it's plugin name to the element's className for easier debugging. For example, if your Control is named "Foo.Bar", it adds "foo_bar" to the className.
  3. Saves the control in $.data: A reference to the control instance is saved in $.data. You can find instances of "Foo.Bar" like: $( '#el' ).data( 'controls' )[ 'foo_bar' ]
  4. Merges Options: Merges the default options with optional user-supplied ones. Additionally, default values are exposed in the static [can.Control.static.defaults defaults] so that users can change them.
  5. Binds event handlers: Setup does the event binding described in can.Control.

How can we specify default values?

Message = can.Control.extend(
    {
        defaults: {
            message: "Hello World"
        }
    }, {
        init: function(){
        this.element.text( this.options.message );
    }
});

New instances of a can.Control will create a shallow copy of the default options. Be aware as shallow copies keep a reference to object types, such as objects, maps and computes.

var Sample = can.Control.extend(
    {
        defaults: {
            computedProp: can.compute(),
            primitiveProp: 'sample'
        }
    }, {
    }
);

var a = new Sample('div');
var b = new Sample('li');

//`computedProp` will be shared across instances of the `Sample` control.
//a.options.computedProp === b.options.computedProp

What is the purpose of can.Control.route?

The can.Control.route plugin adds a route processor to can.Control. This allows creating routes and binding to can.route in a single step by listening to the route event and a route part. Route events will be triggered whenever the route changes to the route part the control is listening to. For example:

var Router = can.Control(
    {
        init : function(el, options) {
        },
        ":type route" : function(data) {
            // the route says anything but todo
        },
        "todo/:id route" : function(data) {
            // the route says todo/[id]
            // data.id is the id or default value
        },
        "route" : function(data){
            // the route is empty
        }
    }
);
new Router(window);

route without a route part will get called when the route is empty. The data passed to the event handler is the serialized route data without the route attribute.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License