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 WrappersWrap 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.