Events are essential in JavaScript components as they drive the user interface, result in AJAX requests, and allow JavaScript components to interact with each other. Cross browser event handling code is difficult to write from scratch as there are various ways in JavaScript of handling events and each browser has its own quirks and issues.
As the number JavaScript components in a page increases, the component code can tend to become more tighlty coupled. This is not an effective way of developing user interfaces as the resulting code becomes less re-usable, difficult to manage, and maintain. There is a need for a way to effectively allow JavaScript components to not be as "tightly coupled" and be capable of being used with many other components with a minimum set of glue code.
Use the Dojo library which abstracts the JavaScript event system and provides a set of JavaScript APIs for event handling and a means of inter-component event communication. Dojo provides a few options for handling events which include simple event handlers, event listeners, and publish/subscribe events. The Dojo event handling APIs are not mutually exclusive, in many cases you will use a combination of the APIs depending on your use cases. Now let's investigate the use cases and APIs in more detail.
Below is a more detailed explanation of each API and the use cases for which they apply.
Use dojo.event.connect()
to register your event handlers in your
JavaScript components rather than the DOM or property based event handlers as dojo.event.connect()
provides a consistent API, abstracts browser differences, and prevents memory leaks that appear on some browsers.
The API also takes care of the details of attaching more than one event handler to a single event type.
Using dojo.event.connect()
you can connect one or more events to an object.
The events are called in the order they were added.
The arguments needed to add a before listener are:
dojo.event.connect(srcObj, "srcFunc", "targetFunc")
Following is an example of attaching an event handler:
<script type="text/javascript" src="dojo.js"></script> <script type="text/javascript"> window.onload = function () { var link = document.getElementById("mylink"); dojo.event.connect(link, "onclick", myHandler); } function myHandler(evt) { alert("dojo.connect handler"); } </script> <a href="#" id="mylink">Click Me</a>
Above the "onclick" property of link
element is mapped to the event handler function
myHandler
. If there was an existing handler the Dojo hanlder will be called after the
existing handler is called. Following is a more detailed example of attaching an anonymous event handler:
<script type="text/javascript" src="dojo.js"></script> <script type="text/javascript"> window.onload = function () { var link = document.getElementById("mylink"); // connect link element 'onclick' property to an anonymous function dojo.event.connect(link, "onclick", function(evt) { var srcElement; // this function is passed the browser specific mouse event if (evt.target) { srcElement = evt.target; } else if (evt.srcElement) { srcElement = evt.srcElement; } if (srcElement) { alert("dojo.event.connect event: " + srcElement.getAttribute("id")); } }); }" </script> <a href="#" id="mylink" onclick="alert('inline event');">Click Me</a>
The example above creates an anonymous event handler (function) for the link
element which is defined in the document
body with the id "mylink". If an event handler does not exist the one defined using dojo.event.connect
will become the default handler. Otherwise, if a hanlder like the inline one above the Dojo event will be called
after the default inline event. As can be seen in the example the dojo.event.connect
API provides
accesses to the event object (evt
) which is available as an argument. Not that Dojo does not provide an abstraction
to the browser specific mouse events. The example above uses object detection to get the source element.
Use event listeners when you want one object/component to listen to events on another object and potentially take some action. Keep in mind that listeners only listen though they do have access to the arguments that will be passed to the event handler they are listening to.
One or more Listeners may be called "before
" or "after
" a source event handler.
The event listeners are passed the same arguments as the source event handler. The listeners are called in the order
that they were connected to the source event handler. Whether you choose to use "before" or "after" is driven by the
use cases of your application. What is important is Dojo provides the flexibility to use either.
The arguments needed to connect a listener are:
dojo.event.connect("before/after", srcObj, "srcFunc", targetObj, "targetFunc")
The first argument is "before" or "after" depending on when you want the listener called. The second argument
is the source object (generally an HTML element) to which you are attaching the event. The third argument is the name
of the function (as a string with quotes) which you plan to listen to. The fourth argument is the target object. The fifth
argument is the function of the target object (as a string with quotes) that will be called before or after the source
function is called depending on the first argument. If you have a difficult time tracking all the argument names you
you may chose to use dojo.event.kwConnect()
which takes an object literal with the parameter names
and values as properties (see The Dojo Event System for
more details).
The following example uses dojo.event.connect
to connect the loadMenuListener
function
to get called "before" calls are made to the loadMenu
event handler.
function loadMenu(args) { alert("args=" + args); } function loadMenuListener(args) { alert("loadMenuListener: args=" + args); } dojo.event.connect("before", this, "loadMenu", this, "loadMenuListener"); loadMenu({name: "MyMenu", items: ['File', 'Save']}); // alerts loadMenuListener: args=[object Object] // alerts args=args=[object Object]
If you want a listener to be notified "after" an event handler is called
speicify "after" as the first argument when using dojo.event.connect
.
function loadMenu(args) { alert("args=" + args); } function loadMenuListener(args) { alert("loadMenuListener: args=" + args); } dojo.event.connect("after", this, "loadMenu", this, "loadMenuListener"); loadMenu({name: "MyMenu", items: ['File', 'Save']}); // alerts args=[object Object] // alerts loadMenuListener: args=[object Object]
In this example the listener gets called after the source event hanlder is called. .
Most Java developers will be most familiar with the listener approach as it is used throughout the Java platform. This approach has the added benefit that you can have the listener called before or after a specific handler is called.
Event Handler Wrappers
Wrap events using "around
" when you want to intercept and modify the behavior of an event handler without
modifying the JavaScript source of the component you are using.
Adding "before
" or "after
" an event handler listeners may not be enough. In some case you may want
to modify the behavior or arguments of an event handler without modifying the source code
of the JavaScript component you are using. In Dojo this is accomplished using dojo.io.connect
with
"around
" as the first argument.
The arguments needed to add a event wrapper are:
dojo.event.connect("around", srcObj, "srcFunc", targetObj, "targetFunc")
These parameters are the same as the listener approach with the only difference being the first argument is "around".
The following example shows a event handler on one object being wrapped by a custom event handler. The custom event handler will apply its own logic and then call the source event handler.
// custom event handler wrapper function customLoadHandler(invocation) { alert("custom menu name =" + invocation.args[0].name); // update the name property of the argument invocation.args[0].name = "Custom Menu"; //call the default event handler invocation.proceed(); } function ImageScroller() { this.load = function (args) { alert("default menu name=" + args.name); } } var is = new ImageScroller(); dojo.event.connect("around", is, "load", this, "customLoadHandler"); is.load({name: "My Menu", items: ['File', 'Save']}); // alerts "custom menu name=My Menu" // alerts "default menu name=Custom Menu"
In the example above shows how you can write custom code to intercept the call to the public function
load
on ImageScroller
and wrap it with the function customLoadHandler
.
The customLoadHandler
function in this example manipulates the arguments that are presented to the
load
function. Note that customLoadHandler
function is passed a single argument by
the Dojo runtime
(invocation
in the example above) with two properties: "args
" which is an array of the arguments
passed to the target event handler and "proceed
" which is a function that will call the target event handler.
If a value was returned by the source function the wrapper could retrieve the value when "proceed
" is called
and assign it to a variable. The wrapper could and change or modify the return variable value depending on the logic in the
wrapper.
For those that have used Servlet Filters
this approach is very similar in that you have the chance to intercept requests and modify the behavior. In the case
of a Filter the getParameters()
would be synonomous to the invocation.args
and the
doFilter()
is synonomous with invocation.proceed()
.
Use publish and subscribe to communicate events anonymously between components. You may also consider customizing the component to allow the topic name to be passed in as an initialization parameter to make the component more flexible. Not all event handling need be exposed using publish and subscribe however try to be flexible as to permit future integration with other components.
Imagine a component ImageScroller
where the products that are displayed are capable of being set by a different
component AccordionMenu
. This may be achieved using the dojo.event.publish
and dojo.event.subscribe
APIs as
seen in the following example.
<script type="text/javascript" src="dojo.js"></script> <script type="text/javascript"> window.onload=init; var ac; var is; function init() { ac = new AccordionMenu(); ac.load(); is = new ImageScroller(); is.load(); } function ImageScroller() { this.setProducts = function(pid) { // show the products for pid } this.handleEvent = function(args) { if (args.event == 'showProducts') { this.setProducts(args.value); } } this.load = function () { dojo.event.topic.subscribe("/scroller", this, handleEvent); } } // accordion menu defined below or in another .js file included in the page <script>
In the case of the example above a JavaScript component ImageScroller
registers to listen for events
on the topic "/scroller". The handleEvent
function is set as the
event handler. Note that the handleEvent
function is defined using 'this' which allows external JavaScript code
or components to manually call the component's event handling functions. This example uses object literal "args
" with properties
"event
" for the event type and "value
" for the event value for the event handler.
It is recommended to use object literals as arguments as they are flexible and will allow you to customize
the parameters you pass to a function without changing the function signature.
function AccordionMenu() { function expandRow(target) { ... var link = document.createElement("a"); dojo.event.connect(link, "onclick", function(evt){ this.target = target; dojo.event.topic.publish("/scroller", {event: "showProducts", value : target}); }); } }
When the "link
" receives an "onclick
" event the event handler will publish an event to
the topic "/scroller
" containing an object literal with the properties "event
" with "showProducts
"
as the property value and the property "value" with the value of target. A closure was used
in this case to properly maintain the value of the target
property as it will be out
of scope when the anonymous handler is called. Keep in mind when using this type of closure that the anonymous handlers
will exist as long as there are any references pointing at the properties that are used to form the closure. Also use
caution with DOM element references as they can lead to memory leaks.
While much of this document has focused on use cases for the connecting to event handlers there is the issue
of detaching events form objects when they are not needed.
To do so call dojo.event.disconnect
for events or dojo.event.unsubscribe
for topics
with the exact set of parameters used when connecting or subscribing to an the event handler.
As you develop your components you may use one or more of these event handling techniques depending on what you are doing. Dojo provides a wide variety of methods for event handling that are very powerful. See the resources below for more details and examples.