Article

Script Smarter: Quality JavaScript from Scratch

Page: 1 2 3 4 5 6 7 8 Next

Chapter 13. Basic Dynamic HTML

Dynamic HTML isn't a single piece of technology that you can point to and say, "This is DHTML." The term is a descriptor that encompasses all of the technologies that combine to make a web page dynamic: the technologies that let you create new elements without refreshing the page, change the color of those elements, and make them expand, contract, and zoom around the screen.

DHTML uses HTML, the DOM, and CSS in combination with a client-side scripting language -- JavaScript -- to bring life to what was traditionally a static medium. In previous chapters, we learned that we can use JavaScript to manipulate parts of a page to achieve some very handy results. DHTML provides solutions to much more complex problems by assembling these parts into a coherent whole -- one that satisfies real-world needs, rather than programming puzzles.

This chapter explores a few of the tools we need in order to create effective user interfaces with DHTML. It then discusses a couple of simple widgets in preparation for the more complex modules we'll consider throughout the rest of this book.

Handling Events

Any interaction that users have with a web page -- whether they're moving the mouse or tapping the keyboard -- will cause the browser to generate an event. Sometimes, we want our code to respond to this interaction, so we listen for these events, which let us know when we should execute our code.

Solution

There are two ways to handle events: the short way, and the W3C way. Each has its pros and cons, but both allow you to execute a specified function when an event occurs on a particular element.

The Short Way: Using Event Handlers

The shorter way of handling an event is to use the DOM 0 event handlers that are assigned as shortcut properties of every element. Much as we saw in Chapter 5, Navigating the Document Object Model when we discussed DOM 0 attribute shortcuts, these event handlers are not future-proof. However, they do offer some advantages over standard W3C event listeners:

  • Every browser that's currently in operation supports DOM 0 event handlers without the need for code branching.
  • Each function executed by a DOM 0 event handler has access to the exact element to which the event handler was assigned. (As you'll see later, this is not always available in W3C event listeners.)

The main problem with utilizing DOM 0 event handlers is that they are not designed to work with multiple scripts. Every time you assign a DOM 0 event handler, you overwrite any previously assigned handler for that event. This can interfere with the operation of multiple scripts that require event handling on the same element. With W3C event listeners, you can apply any number of event listeners on the same element, and enjoy the ability to remove any of them at any time.

If you can be certain that your code will not interfere with someone else's event handling (e.g., you're placing events on elements that are created dynamically in your own script), it will be safe to use DOM 0 event handlers. But -- all things being equal -- it is safer to use the W3C event listeners wherever practical, as we do in this book.

A number of DOM 0 event handlers are available via the browser; Table 13.1, "DOM 0 event handlers" lists the most commonly used handlers.

Table 13.1. DOM 0 event handlers
Table listing DOM 0 event handlers

In using DOM 0 event handlers, once you have a reference to the element whose events you want to handle, it's a simple matter of assigning a handling function to the appropriate property:

Example 13.1. handle_events.js (excerpt)      
     
var mylink = document.getElementById("mylink");      
     
mylink.onclick = engage;      
...      
function engage()      
{      
 alert("Engage!");      
     
 return false;      
}

You'll note that, in the function assignment (button.onclick = engage;), parentheses do not follow the function name. Their inclusion would execute the function immediately, and assign the return value as the event handler. By omitting the parentheses, you can assign the function itself to the handler. This also means that you cannot supply arguments directly to the handling function: the function must obtain its information through other means.

Anonymous Functions

Instead of supplying a reference to a named function, you can supply an anonymous function for an event handler:

var mylink = document.getElementById("mylink");      
     
mylink.onclick = function()      
{      
 alert("Engage!");      
     
 return false;      
}

Depending on whether you need to reuse the handling function (and your own coding preferences), this can be an easier way of writing event handling code.

The return value of the handling function determines whether the default action for that event occurs. So, in the preceding code, if mybutton were a hyperlink, its default action when clicked would be to navigate to its href location. By returning false, the engage function does not allow the default action to occur, and the hyperlink navigation will not take place. If the return value were true, the default action would occur after the event handling function's code had executed.

When an event occurs, detailed information about the how, why, and where of that event is written to an event object. In Internet Explorer, this takes the form of a global window.event object, but in other browsers the object is passed as an argument to the event-handling function. This difference is fairly easy to address within the handling function:

Example 13.2. handle_events2.js (excerpt)      
     
function engage(event)      
{      
 if (typeof event == "undefined")      
 {      
   event = window.event;      
 }      
     
 alert("The screen co-ordinates of your click were: " +      
     event.screenX + ", " + event.screenY);      
     
 return false;      
}

The event object allows you to find out a range of details, such as which element was clicked, whether any keys were pressed, the coordinates of the event (e.g., where the cursor was located when the mouse button was clicked), and the type of event that triggered the function. Quite a few of the event property names are consistent across browsers, but a few differ. The Mozilla event properties can be viewed at the Gecko DOM Reference, while the Internet Explorer event properties can be seen at MSDN. For properties whose names vary between browsers, the potential for associated problems can normally be rectified with a little object detection; we'll discuss this in detail later in this chapter.

The W3C Way (Event Listeners)

Although the DOM 0 event handlers are quick and easy, they do have limitations (aside from the fact that eventually they will become deprecated). The main advantage of the W3C event listeners is that they natively support the addition and removal of multiple handling functions for the same event on a single element. Event listeners also have the capability to respond to events in several phases (though most browsers don't yet support this capability).

In the W3C specification, an event can be added to an element using the element's addEventListener method, but Internet Explorer for Windows chooses to use a method called attachEvent, which has a slightly different syntax. (Internet Explorer for Mac doesn't support either of these event models, so we have to rely on the DOM 0 handlers to work with events in this browser.)

To add an event listener in every browser except Internet Explorer, you would write code similar to this:

var mylink = document.getElementById("mylink");      
     
mylink.addEventListener("click", engage, false);

To support Internet Explorer, you'd need this code:

var mylink = document.getElementById("mylink");      
     
mylink.attachEvent("onclick", engage);

As well as the differing function names, it's important to note that Internet Explorer uses the DOM 0 handler name for the event -- "onclick" -- rather than the true event name: "click". The extra argument that's supplied to addEventListener specifies whether the listener is applied during the capture (true) or bubble (false) event propagation phase. Event propagation is explained in more detail in the discussion below, but bubble is really the most useful choice, and ensures the same behavior in standards-compliant browsers as in Internet Explorer.

The differences between these two approaches are fairly easy to work around using an abstracting function. We can also provide a fallback for browsers that don't support W3C event listeners at the same time:

Example 13.3. handle_events3.js (excerpt)      
     
function attachEventListener(target, eventType, functionRef,      
   capture)      
{      
 if (typeof target.addEventListener != "undefined")      
 {      
   target.addEventListener(eventType, functionRef, capture);      
 }      
 else if (typeof target.attachEvent != "undefined")      
 {      
   target.attachEvent("on" + eventType, functionRef);      
 }      
 else      
 {      
   eventType = "on" + eventType;      
     
   if (typeof target[eventType] == "function")      
   {      
     var oldListener = target[eventType];      
     
     target[eventType] = function()      
     {      
       oldListener();      
     
       return functionRef();      
     };      
   }      
   else      
   {      
     target[eventType] = functionRef;      
   }      
 }      
}

The first two if statements deal with the standards-based and Internet Explorer methods respectively, but the catch-all else deals with older browsers that don't support either of these methods, particularly Internet Explorer 5 for Mac. In this last case, a DOM 0 event handler is used, but to ensure that multiple functions can be used to handle a single event for a particular element, a closure is used to execute any existing functions that are attached to the event.

Closures are an advanced feature of JavaScript that relates to scoping (which you can read about in Chapter 19, Object Orientation in JavaScript). Closures allow an inner function to reference the variables of the containing function even after the containing function has finished running. Simon Willison has explained their usage in relation to event handlers in some detail. Suffice it to say that closures allow us to stack multiple event handlers in browsers that don't support W3C event listeners.

The cross-browser code for assigning an event listener is as follows:

Example 13.4. handle_events3.js (excerpt)      
     
var mylink = document.getElementById("mylink");      
     
attachEventListener(mylink, "click", engage, false);

Not (quite) the Genuine Article

Although the DOM 0 event handler fallback mimics the ability to add multiple event listeners for one event type on an element, it does not provide exact replication of the W3C event model, because specific handlers cannot be removed from an element.

Whereas DOM 0 handlers allowed the cancellation of an element's default action by returning false, W3C event listeners achieve this goal slightly differently. To cancel a default action in this model, we need to modify the event object. Internet Explorer requires you to set its returnValue property to false; standards-based implementations offer the preventDefault method to do the same thing. We can create a small function that figures out the difference for us:

Example 13.5. handle_events4.js (excerpt)      
     
function stopDefaultAction(event)      
{      
 event.returnValue = false;      
     
 if (typeof event.preventDefault != "undefined")      
 {      
   event.preventDefault();      
 }      
}

We can call this function whenever we want to cancel the default action:

Example 13.6. handle_events4.js (excerpt)      
     
function engage(event)      
{      
 if (typeof event == "undefined")      
 {      
   event = window.event;      
 }      
     
 alert("Engage!");      
     
 stopDefaultAction(event);      
     
 return false;      
}

You still need to return false after executing stopDefaultAction in order to ensure that browsers that don't support the W3C event model will also prevent the default action.

Safari and W3C Event Listeners

Due to a bug in Safari, it's impossible to cancel the default action of clicking a hyperlink in that browser when using W3C event listeners. To achieve the cancellation, you'll have to use DOM 0 event handlers with a return value of false.

Checking for attachEvent

Internet Explorer for Windows actually passes an event object to the event-handling function when attachEvent is used to attach an event listener. However, we still need to check for the existence of this object for any browsers that use the old event model.

One of the advantages of using W3C event listeners is that you can remove an individual listener from an element without disturbing any other listeners on the same event. This is not possible using the DOM 0 handlers.

Internet Explorer uses the detachEvent method, while the standards-compliant browsers instead specify a method called removeEventListener. Each of these methods operates fairly similarly to its listener-adding counterpart: an event type must be supplied along with the function that was assigned to handle that event type. The standard method also demands to know whether the event handler was registered to respond during the capture or bubble phase.

Here's a function that supports this approach across browsers:

Example 13.7. handle_events5.js (excerpt)      
     
function detachEventListener(target, eventType, functionRef,      
   capture)      
{      
 if (typeof target.removeEventListener != "undefined")      
 {      
   target.removeEventListener(eventType, functionRef, capture);      
 }      
 else if (typeof target.detachEvent != "undefined")      
 {      
   target.detachEvent("on" + eventType, functionRef);      
 }      
 else      
 {      
   target["on" + eventType] = null;      
 }      
}

The W3C Event Model and Anonymous Functions

The W3C event model doesn't allow for the removal of anonymous functions, so if you need to remove an event listener, hang onto a reference to the function in question.

In browsers that don't support W3C event listeners, this function removes all event handlers on the given event: it's not possible to remove just one of them and leave the others.

Discussion

Referencing the Target Element

Quite often, you'll want to use the object that was the target of an event inside the event handler itself. With DOM 0 event handlers, the use of the special variable this inside a handling function will refer to the event target object. Consider this code:

Example 13.8. handle_events6.js (excerpt)      
     
var mylink = document.getElementById("mylink");      
     
mylink.onclick = engage;      
...      
function engage()      
{      
 var href = this.getAttribute("href");      
     
 alert("Engage: " + href);      
     
 return false;      
}

Here, this refers to the link with ID mylink. We can use it to get the link's href attribute.

However, if you use W3C event listeners, the target of the event is stored as part of the event object, under different properties in different browsers. Internet Explorer stores the target as srcElement, while the standards model stores it as target. But the element to which these properties point isn't necessarily the element to which the event listener was assigned. It is, in fact, the deepest element in the hierarchy affected by the event. Take a look at the following HTML.

Example 13.9. handle_events6.html (excerpt)      
     
<p>      
 These are the voyages of the <a id="mylink"      
     href="enterprise.html">starship Enterprise</a>.      
</p>

If a click event listener were placed on the paragraph and a user clicked on the link, the paragraph's click event handler would be executed, but the event target that was accessible through the above-mentioned properties would be the hyperlink. Some browsers (most notably, Safari) even go so far as to count the text node inside the link as the target node.

We can write a function that returns the event target irrespective of which property has been implemented, but this does not solve the problem of finding the element to which we originally applied the event listener. (The W3C Standard specifies another property called currentTarget, which lets you get the element to which the listener was assigned, but there is no Internet Explorer equivalent. Browsers that support currentTarget also set up the event handler-style this variable with the same value, but again, without Internet Explorer support, this isn't particularly useful.) Often, the best resolution to this quandary is to iterate upwards from the event target provided by the browser until we find an element that's likely to be the element to which we attached an event listener. To do this, we can perform checks against the element's tag name, class, and other attributes.

The abstracting event target function would look like this:

Example 13.10. handle_events7.js (excerpt)      
     
function getEventTarget(event)      
{      
 var targetElement = null;      
     
 if (typeof event.target != "undefined")      
 {      
   targetElement = event.target;      
 }      
 else      
 {      
   targetElement = event.srcElement;      
 }      
     
 while (targetElement.nodeType == 3 &&      
     targetElement.parentNode != null)      
 {      
   targetElement = targetElement.parentNode;      
 }      
     
 return targetElement;      
}

The if-else retrieves the event target across browsers; the while loop then finds the first non-text-node parent if the target reported by the browser happens to be a text node.

If we want to retrieve the element that was clicked upon, we then make a call to getEventTarget:

Example 13.11. handle_events7.js (excerpt)      
     
var mylink = document.getElementById("mylink");      
     
attachEventListener(mylink, "click", engage, false);      
...      
function engage(event)      
{      
 if (typeof event == "undefined")      
 {      
   event = window.event;      
 }      
     
 var target = getEventTarget(event);      
     
 while(target.nodeName.toLowerCase() != "a")      
 {      
   target = target.parentNode;      
 }      
     
 var href = target.getAttribute("href");      
     
 alert("Engage: " + href);      
     
 return true;      
}

Because we know, in this case, that the event-handling function will be attached only to links (<a> tags), we can iterate upwards from the event target, checking for a node name of "a". The first one we find will be the link to which the handler was assigned; this ensures that we aren't working with some element inside the link (such as a strong or a span).

Obviously, this method of target finding is not ideal, and cannot be 100% accurate unless you have knowledge of the exact HTML you'll be working with. Recently, much effort has gone into resolving this problem, and quite a few of the proposed solutions offer the same this variable as is available under DOM 0 event handlers, and in browsers that support the W3C Standard for event listeners (not Internet Explorer).

One such solution is to make the event listening function a method of the target object in Internet Explorer. Then, when the method is called, this will naturally point to the object for which the method was called. This requires both the attachEventListener and detachEventListener to be modified:

Example 13.12. handle_events8.js (excerpt)      
     
function attachEventListener(target, eventType, functionRef,      
   capture)      
{      
 if (typeof target.addEventListener != "undefined")      
 {      
   target.addEventListener(eventType, functionRef, capture);      
 }      
 else if (typeof target.attachEvent != "undefined")      
 {      
   var functionString = eventType + functionRef;      
   target["e" + functionString] = functionRef;      
     
   target[functionString] = function(event)      
   {      
     if (typeof event == "undefined")      
     {      
       event = window.event;      
     }      
     target["e" + functionString](event);      
   };      
     
   target.attachEvent("on" + eventType, target[functionString]);      
 }      
 else      
 {      
   eventType = "on" + eventType;      
     
   if (typeof target[eventType] == "function")      
   {      
     var oldListener = target[eventType];      
     
     target[eventType] = function()      
     {      
       oldListener();      
     
       return functionRef();      
     }      
   }      
   else      
   {      
     target[eventType] = functionRef;      
   }      
 }      
}      
     
function detachEventListener(target, eventType, functionRef,      
   capture)      
{      
 if (typeof target.removeEventListener != "undefined")      
 {      
   target.removeEventListener(eventType, functionRef, capture)      
 }      
 else if (typeof target.detachEvent != "undefined")      
 {      
   var functionString = eventType + functionRef;      
     
   target.detachEvent("on" + eventType, target[functionString]);      
     
   target["e" + functionString] = null;      
   target[functionString] = null;      
 }      
 else      
 {      
   target["on" + eventType] = null;      
 }      
}

This line of thinking was well represented in entries to Peter Paul Koch's improved addEvent competition.

Another solution by Dean Edwards totally eschews the W3C event model in favor of implementing DOM 0 event handlers with independent add and remove abilities.

Although both of these solutions may prove to be well written and robust, they're largely untested as of this writing, so we'll stick with the approach whose flaws we know and can handle: the one presented in the main solution. Besides, in practice, the process of iterating to find an event's target isn't as unreliable as it may appear to be.

What is Event Bubbling, and How do I Control it?

You may have noticed that we needed to supply a third argument to the W3C Standard addEventListener method, and that a capture argument was included in our attachEventListener function to cater for this. This argument determines the phase of the event cycle in which the listener operates.

Suppose you have two elements, one nested inside the other:

<p>      
 <a href="untimely_death.html">Nameless Ensign</a>      
</p>

When a user clicks on the link, click events will be registered on both the paragraph and the hyperlink. The question is, which one receives the event first?

The event cycle contains two phases, and each answers this question in a different way. In the capture phase, events work from the outside in, so the paragraph would receive the click first, then the hyperlink. In the bubble phase, events work from the inside out, so the anchor would receive the click before the paragraph.

Internet Explorer and Opera only support bubbling, which is why attachEvent doesn't require a third argument. For browsers that support addEventListener, if the third argument is true, the event will be caught during the capture phase; if it is false, the event will be caught during the bubble phase.

In browsers that support both phases, the capture phase occurs first and is always followed by the bubble phase. It's possible for an event to be handled on the same element in both the capture and bubbling phases, provided you set up listeners for each phase.

These phases also highlight the fact that nested elements are affected by the same event. If you no longer want an event to continue propagating up or down the hierarchy (depending upon the phase) after an event listener has been triggered, you can stop it. In Internet Explorer, this involves setting the cancelBubble property of the event object to true; in the W3C model, you must instead call its stopPropagation method:

Example 13.13. handle_events9.js (excerpt)      
     
function stopEvent(event)      
{      
 if (typeof event.stopPropagation != "undefined")      
 {      
   event.stopPropagation();      
 }      
 else      
 {      
   event.cancelBubble = true;      
 }      
}

If we didn't want an event to propagate further than our event handler, we'd use this code:

Example 13.14. handle_events9.js (excerpt)      
     
var mylink = document.getElementById("mylink");      
     
attachEventListener(mylink, "click", engage, false);      
     
var paragraph = document.getElementsByTagName("p")[0];      
     
attachEventListener(paragraph, "click", engage, false);      
     
function engage(event)      
{      
 if (typeof event == "undefined")      
 {      
   event = window.event;      
 }      
     
 alert("She canna take no more cap'n!");      
     
 stopEvent(event);      
     
 return true;      
}

Although we have assigned the engage function to listen for the click event on both the link and the paragraph that contains it, the function will only be called once per click, as the event's propagation is stopped by the listener the first time it is called.

Finding the Size of an Element

There are so many variables that affect the size of an element -- content length, CSS rules, font family, font size, line height, text zooming ... the list goes on. Add to this the fact that browsers interpret CSS dimensions and font sizes inconsistently, and you can never predict the dimensions at which an element will be rendered. The only consistent way to determine an element's size is to measure it once it's been rendered by the browser.

Solution

You can tell straight away that it's going to be useful to know exactly how big an element is. Well, the W3C can't help: there's no standardized way to determine the size of an element. Thankfully, the browser-makers have more or less settled on some DOM properties that let us figure it out.

Although box model differences mean that Internet Explorer includes padding and borders inconsistently as part of an element's CSS dimensions, the offsetWidth and offsetHeight properties will consistently return an element's width -- including padding and borders -- across all browsers.

Let's imagine that an element's dimensions were specified in CSS like this:

Example 13.15. find_size_element.css      
     
#enterprise      
{      
 width: 350px;      
 height: 150px;      
 margin: 25px;      
 border: 25px solid #000000;      
 padding: 25px;      
}

We can determine that element's exact pixel width in JavaScript by checking the corresponding offsetWidth and offsetHeight properties:

Example 13.16. find_size_element.js (excerpt)      
     
var starShip = document.getElementById("enterprise");      
var pixelWidth = starShip.offsetWidth;      
var pixelHeight = starShip.offsetHeight;

In Internet Explorer 6, Opera, Mozilla, and Safari, the variable pixelWidth will now be set to 450, and the variable pixelHeight will be set to 250. In Internet Explorer 5/5.5, pixelWidth will be 350 and pixelHeight 150, because those are the dimensions at which the broken box model approach used in those browsers will render the element. The values are different across browsers, but only because the actual rendered size differs as well. The offset dimensions consistently calculate the exact pixel dimensions of the element.

If we did not specify the dimensions of the element, and instead left its display up to the default block rendering (thus avoiding the box model bugs), the values would be comparable between browsers (allowing for scrollbar width differences, fonts, etc.).

Attaining the Correct Dimensions

In order to correctly determine the dimensions of an element you must wait until the browser has finished rendering that element, otherwise the dimensions may be different from those the user ends up seeing. There's no guaranteed way to ensure that a browser has finished rendering an element, but it's normally safe to assume that once a window's load event has fired, all elements have been rendered.

Discussion

It is possible to retrieve the dimensions of an element minus its borders, but including its padding. These values are accessed using the clientWidth and clientHeight properties, and for the example element used above their values would be 300 and 100 in Internet Explorer 5/5.5, and 400 and 200 in all other browsers.

There is no property that will allow you to retrieve an element's width without borders or padding.

If you liked this article, share the love:
Print-Friendly Version Suggest an Article

Sponsored Links