Skip to content

A simple and flexible plugin to do declarative event delegation in Knockout.js

License

Notifications You must be signed in to change notification settings

rniemeyer/knockout-delegatedEvents

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Knockout Delegated Events

knockout-delegatedEvents is a Knockout.js plugin that allows you to add event delegation declaratively in your markup in a way that matches well with a normal Knockout application. This means adding an event handler on a parent element that can respond to events triggered by a child element and connect with a handler that lives on the view model. This is similar to using jQuery's on (previously live/delegate).

More background here: http://www.knockmeout.net/2012/11/revisit-event-delegation-in-knockout-js.html

Parent Element

knockout-delegatedEvents adds a binding called delegatedHandler that you can place on an element to respond to events triggered by its child elements. This binding can go on the body or at a lower-level, as necessary. The binding expects either the name of a single event or an array of events that it should handle.

Single event:

<ul data-bind="delegatedHandler: 'click'">
    ...
</ul>

Multiple events:

<ul data-bind="delegatedHandler: ['click', 'mouseover', 'mouseout']">
    ...
</ul>

Child Elements

Suppose that we have a simple view model like:

var viewModel = {
    items: ko.observableArray([
        { name: "one" },
        { name: "two" },
        { name: "three" }
    ]),
    removeItem: function(item) {
        this.items.remove(item);
    }
};

In our example, we want to indicate that when a child element is clicked it should run the removeItem function on our view model. There are three ways to indicate which handler to run when the event is triggered on your element.

1 - data-theEventName attribute matches function by name

The normal way to specify a handler is to add an attribute with the name data-theEventName to your child element and supply the name of the function to execute. If you want to respond when the element is clicked, then you would use the data-click attribute on the child.

<ul data-bind="delegatedHandler: 'click', foreach: items">
    <li>
        <span data-bind="text: name"></span>
        <a href="#" data-click="removeItem">x</a>
    </li>
</ul>

The plugin will attempt to find your handler by looking for a matching function by name on the current data item and then moving up the binding contexts ($parents array) to see if it can find a matching name at the next level. So, in the above example, removeItem lives on the $parent scope rather than on the actual data item.

With this plugin, you can simply specify removeItem and it will locate it on the parent and execute it with the original context (the correct value for this, the parent in this case). This means that you have less need to use var self = this; or .bind(this) to ensure that your handlers are executed with the proper context. The handler will also receive the element's associated data as the first argument and the event object as the second argument to match how Knockout normally executes event handlers.

2 - data-theEventName attribute matches function in ko.actions

Another option, besides allowing to the plugin to search for your function by name, is to configure your handler in the ko.actions object created by the plugin. There are two ways that actions can be specified:

//pass a function reference or create an anonymous function
ko.actions.removeItem = vm.remove;

//pass a function and indicate the owner (value of `this`) when it runs
ko.actions.removeItem = {
    action: vm.remove,
    owner: vm
};

The ko.actions item is checked first for a matching method before searching the data item and parents as described in option #1.

There are a couple of situations where this option might be advantageous:

  • the method does not live on the data item or one of its parents (could be global)
  • there is some ambiguity in the names of methods that you are binding against. Perhaps you have hierarchichal data where each level has a remove function and you want to give them more descriptive aliases in ko.actions.
  • the definition of the handler changes based on application state

Note: if an owner is not specified, then the value of this will be the current data item, as it would be when using the normal click or event bindings, so the function would need to be bound properly in your view model.

3 - Specify a function reference in delegatedTheEventName binding

This option for specifying a handler is to place a normal binding on the child element that stores a reference to the function with the element (but does not attach an event handler). Bindings for each event will be dynamically created, as necessary, by the delegatedHandler binding.

For example, you can use the delegatedClick binding to associate the element with a specific function.

<ul data-bind="delegatedHandler: 'click', foreach: items">
    <li>
        <span data-bind="text: name"></span>
        <a href="#" data-bind="delegatedClick: $parent.removeItem">x</a>
    </li>
</ul>

There are a couple of cases where this technique might be necessary:

  • you need to run code that returns a function reference: delegatedClick: getMyHandler('someParam')
  • you do not want to use ko.actions and have clashing method names or want to use a function that is outside of your view model.

Note: when attaching a handler using the binding, you will need to manage the value of this as you would when using the normal click and event bindings. So, in this case you would have to take care of it in our view model or bind against it like $parent.removeItem.bind($parent).

4 - Specify a function reference in delegatedParentTheEventName binding and use data-TheEventName-parent attribute

The final option for specifying a handler is to place a normal binding on the parent element that stores a reference to the function with the element (but does not attach an event handler). A data-TheEventName-parent attribute should be added to the element that should respond to the event. Exactly as for delegateTheEventName, bindings for each event will be dynamically created, as necessary, by the delegatedHandler binding. It is possible to handle mutiple child elements in a single parent element using object literal as delegatedParentTheEventName binding where keys should be used as data-TheEventName-parent attribute values and values should be the handler. Otherwise the data-TheEventName-parent value should be set to true and delegatedParentClick value should be the handler.

For example, you can use the delegatedParentClick binding to associate the element with a specific function.

<ul data-bind="delegatedHandler: 'click', foreach: items, delegatedParentClick:removeItem">
    <li>
        <span data-bind="text: name"></span>
        <a href="#" data-click-parent="true">x</a>
    </li>
</ul>

Here is an example how you can use the delegatedParentClick binding to associate multiple elements with different functions.

<ul data-bind="delegatedHandler: 'click', foreach: items, delegatedParentClick:{ remove: removeItem, add: addItem}">
    <li>
        <span data-bind="text: name"></span>
        <a href="#" data-click-parent="remove">x</a>
        <a href="#" data-click-parent="add">+/a>
    </li>
</ul>

When the element with the delegatedParentTheEventName binding and the element that should attach the event handler are the same, it is possible to use delegatedParentHandler binding instead to simplify notation. This binding receive a value of: { EventName: function,....} associated with data-click-parent set to true on child element or { EventName: { KeyOne: functionOne, KeyTwo: functionTwo...}, ...} associated with data-click-parent set to KeyOne and KeyTwo on child elements.

For example, you can re-write preceding example using delegatedParentHandler like this:

<ul data-bind="foreach: items, delegatedParentHandler:{ click :{ remove: removeItem, add: addItem }} ">
    <li>
        <span data-bind="text: name"></span>
        <a href="#" data-click-parent="remove">x</a>
        <a href="#" data-click-parent="add">+/a>
    </li>
</ul>

This technique gives you the power of solution 3 and at the same time you will pay the overhead of parsing and executing a binding only on parent element and not on all child elements.

Control Bubbling

Normally, when an event is handled, the plugin will prevent further bubbling of the event. In a scenario that you do want an event to continue bubbling, you can add an additional binding per event name to allow bubbling.

<div data-bind="delegatedHandler: ['click', 'mouseover'], delegatedClickBubble: true">
...
</div>

In this case, the delegatedClickBubble additional binding will signal the plugin that when it handles a click event it should allow the event to continue bubbling. For even greater control, the handler could then access the event argument (2nd arg) and choose to prevent bubbling on a case-by-case basis.

TODO

  • Improve example
  • Make it easier to configure actions that are dependent on modifiers (control+click, enter key, etc.) without needing to access the event in the view model

Dependencies

  • Knockout 2.0+

Build

This project uses grunt for building/minifying.

Examples

The examples directory contains a sample that demonstrates the three different ways that a handler can be found when a child element triggers an event.

View the sample in jsFiddle here: http://jsfiddle.net/rniemeyer/x57Lv/

License

MIT http://www.opensource.org/licenses/mit-license.php

About

A simple and flexible plugin to do declarative event delegation in Knockout.js

Resources

License

Stars

Watchers

Forks

Packages

No packages published