CanJS - Event Handling

canjs

http://blog.bitovi.com/training-series-events-part-1/
http://blog.bitovi.com/training-series-events-part-2/

How can we handle the click event using can.Control?

Using a list of people like previous recipes, clicking any individual person's name will remove that person from the list. Previous examples have used jQuery's event handlers:

$("#push").click(function(){
    //handle the event
});
var people = new can.List([
        {firstname: "John", lastname: "Doe"},
        {firstname: "Emily", lastname: "Dickinson"}
])

// pass the observable list into the can.view
var frag = can.view("app-template", {people: people})
$("#my-app").html(frag);

$("#push").click(function(){
    people.push({firstname: "Paul", lastname: "Newman"})
})

$("#pop").click(function(){
    people.pop();
})

In the previous example, the user click on a button (not a particular name on the list), and the code would remove the last person from the list. Notice that the code is still just a bunch of JavaScript which can appear anywhere. This code can be in app.js, or it can be in a component's specific JavaScript file.

CanJS provides a few different ways to respond to events. As well as making application code simpler, using CanJS to handle events can help to automatically prevent memory leaks. To handle events, extend can.Control:

var PeopleList = can.Control.extend({
    //behavior
});

You create a can.Control by calling it as a constructor function. The first argument is the element the control will be created on. The second argument is an object of options.

new PeopleList('#my-app', {people: people});

A can.Control handles events with functions declared with two arguments: an element or list of elements (using a jQuery-style selector) and a specific event. Below, this is 'li click', meaning when any li elements that are clicked the function will be called to handle the click event.

var PeopleList = can.Control.extend({
    init: function( element, options ){
        this.people = new can.List(options.people);
        this.element.html( can.view('app-template', {
            //defines people in the template as the observable can.List
            people: this.people
        }));
    },
    'li click': function( li, event ){
        //Handle the click event
    }
});

When the constructor function is called and the can.Control is instantiated:

  1. The init method is called
  2. An observable can.List is created from people
  3. The list is rendered using can.view so when the list changes, so will the view
var people = [
    {firstname: "John", lastname: "Doe"},
    {firstname: "Emily", lastname: "Dickinson"},
    {firstname: "William", lastname: "Adams"},
    {firstname: "Stevie", lastname: "Nicks"},
    {firstname: "Bob", lastname: "Barker"}
];

When the event handler for a click runs, it needs a way to access the object associated with the li that was clicked. With the data helper, the element will retain a reference to the object it is associated with (in this case, a person).

<ul>
    {{#each people}}
        <li {{data 'person'}}>
            {{lastname}}, {{firstname}}
        </li>
    {{/each}}
</ul>

Finally, the event handler must be defined. In a can.Control, an event handler function can be defined with a string containing a selector and an event. In this case, these are li and click, respectively, since we want to handle click events on each list item.

var PeopleList = can.Control.extend({
    init: function(){
        ...
    },
    'li click': function( li, event ) {
        var people = this.options.people;
        var person = li.data('person');
        var index = people.indexOf(person);
        people.splice(index, 1);
    }
});

When a user clicks a list item:

  1. The function bound to 'li click' is called
  2. The object associated with that list item is accessed using the data helper
  3. That 'person's data is removed from the observable list of people
  4. The template updates automatically

As a reminder, though event handlers respond to actions on the page, they should change application state or data (e.g. make a change to a can.Map) rather than modifying the DOM directly (e.g. toggling a class). This will update the page automatically, keeping code manageable.

How can we use jQuery's event delegation directly without using CanJS?

jQuery's event delegation should prevent memory leak (need confirmation). There is nothing preventing us from using jQuery's event delegation directly. We can put the code in app.js, or we can put it into the component's specific JavaScript file.

As a reminder, though event handlers respond to actions on the page, they should change application state or data (e.g. make a change to a can.Map) rather than modifying the DOM directly (e.g. toggling a class). This will update the page automatically, keeping code manageable.

How can we handle events inside a can.Component?

var people = [
    {firstname: "John", lastname: "Doe"},
    {firstname: "Emily", lastname: "Dickinson"},
    {firstname: "William", lastname: "Adams"},
    {firstname: "Stevie", lastname: "Nicks"},
    {firstname: "Bob", lastname: "Barker"}
];

can.Component.extend({
    tag: 'people',
    template: '<ul>' +
                '{{#each people}}' +
                '<li can-click="remove">' +
                    '{{lastname}}, {{firstname}}' +
                '</li>' +
                '{{/each}}' +
                '</ul>',
    scope: {
        people: people,
        remove: function( person ) {
            var people = this.attr("people");
            var index = people.indexOf(person);
            people.splice(index, 1);
        }
    }
});

var frag = can.view("app-template", {people: people});
$("#my-app").html(frag);
<script type="text/mustache" id="app-template">
    <people></people>
</script>
<!-- CanJS needs a place to put your application -->
<div id="my-app"></div>

In the above code, when we use can.Component.extend, we specify a custom tag 'people', an in-line template string, and a scope object, and then in our main template, we use '<people></people>'

A component is created wherever <people></people> appears in a template.

The scope object on a Component contains the component's state, data, and behavior. Here, it specifies how to remove a person from the list.

The template for the component itself is passed via the template property. This can either be an external file or a string. Each li uses can-click, which declares an event binding. Here, remove inside the component's scope will be called with the relevant people object as an argument.

What is the purpose of the 'events' object when used with can.Component.extend?

A component's events object is used to listen to events (that are not listened to with view bindings). The following component adds "!" to the message every time <hello-world> is clicked:

can.Component.extend({
    tag: "hello-world",
    template: can.stache("<h1>{{message}}</h1>"),
    events: {
        "click" : function(){
            var currentMessage = this.viewModel.attr("message");
            this.viewModel.attr("message", currentMessage+ "!")
        }
    }
});

Components have the ability to bind to special inserted and removed events that are called when a component's tag has been inserted into or removed from the page.

What is the preferred way to define event handlers for a component?

The preferred way is to not define event handlers using the 'events' object:

can.Component.extend({
    tag: "hello-world",
    template: can.stache("<h1>{{message}}</h1>"),
    events: {
        "click" : function(){
            var currentMessage = this.viewModel.attr("message");
            this.viewModel.attr("message", currentMessage+ "!")
        }
    }
});

but to define event handler using the can- prefix such as can-click:

var people = [
    {firstname: "John", lastname: "Doe"},
    {firstname: "Emily", lastname: "Dickinson"},
    {firstname: "William", lastname: "Adams"},
    {firstname: "Stevie", lastname: "Nicks"},
    {firstname: "Bob", lastname: "Barker"}
];

can.Component.extend({
    tag: 'people',
    template: '<ul>' +
                '{{#each people}}' +
                '<li can-click="remove">' +
                    '{{lastname}}, {{firstname}}' +
                '</li>' +
                '{{/each}}' +
                '</ul>',
    scope: {
        people: people,
        remove: function( person ) {
            var people = this.attr("people");
            var index = people.indexOf(person);
            people.splice(index, 1);
        }
    }
});

var frag = can.view("app-template", {people: people});
$("#my-app").html(frag);

While can.view.bindings conveniently allows you to call a viewModel method from a template like:

<input ($change)="doSomething"/>

This has the effect of binding an event handler directly to this element. Every element that has a can-click or similar attribute has an event handler bound to it. For a large grid or list, this could have a performance penalty.

By contrast, events bound using can.Component's events object use event delegation, which is useful for high performance template rendering. In a large grid or list, event delegation only binds a single event handler rather than one per row.

What is the purpose of the can.bind.call method?

Listen for events on an object.

can.bind.call(target, eventName, handler)
  1. target: {Object} the object that emits events.
  2. eventName: {String} the name of the event to listen for.
  3. handler: {function()} the function to execute when the event occurs.

The can.bind.call function returns an object, the target. The can.bind.call function binds a callback handler on an object for a given event. It works on HTML element, the window object, objects with the bind and unbind method, and objects.

The idea is that can.bind can be used on anything that produces events and it will figure out the appropriate way to bind to it. Typically, can.bind is only used internally to CanJS; however, if you are making libraries or extensions, use can.bind to listen to events independent of the underlying library.

var obj = {};
can.bind.call(obj,"something", function(ev, arg1, arg){
    arg1 // 1
    arg2 // 2
});

can.trigger(obj,"something",[1,2]);

var el = document.getElementById('foo')
can.bind.call(el, "click", function(ev){
    this // el
});

What is the purpose of the can.delegate.call function?

Listen for events from the children of an element.

can.delegate.call(element, selector, eventName, handler)
  • element: {HTMLElement} The HTML element to bind to.
  • selector: {String} A selector for delegating downward.
  • eventName: {String} The name of the event to listen for.
  • handler: {function()} The function to execute when the event occurs.

The can.delegate.call function returns an object, the element. The can.delegate.call function works on HTML element and the window. The idea is that delegate can be used on anything that produces delegate events and it will figure out the appropriate way to bind to it. Typically, can.delegate is only used internally to CanJS; however, if you are making libraries or extensions, use can.delegate to listen to events independent of the underlying library.

// Assuming an HTML body like the following:
// <div id="parent">
//     <div class="child">Hello</div>
// </div>
var el = document.getElementById('parent');
can.delegate.call(el, ".child", "click", function(ev) {
    return this; //-> el
});

What events does CanJS add to the base library?

  1. attributes - called when an element's attributes are changed
  2. inserted - called when an element is inserted into the document
  3. removed - called when an element is removed from the document

How can we listen to the attributes event?

The event object dispatched when an attribute changes on an element. This event object contains the following properties:

  1. attributeName: {String} The name of the attribute that was changed.
  2. oldValue: {String} The old value of the attribute.
  3. target: {HTMLElement} The attribute that changed.
  4. type="attributes": {String} The type property is always "attributes" for an attributes event.
  5. bubbles=false: {Boolean} Attributes events do not bubble.

To listen to an attributes event on an element with the base-library's NodeList. For example, with jQuery:

$(el).bind("attributes", function(ev){
    ev.type // "attributes"
    ev.attributeName // "title"
    ev.oldValue // ""
    ev.target // el
})
$(el).attr("title","Mr. Sprinkles")

To listen to an attributes event with can.Control like:

can.Control.extend({
    "attributes": function(el, ev){
        //
    }
})

To lsten to an attributes event with can.Component's events object like:

can.Component.extend({
    tag: "panel",
    events: {
        "attributes": function(el, ev){
            //
        }
    }
})

Unlike all other events in CanJS, "attributes" events are dispatched asynchronously.

How can we listen for the inserted event?

The event object dispatched when an element is inserted into the document. This event object contains the following properties:

  1. target: {HTMLElement} The attribute that changed.
  2. type="inserted": {String} The type is always "inserted" for an inserted event.
  3. bubbles=false: {Boolean} The inserted event type do not bubble.

To listen to an inserted event on an element with the base-library's NodeList. For example, with jQuery:

$(el).bind("inserted", function(ev){
    ev.type // "inserted"
    ev.target // el
})
$(parent).append(el);

To listen to an inserted event with can.Control like:

can.Control.extend({
    "inserted": function(el, ev){
        //
    }
})

To call a method when an element is inserted within a template like:

<div can-inserted="addItem">...</div>

To listen to an inserted event with can.Component's events object like:

can.Component.extend({
    tag: "panel",
    events:{
        "inserted": function(el, ev){
            //
        }
    }
)

To create an inserted event, you must use the base-library NodeList's DOM modifier methods. For jQuery or Zepto, use $.fn.html, $.fn.append, $.fn.after, etc:

$(parent).html(el);

How can we listen for the removed event?

The event object dispatched when an element is removed from the document. This event object contains the following properties:

  1. target: {HTMLElement} The attribute that changed.
  2. type="removed": {String} The type is always "removed" for a removed event.
  3. bubbles=false: {Boolean} The removed event type do not bubble.

To listen to a removed event on an element with the base-library's NodeList. For example, with jQuery:

$(el).bind("removed", function(ev){
    ev.type // "removed"
    ev.target // el
})
$(el).remove()

To listen to a removed event with can.Control like:

can.Control.extend({
    "removed": function(el, ev){
        //
    }
})

To call a method when an element is removed within a template like:

<div can-removed="destroyItem">...</div>

To listen to a removed event with can.Component's events object like:

can.Component.extend({
    tag: "panel",
    events: {
        "removed": function(el, ev){
            //
    }
})

To trigger a removed event, you must use the base-library NodeList's DOM modifier methods. For jQuery or Zepto, use $.fn.html, $.fn.remove, $.fn.empty, etc:

$(el).remove();

What is the purpose of the can.off.call function?

Stop listening for events on an object.

can.off.call(target, eventName, handler)
  1. target {Object}: The object that emits events.
  2. eventName {String}: The name of the event to listen for.
  3. handler {function()}: The function to unbind.

The can.off.call function returns an object, the target. The can.off(eventName, handler) is an alias for can.unbind(eventName, handler) and unbinds a callback handler from an object for a given event. It works on:

  1. HTML elements and the window
  2. Objects
  3. Objects with bind / unbind methods

The idea is that can.unbind can be used on anything that produces events and it will figure out the appropriate way to unbind to it. Typically, can.off is only used internally to CanJS; however, if you are making libraries or extensions, use can.on to listen to events independent of the underlying library.

// Binding/unbinding to an object
var obj = {},
handler = function(ev, arg1, arg) {
    arg1 // 1
    arg2 // 2
};
can.on.call(obj,"something", handler)
can.trigger(obj,"something",[1,2])
can.off.call(obj,"something", handler)

// Binding/unbinding to an HTMLElement
var el = document.getElementById('foo'),
handler = function(ev){
    this // el
};
can.on.call(el, "click", handler)
can.off.call(el, "click", handler)

What is the purpose of the can.on.call function?

Listen for events on an object. It is an alias for can.bind(eventName, handler) .

What is the purpose of the can.trigger function?

Trigger an event on an object.

can.trigger(target, eventName[, args])
  1. target {Object}: The object to trigger the event on.
  2. eventName {String}: The event to trigger.
  3. args {Array<*>}Optional: The event data.

The can.trigger(target, eventName) function triggers an artificial event on an object and fires all associated callback handlers that were bound to it. This is exceptionally handly for simulating events.

var button = document.createElement('button');
can.bind.call(button, 'click', function() {
    console.log('I have been clicked');
})
can.trigger(button, 'click');

What is the purpose fo the can.unbind.call function?

Stop listening for events on an object.

can.unbind.call(target, eventName, handler)
  1. target {Object}: The object that emits events.
  2. eventName {String}: The name of the event to unbind.
  3. handler {function()}: The function to unbind.

The can.unbind.call function returns an object, the target. The can.unbind(eventName, handler) unbinds a callback handler from an object for a given event. It works on:

  • HTML elements and the window
  • Objects
  • Objects with bind / unbind methods

The idea is that can.unbind can be used on anything that produces events and it will figure out the appropriate way to unbind to it. Typically, can.unbind is only used internally to CanJS; however, if you are making libraries or extensions, use can.bind to listen to events independent of the underlying library.

// Binding/unbinding to an object
var obj = {},
handler = function(ev, arg1, arg) {
    arg1 // 1
    arg2 // 2
};
can.bind.call(obj,"something", handler)
can.trigger(obj,"something",[1,2])
can.unbind.call(obj,"something", handler)

// Binding/unbinding to an HTMLElement
var el = document.getElementById('foo'),
handler = function(ev){
    this // el
};
can.bind.call(el, "click", handler)
can.unbind.call(el, "click", handler)

What is the purpose of the can.undelegate.call function?

Stop listening for events from the children of an element.

can.undelegate.call(element, selector, eventName, handler)
  1. element {HTMLElement}: The HTML element to unbind from.
  2. selector {String}: A selector for delegating downward.
  3. eventName {String}: The name of the event to listen for.
  4. handler {function()}: The function that was bound.

The can.undelegate.call function returns an object, the element. The can.undelegate(selector, eventName, handler) function unbinds a delegate handler on an object for a given event. It works on:

  • HTML elements and the window

The idea is that undelegate can be used on anything that produces delegate events and it will figure out the appropriate way to bind to it. Typically, can.undelegate is only used internally to CanJS; however, if you are making libraries or extensions, use can.undelegate to stop listening to events independent of the underlying library.

// Delegate/undelegate binding to an HTMLElement
// Assuming an HTML body like the following:
// <div id="parent">
//     <div class="child">Hello</div>
// </div>
var el = document.getElementById('parent');
var handler = function(ev) {
    return this; //-> el
};
can.delegate.call(el, ".child", "click", handler);
can.undelegate.call(el, ".child", "click", handler);

What is the purpose of the can.view.attr function?

can.view.attr is used to add custom behavior to elements that contain a specified html attribute. Typically it is used to mixin behavior (whereas can.view.tag is used to define behavior).

can.view.attr( attributeName, attrHandler(el, attrData) )
can.view.attr("tooltip", function( el, attrData ) {
    $(el)."tooltip"({
        content: el.getAttribute("tooltip"), 
        items: "[tooltip]"
    })
});

The above code adds a jQueryUI tooltip to any element that has a tooltip attribute like <div tooltip="Click to edit">Name</div>.

In the previous example, the content of the tooltip was static. However, it's likely that the tooltip's value might change. For instance, the template might want to dynamically update the tooltip like:

<button tooltip="{{deleteTooltip}}">
    Delete
</button>

Where deleteTooltip changes depending on how many users are selected:

deleteTooltip: function(){
    var selectedCount = selected.attr("length");
    if(selectedCount) {
        return "Delete "+selectedCount+" users";
    } else {
        return "Select users to delete them.";
    }
}

The attributes event can be used to listen to when the toolip attribute changes its value like:

can.view.attr("tooltip", function( el, attrData ) {
    var updateTooltip = function(){
        $(el).tooltip({
            content: el.getAttribute("tooltip"), 
            items: "[tooltip]"
        });
    };

    $(el).bind("attributes", function(ev){
        if(ev.attributeName === "tooltip") {
            updateTooltip();
        }
    });

    updateTooltip();

})

can.view.attr must be called before a template is processed. When using can.view to create a renderer function, can.view.attr must be called before the template is loaded, not simply before it is rendered.

//Call can.view.attr first
can.view.attr('tooltip', tooltipFunction);
//Preload a template for rendering
var renderer = can.view('app-template');
//No calls to can.view.attr after this will be used by `renderer`

How can we read values from the scope?

It's common that attribute mixins need complex, observable data to perform rich behavior. The attribute mixin is able to read data from the element's scope. For example, toggle and fade-in-when will need the value of showing in:

<button toggle="showing">
    {{#showing}}Show{{else}}Hide{{/showing}} more info</button>
<div fade-in-when="showing">
    Here is more info!
</div>

These values can be read from attrData's scope like:

attrData.scope.attr("showing")

But often, you want to update scope value or listen when the scope value changes. For example, the toggle mixin might want to update showing and the fade-in-when mixin needs to know when the showing changes. Both of these can be achived by using can.view.Scope.compute to get a get/set compute that is tied to the value in the scope:

var showing = attrData.scope.compute("showing")

This value can be written to by toggle:

can.view.attr("toggle", function(el, attrData){
    var attrValue = el.getAttribute("toggle")
    toggleCompute = attrData.scope.compute(attrValue);
    $(el).click(function(){
        toggleCompute(! toggleCompute() )
    })
})

Or listened to by fade-in-when:

can.view.attr("fade-in-when", function( el, attrData ) {
    var attrValue = el.getAttribute("fade-in-when");
    fadeInCompute = attrData.scope.compute(attrValue),
    handler = function(ev, newVal, oldVal){
        if(newVal && !oldVal) {
            $(el).fadeIn("slow")
        } else if(!newVal){
            $(el).hide()
        }
    }
    fadeInCompute.bind("change",handler);
        ...
    })

When you listen to something other than the attribute's element, remember to unbind the event handler when the element is removed from the page:

$(el).bind("remove", function(){
    fadeInCompute.unbind(handler);
});

What is the purpose of the can.view.tag method?

Registers the tagHandler callback when tagName is found in a template.

can.view.tag( tagName, tagHandler(el, tagData) )
  1. tagName {String}: A lower-case, hypenated or colon-seperated html tag. Example: "my-widget" or "my:widget". It is considered a best-practice to have a hypen or colon in all custom-tag names.
  2. tagHandler {function(el, tagData)}: Adds custom behavior to el. If tagHandler returns data, it is used to render tagData.subtemplate and the result is inserted as the childNodes of el.

can.view.tag is a low-level way to add custom behavior to custom elements. Often, you want to do this with can.Component. However, can.view.tag is useful for when can.Component might be considered overkill. For example, the following creates a jQueryUI DatePicker everytime a <jqui-datepicker> element is found:

can.view.tag("jqui-datepicker", function(el, tagData){
    $(el).datepicker()
})

The tagHandler's tagData argument is an object that contains the mustache scope and helper options where el is found and a subtemplate that renders the contents of the template within the custom tag.

tagData.scope can be used to read data from the template. For example, if I wanted the value of "format" within the current template, it could be read like:

can.view.tag("jqui-datepicker", function(el, tagData){
    $(el).datepicker({format: tagData.scope.attr("format")})
})
var template = can.mustache("<jqui-datepicker></jqui-datepicker>")
template({format: "mm/dd/yy"})

tagData.options contains the helpers and partials provided to the template. A helper function might need to be called to get the current value of format like:

can.view.tag("jqui-datepicker", function(el, tagData){
    $(el).datepicker({format: tagData.options.attr("helpers.format")()})
})
var template = can.mustache("<jqui-datepicker></jqui-datepicker>")
template({},{format: function(){
    return "mm/dd/yy"
}})

Often, data passed to a template is observable. If you use can.view.tag, you must listen and respond to chagnes yourself. Consider if format is property on a settings can.Map like:

var settings = new can.Map({
    format: "mm/dd/yy"
})

You want to update the datepicker if format changes. The easist way to do this is to use Scope's compute method which returns a get-set compute that is tied to a key value:

can.view.tag("jqui-datepicker", function(el, tagData){
    var formatCompute = tagData.scope.compute("format"),
    changeHandler = function(ev, newVal){
        $(el).datepicker("option","format", newVal});
    }

    formatCompute.bind("change",changeHandler)

    changeHandler({}, formatCompute());

    ... 

})
var template = can.mustache("<jqui-datepicker/>")
template(settings)

If you listen on something outside the tag, it's a good practice to stop listening when the element is removed from the page:

$(el).bind("removed", function(){
    formatCompute.unbind("change",changeHandler)
})

If content is found within a custom tag like:

var template = can.mustache(
    "<my-form>\
    <input value="{{first}}"/>\
    <input value="{{last}}"/>\
    </my-form>")

A seperate template function is compiled and passed as tagData.subtemplate. That subtemplate can be rendered with custom data and options. For example:

can.view.tag("my-form", function(el, tagData){
    var frag = tagData.subtemplate({
        first: "Justin"
    }, tagData.options)

    $(el).html( frag )
})

template({
    last: "Meyer" 
})

In this case, the sub-template will not get a value for last. To include the original data in the subtemplate's scope, [can.view.Scope::add add] to the old scope like:

can.view.tag("my-form", function(el, tagData){
    var frag = tagData.subtemplate(
        tagData.scope.add({ first: "Justin" }), 
        tagData.options
    )

    $(el).html( frag )
})

template({
    last: "Meyer" 
})
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License