Knock Me Out

Thoughts, ideas, and discussion about Knockout.js

Revisting Event Delegation in Knockout.js

| Comments

Previously, I discussed a technique to do event delegation in Knockout.js. That post was created before Knockout 2.0, which added the ability to use ko.dataFor and ko.contextFor to retrieve the data context related to a specific element (as described here). With these new tools, there are now much better ways to add a single handler on a parent element that can respond to events triggered on its child elements.

Note: based on the techniques in this post, I created a plugin called knockout-delegatedEvents located here: https://github.com/rniemeyer/knockout-delegatedEvents.

Why event delegation in Knockout.js?

Currently when you use the click or event binding in Knockout, an event handler is attached directly to that element. Generally this is fine and causes no real problems. However, if you face a scenario where you have a large number of elements that require these bindings, then there can be a performance impact to creating and attaching these handlers to each element, especially in older browsers. For example, you may be creating a grid where each cell needs various event handlers or a hierarchical editor where you are potentially attaching handlers to interact with the parent items as well as each child (and each child may have children).

In this case, there are advantages to using event delegation:

  • you only have to attach one (or a few) event handlers rather than one on each element. Events bubble up to the higher-level handler and can understand the original element that triggered it.
  • dynamically added content does not need new event handlers added to them (largely not an issue with KO bindings and templating)

Normal event delegation with KO 2.0+

With Knockout 2.0+, the common way to use event delegation is to avoid attaching event handlers in the bindings themselves and add another layer of code to attach a single handler to a parent element using something like jQuery’s on (previously live/delegate). So, rather than doing:

1
2
3
4
5
<ul data-bind="foreach: items">
    <li>
        <a href="#" data-bind="click: $root.selectItem, text: name"></a>
    </li>
</ul>

You would instead do:

1
2
3
4
5
<ul id="items" data-bind="foreach: items">
    <li>
        <a href="#" data-bind="text: name"></a>
    </li>
</ul>

and attach an event handler elsewhere like:

1
2
3
4
5
6
$("#items").on("click", "a", function() {
    var context = ko.contextFor(this); //this is the element that was clicked
    if (context) {
        context.$root.selectItem(context.$data);
    }
});

Attaching a single handler like this works quite well. However, there are a few issues that I still have with this technique:

  • you have another layer of code to manage besides your view model. This is the type of code that Knockout generally helps you get away from.
  • this code is tightly coupled (fairly) with this view’s markup
  • in our example, if the entire ul is part of a template that gets swapped, then we would need to hook up our handler again or choose to add our handler at a higher level (possibly up to the body)
  • this particular code also adds a dependency on jQuery that is not really necessary

In my perfect Knockout application, you have your view and your view model with the only code outside of that being a call to ko.applyBindings. To accomplish this, we would need to be able to wire it up declaratively in the view. I have been exploring a few ways to make this happen.

Declaratively adding a handler

The first step is attaching a handler on the parent or root element that will respond when events bubble up to it. This is pretty easy to accomplish with a custom binding. When the event is handled, we can determine the original element and use ko.dataFor or ko.contextFor to get the original data context and then execute some method.

1
2
3
4
5
6
7
8
9
10
//add a handler on a parent element that reponds to events from the children
ko.bindingHandlers.delegatedHandler = {
    init: function(element, valueAccessor) {
        //array of events
        var events = ko.utils.unwrapObservable(valueAccessor()) || [];
        ko.utils.arrayForEach(events, function(event) {
            ko.utils.registerEventHandler(element, event, createDelegatedHandler(event, element));
        });
    }
};

To create the handler, we take the target element and get its context:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//create a handler for a specific event like "click"
var createDelegatedHandler = function(eventName, root) {
    //return a handler
    return function(event) {
        var el = event.target,
            data = ko.dataFor(el),
            action = findAnAction(el, context); //??? how do we know what method to call


        //execute the action
        if (action) {
            //call it like KO normally would with the data being both the value of `this` and the first arg
            result = action.call(data, data, event);

            //prevent the default action, unless the function returns true
            if (result !== true) {
                if (event.preventDefault) {
                    event.preventDefault();
                }
                else {
                    event.returnValue = false;
                }
            }
        }
    };
};

When used on an element, the binding would take in an array of events like:

1
2
3
<ul data-bind="delegatedHandler: ['click', 'mouseover', 'mousedown']">
...
</ul>

With this binding in place, we can handle the event, find the element that triggered the event, and determine the data context of the element. However, we still need to know what method to call. I looked at a few alternatives for this piece.

Option #1 - using a binding

On the child elements, we could use a binding that stores a reference to the function to call. The binding can use Knockout’s ko.utils.domData functions to store and retrieve data on the element, as Knockout automatically cleans up this data whenever it removes elements from the document. For example, we could create a delegatedClick binding to make this association.

1
2
3
4
5
6
7
//associate a handler with an element that will be executed by the delegatedHandler
ko.bindingHandlers.delegatedClick = {
    init: function(element, valueAccessor) {
        var action = valueAccessor();
        ko.utils.domData.set(element, "ko_delegated_click", action);
    }
};

Now, on a child element, you would associate a function like:

1
2
3
<li>
    <a href="#" data-bind="text: name, delegatedClick: $parent.selectItem"></a>
</li>

We could create additional bindings for other events that we are interested in and could even create a helper function to generate these bindings given an array of events that we need to handle.

Pros

  • Can simply replace click bindings with delegatedClick (and other events) without any other changes
  • Can bind against specific functions and can execute code for cases where you need to call a function with a parameter to create a handler (delegatedClick: $parent.createHandler('some_modifier'))

Cons

  • The binding on the child elements is lightweight, but we still have to pay the overhead of parsing and executing a binding.

Option #2 - use a data- attribute with method name

The first solution works well, but I still do not like having to pay the price of executing bindings on the child elements. For an alternative solution, I looked at a different approach where the child elements are tagged with a data-eventName attribute that contains the method name.

1
2
3
<li>
    <a href="#" data-click="selectItem" data-bind="text: name"></a>
</li>

With this technique we would no longer need the child bindings. In the event handling code, we would now need to locate a method that matches our data-eventName attribute value. This time we can use ko.contextFor to get back the entire binding context. It is very common to call functions off of $parent or $root as well as off of the current data itself, so the $parents array allows us to walk up the scope chain to locate an appropriate method.

Pros

  • No overhead of executing a binding on the child elements
  • Can execute the function with the correct owner, so we do not need to use .bind or var self = this; to ensure that the context is correct. This is a really nice benefit, we have to search for the method in each scope, so we know the appropriate owner of the method to use as the context.

Cons

  • could run into issues with clashing names (child has an add method, but you wanted to call parent’s add)
  • cannot execute code to create a handler, as the attribute value would be a string that needs to match a method name

Option #3 - associate method and name in root/global object

Another alternative might be to use the data- attribute, but use the string to key into an object that holds the associations between names and actions. Perhaps something like ko.actions or ko.commands. This object could hold just function references or maybe a function and owner. For arrays of objects, we would not be able to indicate an appropriate owner, so we would have to fall back to making sure that the function is properly bound on each instance. For example, we could even give the function an alias of select like:

1
2
3
4
ko.actions.select = {
    action: this.selectItem,
    owner: this
};

Pros

  • No overhead of executing a binding on the child elements
  • No issues with ambiguity in names, as names can be aliased
  • Functions outside of a view model could be included
  • Could dynamically change the definition of the action

Cons

  • have to manage adding (and maybe removing) functions to this object
  • different types of objects with the same method names would have to be aliased with unique names in this object

Summary

In cases where you need to attach a large number of event handlers, using event delegation can be a nice win. Attaching these handlers declaratively provides an alternative to wiring this up outside of your view/viewmodel. While considering each of the techniques to determine the function to execute, I was leaning towards #2, as it does not require the child bindings and as a bonus gets the context right when it calls the handler. Instead though, I decided to create a plugin that can use all of these techniques interchangeably. The plugin lives at: http://github.com/rniemeyer/knockout-delegatedEvents. Please let me know if you have feedback or suggestions for the plugin.

Comments