Web DevCenter    
 Published on Web DevCenter (http://www.oreillynet.com/javascript/)
 See this if you're having trouble printing code examples


Supporting Three Event Models at Once

by Danny Goodman and Apple Developer Connection
09/25/2001

Events make the client-side JavaScript world go 'round. After a Web page loads, the only way a script can run is in response to a system or user action. While simple events have been part of the JavaScript vocabulary since the first scriptable browsers, more recent browsers implement robust event models that allow scripts to process events more intelligently. The problem, however, is that in order to support a wide range of browsers you must contend with multiple advanced event models. Three, to be exact.

The three event models align themselves with the Document Object Model (DOM) triumvirate: Netscape Navigator 4 (NN4), Internet Explorer 4 and later for Macintosh and Windows (IE4+), and the W3C DOM, as implemented in Netscape Navigator 6. Despite sometimes substantial differences among the models, they can all work side-by-side in the same document with the help of a few JavaScript shortcuts. This article focuses on two key aspects of the conflicting event models:

Approaches to Event Binding

Event binding is the process of instructing an HTML element to respond to a system or user action. There are no fewer than five binding techniques implemented in various browser versions. A quick overview of these techniques follows.

Event Binding I: Element Attributes

The simplest and most backward-compatible event binding avenue is an element tag's attribute. The attribute name consists of the event type, preceded by the preposition "on." Although HTML attributes are not case-sensitive, a convention has evolved to capitalize the first letters of each "word" of the event type, such as onClick and onMouseOver. These attributes are also known as event handlers, because they instruct an element how to "handle" a particular type of event.

Proper values for event handler attributes come in the form of JavaScript statements within quoted strings. Most commonly, a statement calls a script function that is defined inside a <SCRIPT> tag earlier in the document -- usually in the <HEAD> portion. For a function defined as:

function myFunc() {
    // script statements here
}

an event handler in, say, a button form control looks like the following:

<INPUT TYPE="button" NAME="myButton" VALUE="Click Here" onClick="myFunc()">

Event binding through element attributes has the advantage of allowing you to pass parameters to the event handler's function. A special parameter value -- the this keyword -- passes a reference to the element receiving the event. The following code demonstrates how one function can convert the content of any number of text boxes to uppercase characters with the help of the passed parameter:

<SCRIPT LANGUAGE="JavaScript">
function convertToUpper(textbox) {
    textbox.value = textbox.value.toUpperCase();
}
</SCRIPT>
...
<FORM ....>
<INPUT TYPE="text" NAME="first_name" onChange="convertToUpper(this)">
<INPUT TYPE="text" NAME="last_name" onChange="convertToUpper(this)">
...
</FORM>

Event Binding II: Object Properties

For NN3+ and IE4+ browsers, scripters can bind events to objects by way of script statements instead of as tag attributes. Each element object that responds to events has properties for each event it recognizes. The property names are lowercase versions of the tag attributes, such as onmouseover. NN4 also accepts the interCap versions of the property names, but for cross-browser compatibility, the all-lowercase versions are trouble-free.

Comment on this articleWhat are your thoughts about scripting as you work in this cross-platform, cross-browser environment we know as the Web?
Post your comments

Previously in this series:

Scripting for IE 5 Macintosh Edition -- Windows-centric IE developers might assume that what they design works equally well on Mac and PC versions of the Microsoft browser. But that's not always true. Here's a look at how the Macintosh version of IE behaves with some popular Windows design techniques.

Binding occurs when you assign a function reference to the event property. A function reference is the name of the function, but without the parentheses of the definition. Therefore, to bind the click event of a button named myButton to invoke a function defined as myFunc(), the assignment statement is as follows:

document.forms[0].myButton.onclick = myFunc;

One point you should notice is that there is no way to pass a parameter to the function when the event fires. I'll come back to this point later in the discussion of event processing.

Event Binding III: IE4+ <SCRIPT FOR> Tags

In IE4+, Microsoft implements its own extensions to the <SCRIPT> tag that bind the enclosed script statements to one event type for one element. The tag's attributes that make this binding possible (not sanctioned by the W3C for HTML) are FOR and EVENT.

The value of the FOR attribute must be the unique identifier you assign to an element's ID attribute. You must then assign the name of the event (onmouseover, onclick, etc) to the EVENT attribute. Using the button example above, the button's tag must be modified to include an ID attribute:

<INPUT TYPE="button" NAME="myButton" ID="button1" VALUE="Click Here">

Script statements are not in a function, but within the <SCRIPT> tag, as follows:

<SCRIPT FOR="button1" EVENT="onclick">
// script statements here
</SCRIPT>

Naturally, statements inside the tag can call any other functions defined elsewhere on the page (or imported from a .js file). But this binding style means that you must create a <SCRIPT FOR> tag for each element and each event.

You must also be careful to deploy this binding approach only on pages that will be viewed by IE4+ browsers. Any other scriptable browser (including IE3) that doesn't implement this special kind of <SCRIPT> tag treats the tag as a regular <SCRIPT> tag, and attempts to execute the statements inside while the page loads -- inevitably leading to script errors.

Event Binding IV: The IE5/Windows attachEvent() Method

Implemented before the W3C DOM working group honed the standard event model, the attachEvent() method is available for every HTML element object in the Windows version of IE5 and later. In other words, to bind an event to an element object, invoke the attachEvent() method on that object.

The syntax for the attachEvent() method is as follows:

elemObject.attachEvent("eventName", functionReference);

Values for the eventName parameter are strings representing the event name, such as onmousedown. The functionReference parameter is the same kind of parenthesis-free reference to a function described earlier in the event property approach. Thus, for the button object I've been using as an example, the statement that binds the function to the button's click event is as follows:

document.getElementById("button1").attachEvent("onclick", myFunc);

Since the attachEvent() method works strictly in the IE5+/Windows environment, you can use either the W3C DOM element reference (above) or the IE4+ reference:

document.all.button1.attachEvent("onclick", myFunc);

One caveat about this approach: You cannot permit the statement to execute before the element's tag loads in the browser. The reference to the button object is not valid until the HTML creates the element in the browser. Therefore, such binding statements need to run either at the bottom of the page or in a function invoked by the onLoad event handler of the BODY element.

Event Binding V: The W3C DOM addEventListener() Method

Netscape 6 adopts the W3C DOM Level 2 event binding mechanism, which, while similar to the IE5/Windows attachEvent() approach, has its own syntax. The W3C DOM specification assigns the addEventListener() method to every node in the DOM hierarchy. While an HTML element is a type of DOM node, a text node inside an element's tag set is also a node capable of receiving events. This is an important point that haunts NN6 event processing, as you will see later in this article.

The syntax for the addEventListener() method is as follows:

nodeReference.addEventListener("eventType", listenerReference, captureFlag);

In the jargon of the W3C DOM specification, the addEventListener() method registers an event with a node, instructing the node what to do with the event. The first parameter of the method is a string declaring the type of event (without the "on"), such as click, mousedown, and keypress. Treat the second parameter of addEventListener() in the same manner as the function reference described earlier. The third parameter is a Boolean value that signifies whether the node should listen for the event in what the DOM calls capture mode. The subject of event capture and bubbling -- collectively called event propagation -- is subject best left for another article. For a typical event listener, the third parameter should be false.

Which Binding is Best?

If you're lucky enough to be creating applications for a single browser version and operating system, you can opt for the most modern binding for the chosen browser. Cross-browser authors, however, have a substantial challenge.

If you plan to support IE5/Mac, you can dismiss the attachEvent() and addEventListener() methods because IE5/Mac supports neither of these. Your practical choice, then, is between the tag attribute and object property approaches. This is where psychological conflicts come into play.

On the one hand, the tag attribute approach is acknowledged by the W3C DOM Level 2 recommendation as an acceptable substitute for the addEventListener() method. To be compatible with millions of existing scripts, all scriptable browsers support tag attribute binding. Automated authoring tools, such as DreamWeaver, also embed event handler attributes in HTML tags.

On the other hand, embedding script-oriented information within an element's tag flies in the face of the compelling trend toward separating content from style and behavior. Binding events as object properties heads in the right direction, but there is no "official" support for event properties in W3C standards related to HTML, XHTML, or DOM. The approach is, however, supported in real life by all but the first-generation scriptable browsers.

A standards purist will find fault with either approach; but a practical developer should feel "safe" with either approach, even when considering compatibility with future mainstream browsers.

The Event Mother Lode: The Event Object

At the core of all three event models is an event object -- an abstract entity whose properties contain a ton of information that is potentially valuable to functions that process events. From the discussion of event binding techniques earlier in this article, you might deduce one reason why the event object is so vital to scripts: with the exception of tag attribute binding, no binding approach permits parameter passing to the event handler function.

The event object fills the gap by providing enough "hooks" to let a function read event characteristics. Thus, a function can retrieve a reference to the element that received the event and other tidbits, such as coordinates of mouse actions, mouse button(s) used, keyboard keys pressed, and whether any modifier keys were held down during the event (for example, to detect a Shift-click).

Accessing the Event Object

Although the precise composition of the event object varies according to the three DOMs discussed throughout this article (NN4, IE4+, and W3C/NN6), an event handler function can access an event object in only one of two ways: the NN way and the IE way. The W3C/NN6 DOM event object exposes itself to scripts the same way the NN4 event object does; IE4+, on the other hand, has a different approach.

The IE4+ event object is easier to describe, so we'll cover that first. Simply said, the event object is a property of the window object. By implication, this means that only one event object exists at any instant. For example, the simple act of pressing and releasing a keyboard key generates three events: onKeyDown, onKeyPress, and onKeyUp (in that order). If a function invoked by the onKeyDown event takes a long time to process, the browser holds the other two events in a queue until all of the onMouseDown event's function processing completes.

On the NN4 and W3C DOM sides, the event object seems even more abstract. For all event binding techniques except the tag attribute style, the event object gets passed automatically to the function bound to the event. A single parameter is sent to the function. It is your job to provide a parameter variable in the function definition to "receive" that parameter value. To avoid conflict with the IE window.event object, do not use event as the parameter name. The variable name evt is as good as any. Such a function definition looks like the following:

function myFunc(evt) {
    // script statements here
}

If you use the tag attribute event binding technique, however, you must explicitly pass the event as one of the parameters of your function call. To do this, use the event keyword as the parameter:

onClick = "myFunc(event)"

The incoming parameter variable is your function's only connection to the NN event object. If the object or its values are needed in another function that is invoked from within the main event handler function, you must relay the object or its property value(s) as a parameter(s) to the other functions.

If you're wondering whether IE would plug the window.event property into this event reference, the answer is, "Yes." This syntax overlap is a safe because both the NN and IE event objects that get passed to the function have the desired property values of the current event.

Accommodating Both Event Object References

Assuming that a function needs to inspect one or more properties of an event object to process the event, a simple technique lets your function work with the event object passed as a parameter or read from the window.event property. Moreover, the technique doesn't require an ounce of browser version sniffing.

To begin, always define your event handler functions with a parameter variable to prepare for the possibility of an incoming event object. Then use a simple conditional expression to assign the event object of the browser to that parameter variable:

function myFunc(evt) {
  evt = (evt) ? evt : ((window.event) ? window.event : "")
  // process event here
}

If the event object arrives as a parameter, it remains assigned to the evt local variable inside the function. But if the parameter is null and the browser's window object includes an event property, then the window.event object assigns itself to the evt variable.

To complete the job, however, you should also include one more conditional layer that gracefully handles earlier browsers lacking an event object in their object models:

function myFunc(evt) {
  evt = (evt) ? evt : ((window.event) ? window.event : "")
  if (evt) {
      // process event here
  }
}

By building all event handler functions this way, you define a function that accommodates event objects passed explicitly from tag attribute binding or implicitly from event property binding. Even if you change the binding style during development, the function doesn't have to change.

Event Object Smorgasbord

Establishing a reference to the event object is only part of the battle, however. Each event object from each event model has its own set of properties containing event details. The table below lists the most commonly accessed properties and the names of those properties in the three primary event object types.

Table 1. Popular Event Object Properties
DescriptionNN4IE4+W3C/NN6
Event targettargetsrcElementtarget
Event typetypetypetype
X coordinate on pagepageX*pageX
Y coordinate on pagepageY*pageY
Mouse buttonwhichbuttonbutton
Keyboard keywhichkeyCodekeyCode

*Values can be calculated by evaluating event.clientX + document.body.scrollTop or event.clientY + document.body.scrollTop.

IE5 for the Macintosh generally follows the IE4+ event object. The one exception is that the IE5/Mac event object features both a srcElement and target property to refer to the element that receives the event.

Perhaps the most important event object property to extract is a reference to the HTML element that receives the event. The NN4 and W3C event objects share the same property name (target), while the IE4+ event object uses srcElement. Object detection (instead of laborious and hazard-prone browser version sniffing) comes to the rescue again. For elements that are not text containers, a simple conditional expression handles the syntactic differences with ease:

var elem = (evt.target) ? evt.target : evt.srcElement

From here, your script can read and/or write whatever element object properties that the browsers object model exposes.

W3C DOM Node Event Targets

The W3C DOM node architecture allows each node in a document tree to receive events. In browsers that support this architecture, event handlers assigned to text containers are not the targets of events that occur atop the nested text. The text nodes are the target objects. Consider the following:

In the events example, when the mouse pointer rolls atop the text of a SPAN element, the text within that span will be highlighted. Event binding occurs via object properties in the init() function. On the face of it, when a user rolls the mouse atop the SPAN element, the onMouseOver event action assigns the class name (highlight) associated with a style sheet rule that displays the text in bold face and a yellow background; with onMouseOut, the style reverts to its original version (class normal). Notice how one toggleHighlight() function performs both actions with the help of the event object's type property (a property whose name is the same across all event model objects). Try the events example.

Related Reading

Designing with JavaScript, 2nd EditionDesigning with JavaScript, 2nd Edition
By Nick Heinle & Bill Peña
Table of Contents
Index
Sample Chapter
Full Description

But if you load the example into NN6, the true target of the mouse events is the text node inside the SPAN element. Although we don't cover event propagation in this article, take it on faith that the default behavior of the W3C DOM event model causes events to bubble upward through the node containment hierarchy (much like IE4+ events bubble up through element containers). Therefore, in the events example, the mouse events bubble upward from their true targets to the text node's container (that is, the SPAN element). These events trigger the SPAN element's event handlers for those two events.

Even though the event handler belongs to the SPAN element, the event object preserves the reference to the text node as being the event's original target. Modifying the text node's style, however, requires acting on its container. To equalize the operation of the toggleHighlight() function so that it can modify the className property of the SPAN container, the function needs to derive a reference to the text node's container.

One tactic is to use the W3C DOM event object's currentTarget property, which returns a reference to the node that processes the event. The decision tree required to take this property into account lengthens the toggleHighlight() function as follows:

function toggleHighlight(evt) {
  evt = (evt) ? evt : ((window.event) ? window.event : "")
  if (evt) {
    var elem
    if (evt.target) {
      if (evt.currentTarget && (evt.currentTarget != evt.target)) {
        elem = evt.currentTarget
      } else {
        elem = evt.target
      }
    } else {
      elem = evt.srcElement
    }
    elem.className = (evt.type == "mouseover") ? "highlight" : "normal"
  }
}

An alternate approach is to examine the nodeType property of the object returned by the target property. A browser capable of directing events to text nodes would also report the nodeType property of a text node as a value of 3, rather than the value for an element node (a value of 1). If the event target is a text node, then a script can obtain a reference to the surrounding element node via the text node's parentNode property. The decision tree for this approach is somewhat more streamlined:

function toggleHighlight(evt) {
  evt = (evt) ? evt : ((window.event) ? window.event : "")
  if (evt) {
    var elem
    if (evt.target) {
      elem = (evt.target.nodeType == 3) ? evt.target.parentNode : evt.target
    } else {
      elem = evt.srcElement
    }
    elem.className = (evt.type == "mouseover") ? "highlight" : "normal"
  }
}

If you are viewing this article with Netscape 6, try this modified version to see the style change with the mouse rollover.

With this last version of toggleHighlight() embedded into the events example, the page demonstrates how to use JavaScript to add value to the page for browsers capable of the desired effect. At the same time, the basic content, albeit in a less engaging and interactive mode, is still available for users of older or other non-scriptable browsers.

An Event Handler Function Template

Not every event handler function deals with the same properties or behaviors of element objects on the page, but you can start coding such functions with the help of a template derived from the discussions above. The template follows:

function functionName(evt) {
  evt = (evt) ? evt : ((window.event) ? window.event : "")
  if (evt) {
    var elem
    if (evt.target) {
      elem = (evt.target.nodeType == 3) ? evt.target.parentNode : evt.target
    } else {
      elem = evt.srcElement
    }
    if (elem) {
      // process event here
    }
  }
}

Substitute your function's name in the first line and start your event-specific code where indicated. This format should get you started for any cross-browser event binding style you employ. If you use this format a lot within a page, you can condense it further by extracting the target-reading code as a reusable utility function, and invoke it from each of your event handler functions:

// shared function
function getTargetElement(evt) {
  var elem
  if (evt.target) {
    elem = (evt.target.nodeType == 3) ? evt.target.parentNode : evt.target
  } else {
    elem = evt.srcElement
  }
  return elem

}

function functionName(evt) {
  evt = (evt) ? evt : ((window.event) ? window.event : "")
  if (evt) {
    var elem = getTargetElement(evt)
    if (elem) {
      // process event here
    }
  }
}

With this kind of framework in place, you should now be able to focus more closely on the specific actions required for each event handler function.

Danny Goodman has been writing about technology and computers full-time since 1981 and is the author of Dynamic HTML: The Definitive Reference and "The Complete HyperCard Handbook."


Return to the JavaScript and CSS DevCenter.

Copyright © 2009 O'Reilly Media, Inc.