Extension Events

By on April 11, 2014 9:41 am

When working in an event-driven environment such as the web, it is important to utilize tools that allow you to create succinct, easy-to-read code that’s extensible and flexible. One great mechanism that Dojo provides is the ability to use extension events.

What Are They?

Extension events are functions that are passed in lieu of an event type to dojo/on. The extension event function should accept a DOM node and an event listener. The function should encapsulate any logic related to the node and invoke the listener when appropriate. The most common case is adding a listener to a native DOM event (click, mouseover, etc.) with custom logic included that determines whether the handler is called or not.

Why Are They Useful?

Extension events are useful because they enable you to encapsulate commonly used event-handler logic in pseudo-event functions and assign the function a relevant name for reuse. Instead of registering an event handler for a native DOM event and repeatedly including code that tests for conditions relevant to your application logic, you can put that code in an extension event and register event handlers for the extension event with dojo/on. This is great because it allows you to keep your code clean and organized. Another benefit extension events provide is a consistent event API for multiple event types.

How Are They Implemented?

Extension event functions are executed immediately and are passed a target DOM node and the event listener. The extension event is responsible for attaching the event and is expected to return a handle-like object with a remove method similar to the handle object returned by dojo/on. If you are attaching a single event inside of an extension event, you can return the handler from an internal dojo/on call. Let’s look at implementing a mouseenter/mouseover extension event that will flatten the event API without additional code in our dojo/on call. While the mouseenter event type was added to WebKit recently, it is still not available as a native event type for some browsers such as Safari.

HTML:

<div id="testNode">Hover Over Me</div>

JavaScript:

require([
    'dojo/on',
    'dojo/dom'
], function (on, dom) {
    // Define an extension event function
    function mouseEnter(node, listener) {
        // Use dojo/on for DOM event handling
        return on(node, 'mouseover', function (event) {
            // Add custom logic for the extension event:
            // If the node currently receiving the 'mouseover' event
            // is not a descendant of the node previously being
            // moused over (event.relatedTarget), then the cursor has
            // entered a new node and the handler for the 'mouseEnter'
            // extension event should be called
            if (!dom.isDescendant(event.relatedTarget, node)) {
                console.log('mouseEnter');
                listener.apply(node, arguments);
            }
        });
    }

    // Example usage of the 'mouseEnter' extension event
    on(dom.byId('testNode'), mouseEnter, function () {
        console.log('entered');
    });
});

So far, the example always uses the native mouseover event. To make this custom handler more efficiently handle browsers that implement the mouseenter event natively versus those that do not, we can add logic to detect the native event and defer to it if available:

require([
    'dojo/on',
    'dojo/dom',
    'dojo/has'
], function (on, dom, has) {
    // Add a "has" test to detect native availability of the
    // 'mouseenter' event
    has.add('mouseenter', function () {
        var node = document.createElement('div');
        return "onmouseenter" in node;
    });

    function mouseEnter(node, listener) {
        if (has('mouseenter')) {
            // If the browser natively supports the 'mouseenter'
            // event then register a handler for it
            return on(node, 'mouseenter', function () {
                console.log('native mouseenter');
                listener.apply(node, arguments);
            });
        }
        else {
            return on(node, 'mouseover', function (event) {
                if (!dom.isDescendant(event.relatedTarget, node)) {
                    console.log('mouseEnter');
                    listener.apply(node, arguments);
                }
            });
        }
    }

    // Example usage of the 'mouseEnter' extension event
    on(dom.byId('testNode'), mouseEnter, function () {
        console.log('entered');
    });
});

This allows us to simply pass mouseEnter as the event type and dojo/on and our extension event will handle the rest. As you can see, this is an extremely useful tool. The Dojo Toolkit leverages extension events in a few key areas: dojo/touch and dojo/mouse.

Putting It To Work

We’ve seen what extension events are and how we can use them, let’s build a solution to a common problem. Sometimes we want to pause or temporarily disable an event handler. While¬†dojo/on provides an on.pausable method, the dojo/Evented module, which widgets use, does not expose a method to pause events. We can write an extension event to handle this.

HTML:

<button id="pausable">Pausable Button</button>
<button id="pause">Pause</button>

pausable.js:

define([
	'dojo/on'
], function (on) {
	return function (type) {
		return function (node, listener) {
			var paused = false,
				signal;

			signal = on(node, type, function () {
				if (!paused) {
					listener.apply(node, arguments);
				}
			});

			signal.pause = function () {
				paused = true;
			};

			signal.resume = function () {
				paused = false;
			};

			signal.isPaused = function () {
				return paused;
			};

			return signal;
		};
	};
});

Using pausable.js:

require([
	'dijit/form/Button',
	'demo/pausable'
], function (Button, pausable) {
	var pausableButton = new Button({}, 'pausable'),
		pauseButton = new Button({}, 'pause'),
		handle;

	handle = pausableButton.on(pausable('click'), function () {
		console.log('Clicked!');
	});

	pauseButton.on('click', function () {
		if (handle.isPaused()) {
			handle.resume();
			pauseButton.set('label', 'Pause');
		} else {
			handle.pause();
			pauseButton.set('label', 'Unpause');
		}
	});
});

In this example, we’ve now created a module that allows for pausable events to be used on both DOM nodes as well as widgets in a uniform and flattened API.

Now that we’ve seen how extension events can add awesome event functionality to objects that would otherwise not have it, let’s look at an example of how extension events can be used to handle multiple events on a node. In the following example, we will compensate for browsers that do not fire events in a predictable manner, We will listen for both keypress and keydown events and the first one that fires wins.

HTML:

<input type="text" id="input" />

JavaScript:

require([
	'dojo/on',
	'dojo/dom',
], function (on, dom) {
	function keyPressOrDown(node, listener) {
		var fired = false;
		var keydown;
		var keypress;

		// Handle keydown events from node
		keydown = on(node, 'keydown', function () {
			if (!fired) {
				console.log('keydown');

				listener.apply(node, arguments);
				fired = true;
			}
		});

		// Handle keypress events from node
		keypress = on(node, 'keypress', function () {
			if (!fired) {
				console.log('keypress');

				listener.apply(node, arguments);
				fired = true;
			}
		});

		return {
			remove: function () {
				// Any event handlers registered by this 
				// extension event will need to be removed
				keydown.remove();
				keypress.remove();
			}
		};
	}

	on(dom.byId('input'), keyPressOrDown, function () {
		console.log('Event Fired');
	});
});

In this example, keyPressOrDown attaches event handlers for both onkeypress and onkeydown to our input field and guarantees that listener is called only once. In order to clean up the event handlers registered our extension event , keyPressOrDown returns a handler with a remove method that removes the onkeypress and onkeydown event handlers.

Conclusion

As you can see, extension events are a very powerful tool that can help abstract your events and help you provide a consistent event handling API for multiple event types. If you’re interested in learning more about extension events or Dojo in general, at SitePen we offer extensive Dojo workshops to help turn you into a Dojo master. Dojo 101 covers dojo/on, dojo/aspect, and dojo/topic, while Dojo 201 covers widgets & widget events in depth, including extension events and dojo/Evented.