Supporting Three Event Models at
Once
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:
- Methods for binding an event to an HTML element object.
- Processing the event after it fires.
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.
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
| Description |
NN4 |
IE4+ |
W3C/NN6 |
| Event target |
target |
srcElement |
target |
| Event type |
type |
type |
type |
| X coordinate on page |
pageX |
* |
pageX |
| Y coordinate on page |
pageY |
* |
pageY |
| Mouse button |
which |
button |
button |
| Keyboard key |
which |
keyCode |
keyCode |
*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.
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.
Return to Working
with JavaScript Index