Update #2: Check out this post for a new look at this topic and take a look at the plugin here.
Update: In Knockout 2.0, it is now possible to use ko.dataFor
and ko.contextFor
instead of tmplItem
and use this technique with native templates. Documentation here.
Prior to working with Knockout, I had recently grown fond of event delegation in JavaScript where an event listener is attached to a parent container and events from its children bubble up to this handler. I had primarily been using jQuery’s live() or its more precise cousin delegate() to handle this type of interaction in a fairly painless way.
There are several fairly obvious advantages to using event delegation in cases where you would be adding listeners for lots of events (like for every cell in a large grid):
- New elements added dynamically to the container can be handled without any additional code. Elements can be removed without having to manage unbinding handlers.
- Less overhead (memory, processing) than wiring up events to every element.
- Gives you the flexibility to use one handler to respond to events at multiple levels or from different kinds of elements.
At first glance, it seemed like event delegation didn’t quite fit the Knockout model. You certainly could set a handler on a parent element using the click
or event
binding. This would allow you to respond to actions on the children, but you would have no connection to the actual data that was used to generate the child element. You could also just use something like jQuery’s live()
or delegate()
functionality, but you would not get the integration with your view model data that you are used to in the bindings.
tmplItem - a hidden gem in jQuery Templates
In Knockout, the majority of the child content is typically rendered through templates, using the jQuery Templates plug-in. One lesser publicized feature of the plug-in is the tmplItem function. For any element generated by a template, this function allows you to get back to the data that was used to render the template.
For example,
1 2 3 4 5 6 |
|
1 2 3 4 5 6 7 8 9 10 |
|
So, using the tmplItem
function, we are actually able to retrieve some context from an element that was rendered by a template. This is the piece that we were missing to connect our delegated event with the model data related to our child element.
One note: if you are using {{each}}
syntax, tmpItem()
will still return the data that was the context of the entire template. You can use the template binding or the {{tmpl}}
tag to help achieve the proper context for your elements.
Are there benefits to doing event delegation in Knockout?
Wiring up events for dynamically created elements is generally not a problem in Knockout, as we are usually generating the content in templates and can easily add our event handlers declaratively. The other benefits of event delegation still stand though, especially in the case that we have a need to wire up a large number of handlers on our page. I can also see one other small advantage: the first time that you wrote a removeItem
method, you might have been a little disappointed that you had to use an anonymous function like:
1
|
|
Update: Knockout 2.0 eliminates this pain point by automatically passing the data as the first argument.
With event delegation, we could possibly add a binding to a parent element and just call a method off of the view model passing the actual data that was returned through tmplItem
as part of the binding.
Creating a custom binding for delegated clicks
Here is what I started with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
So, we are just trying to prepare a function that we can pass on to the real click binding. The function uses tmplItem
to retrieve the actual data object related to the element that was clicked and then executes the callback that was passed in through the binding. The actual data object is passed to the function. Now, I can put a binding at the table level to remove an item when I click on a “Delete” button in a cell.
1
|
|
This works, but removeItem
will get called for Any click inside the table. It doesn’t matter if we click the button, another cell, or a header cell, we will always attempt to run the method. This is certainly not what we want. To limit the elements that trigger our actual method, we can pass in a selector to the binding. As we already have a dependency on jQuery through the jQuery Templates plug-in, we can use jQuery’s .is
functionality to make sure that we only proceed when there is a match. Now we pass an object containing our function and a selector to the binding:
1
|
|
In our binding handler, we can make a small modification to check if the element matches our selector.
1 2 3 4 5 |
|
This works properly when calling parent functions that receive the children as a parameter. However, what if you want to actually call a method off of the child object in this manner? I found that based on the way that Knockout does parsing of the bindings, something like this will not work:
1
|
|
This will error out before it gets to my binding, unless childFunction
actually exists on the data that is the context of the table’s data binding (the view model passed to applyBindings
, unless you are in a template). We can still make this work though by passing the function name as a string and reconciling it in the binding.
1
|
|
For a minute, I was disappointed that it requires passing a string for the callback, but then I came to the realization that really the whole data-bind attribute is just a string anyways.
Now, I can check for this in the binding:
1 2 3 4 5 6 7 |
|
If the callback supplied in the binding is a string, then we will call the function off of the child object passing just the event to it. If the function is not a string, then we will call it off of the parent object and pass the child object and the event to it. This allows us to decide whether we want to use a method on our view model or a method on our child object.
One limitation that I immediately ran into was that I might want to pass in multiple click handlers that use different selectors. We should be able to pass in a list of handlers to add as an array.
So, my final version looked like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
and I can bind to multiple click events like:
1
|
|
Generic binding for all events
Finally, it might be nice to respond to other events in this way, like mouseover
and mouseout
. Based on the existing event binding, here is a more generic version of the delegatedClick
binding:
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 27 28 29 30 31 32 33 34 35 |
|
We could bind to the mouseover
and mouseout
events of our header cells like:
1
|
|
I can also now rewrite the delegatedClick
binding to be a wrapper to this generic binding.
Here is a grid-like sample that shows using event delegation when binding to a function on the view model, binding to a function on a cell’s related object, and binding to the mouseover
and mouseout
events on the header cells:
Link to full sample on jsFiddle.net:
Link to full sample on jsFiddle.net
In Knockout, event delegation is probably not necessary in most cases. However, I think that there are scenarios where this could be useful and beneficial, especially when dealing with an extremely large number of elements that need their events handled in similar ways.