Events For JavaScript Components

Greg Murray

Problem Description

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.

Solution

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.

Simple Event Handlers

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.

Event Listeners

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().

Publish/Subscribe Event Handling

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.

Disconnecting Event Handlers

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.

Resources

The Dojo Event System

Dojo Event Examples


© Sun Microsystems 2006. All of the material in The Java BluePrints Solutions Catalog is copyright-protected and may not be published in other works without express written permission from Sun Microsystems.