Advertisement

Javascript and the DOM: Lesson 2

by

Hello and welcome back to the "JavaScript and the DOM" series. Last time we covered some JavaScript basics and we touched on various aspects of the Document Object Model, including how to access nodes and traverse through the DOM. Today we'll be covering how to manipulate elements within the DOM and we'll be discussing the browser event model.

Manipulating Elements

In the last lesson we covered the steps involved in accessing a collection of DOM nodes or a singular DOM node. The real magic occurs when you then manipulate certain properties resulting in what is widely known as "behaviour".

Every single DOM node has a collection of properties; most of these properties provide abstractions to certain functionality. For example, if you have a paragraph element with an ID of 'intro' you could quite easily change the colour of that element via the DOM API:

document.getElementById('intro').style.color = '#FF0000';

To illustrate the object/property nature of this API it might be easier to understand if we break it up by assigning each object to a variable:

var myDocument = document;
var myIntro = myDocument.getElementById('intro');
var myIntroStyles = myIntro.style;
  
// And now, we can set the color:
myIntroStyles.color = '#FF0000';

Now that we have a reference to the 'style' object of the paragraph we can add other CSS styles:

myIntroStyles.padding = '2px 3px 0 3px';
myIntroStyles.backgroundColor = '#FFF';
myIntroStyles.marginTop = '20px';

We're just using basic CSS property names here. The only difference is that where you would normally find a dash('-') the text is camel-cased. So instead of 'margin-top' we use 'marginTop'. The following, for example, wouldn't work and would produce a syntax error:

myIntroStyles.padding-top = '10em';
    
// Produces a syntax error:
//   - The '-' character is the minus operator in JavaScript.
//   - Additionally, there's no such property name.

Properties can be accessed in an array-like manner. So, with this knowledge we could create a small function to change any style of a given element:

function changeStyle(elem, property, val) {
    elem.style[property] = val; // Notice the square brackets used to access the property
}
    
// You would use the above plugin like this:
var myIntro = document.getElementById('intro'); // Grab Intro paragraph
changeStyle(myIntro, 'color', 'red');

This is just an example - to be honest it's probably not a very useful function since, syntactically, it's quicker to use the conventional means shown earlier (e.g. elem.style.color = 'red').

As well as the 'style' property there are plenty of others you can use to manipulate certain aspects of a node/element. In fact, if you have Firebug installed you should try "inspecting an element", then click on the "DOM" tab (normally to the right or below the element viewing panel) to view all of its properties:

DOM Element properties, in the Firebug addon for Firefox
DOM Element properties, in Firebug

All the properties can be accessed using the conventional dot notation (e.g. Element.tabIndex). Not all of the properties are primitive data types (strings, numbers, Booleans etc.); the 'style' property for example, which we discussed earlier, is an object containing its own properties. Many of an element's properties will be readable only; what I mean by this is that you cannot change their value. For example, you cannot directly change the 'parentNode' property of a node. The browser will usually throw an error if you try to change one of these read-only properties: e.g. ERROR: "setting a property that has only a getter". It's just something to be aware of...

One common requirement is to change the content within an element. There are a few different ways of doing this. By far the easiest way is to use the 'innerHTML' property, like this:

var myIntro = document.getElementById('intro');

// Replacing current content with new content:
myIntro.innerHTML = 'New content for the <strong>amazing</strong> paragraph!';

// Adding to current content:
myIntro.innerHTML += '... some more content...';

The only problem with this method is that it's not specified in any standard and isn't in the DOM specification. If you're not bothered about that then go ahead and use it. It's normally much faster than conventional DOM methods anyway, which we'll be covering next.

Nodes

When creating content via the DOM API you need to be aware of two different types of nodes, an element node and a text node. There are many other types of nodes but these two are the only important ones for now.

To create an element you use the 'createElement' method and to create a text node you use the 'createTextNode' method, they are both shown below:

var myIntro = document.getElementById('intro');

// We want to add some content to the paragraph:
var someText = 'This is the text I want to add';
var textNode = document.createTextNode(someText);
myIntro.appendChild(textNode);

Here we're using the 'appendChild' method to add our new text node to the paragraph. Doing it this way takes a little longer than the non-standard innerHTML method but it's still important to know both ways so you can make the right decision. Here's a more advanced example using DOM methods:

var myIntro = document.getElementById('intro');

// We want to add a new anchor to the paragraph:
// First, we create the new anchor element:
var myNewLink = document.createElement('a'); // <a/>
myNewLink.href = 'http://google.com'; // <a href="http://google.com"/>
myNewLink.appendChild(document.createTextNode('Visit Google')); // <a href="http://google.com">Visit Google</a>

// Now we can append it to the paragraph:
myIntro.appendChild(myNewLink);

There is also an 'insertBefore' DOM method which is quite self-explanatory. Using these two methods ('insertBefore' & 'appendChild') we can create our very own 'insertAfter' function:

// 'Target' is the element already in the DOM
// 'Bullet' is the element you want to insert
    
function insertAfter(target, bullet) {
    target.nextSibling ?
        target.parentNode.insertBefore(bullet, target.nextSibling)
        : target.parentNode.appendChild(bullet);
}

// We're using a ternary operator in the above function:
// Its format: CONDITION ? EXPRESSION IF TRUE : EXPRESSION IF FALSE;

The above function checks for the existence of the target's next sibling within the DOM, if it exists then it will insert the 'bullet' node before the target's next sibling, otherwise it will assume that the target is the last child of an element and so it's okay to append the bullet as a child of the parent. The DOM API gives us no 'insertAfter' method because there's no need - we can create it ourselves.

There is quite a bit more to learn about manipulating elements within the DOM but the above should be a sufficient foundation on which you can build.

Events

Browser events are at the very core of any web application and most JavaScript enhancements. It's through these events that we define when something is going to happen. If you have a button in your document and you need some form validation to take place when it's clicked then you would use the 'click' event. Below is an overview of most standard browser events:

Note: As we discussed last time, the DOM and the JavaScript language are two separate entities. Browser events are part of the DOM API, they are not part of JavaScript.

Mouse events

  • 'mousedown' - The mousedown event is fired when the pointing device (usually a mouse) is pressed downwards over an element.
  • 'mouseup' - The mouseup event is fired when the pointing device (usually a mouse) is released over an element.
  • 'click' - The click event is defined as a mousedown followed by a mouseup in exactly the same position.
  • 'dblclick' - This event is fired when an element is clicked twice in quick succession in the same position.
  • 'mouseover' - The mouseover event is fired when the pointing device is moved over an element.
  • 'mouseout' - The mouseout event is fired when the pointing device is moved out of an element. (away from an element)
  • 'mousemove' - The mousemove event is fired when the pointing device is moved while hovering over an element.

Keyboard events

  • 'keypress' - This event is fired whenever a key on the keyboard is pressed.
  • 'keydown' - This event also fires whenever a key is pressed, it runs before the 'keypress' event.
  • 'keyup' - This event is fired when a key is released, after both the 'keydown' and 'keypress' events.

Form events

  • 'select' - This event is fired when text within a textfield (input, textarea etc.) is selected.
  • 'change' - This event is fired when a control loses the input focus and/or the value has been modified since gaining focus.
  • 'submit' - This event is fired when a form is submitted.
  • 'reset' - This event is fired when a form is reset.
  • 'focus' - This event is fired when an element receives focus, usually from a pointing device.
  • 'blur' - This event is fired when an element loses focus, usually from a pointing device.

Other events

  • 'load' - This event is fired when the user agent finished loading all content within a document, including content, images, frames and objects. For elements, such as 'IMG' it fires when the content in question has finished loading.
  • 'resize' - This event is fired when the document view is resized. (i.e. when the browser is resized.)
  • 'scroll' - This event is fired when the document is scrolled.
  • 'unload' - This event is fired when the user agent removes all content from a window or frame, i.e. when you leave a page.

There are plenty more events to choose from. Those shown above are the main ones which you'll come across frequently in JavaScript code. Be aware that some of them have subtle cross-browser differences. Also, be aware that many browsers implement proprietary events, for example there are quite a few Gecko-specific events, such as 'DOMContentLoaded' or 'DOMMouseScroll' - you can read more about these here: https://developer.mozilla.org/en/Gecko-Specific_DOM_Events

Event handling

We've covered the actual events but we've yet to discuss the process of attaching a function to an event. This is where the magic happens. The events listed above will all occur regardless of whether or not you've written any JavaScript, so to harness their power you have to register "event handlers", - this is a fancy term to describe a function being used to handle an event. Here's a simple example using the basic event registration model (also known as "traditional event registration"):

Basic event registration:

<!-- HTML -->
<button id="my-button">Click me!</button>
// JavaScript:
var myElement = document.getElementById('my-button');

// This function will be our event handler:
function buttonClick() {
    alert('You just clicked the button!');
}

// This is the event-registration part:
myElement.onclick = buttonClick;

We've got an HTML button with an ID of 'my-button' and we've accessed it using the 'document.getElementById' command. Then we're creating a new function which is later assigned to the 'onclick' DOM property of the button. That's all there is to it!

The "basic event registration" model is as simple as it gets. You prefix the event you're after with 'on' and access it as a property of whatever element you're working with. This is essentially the unobtrusive version of doing something like this (which I don't recommend):

<button onclick="return buttonClick()">Click me!</button>

Inline event handling (using HTML attributes) is very obtrusive and makes your website a lot harder to maintain. It's better to use unobtrusive JavaScript and have it all contained within respective '.js' files which can be included in the document as/when needed. While we're on the subject of unobtrusive JavaScript I'd like to correct the common misconception that libraries like jQuery make it "possible to code unobtrusively" - this isn't true. When you're using jQuery it's just as easy to do things the wrong way. The reason you shouldn't use inline event handling is exactly the same as the reason you shouldn't apply inline CSS styles (using style="").

Advanced event registration:

Don't let this name mislead you, just because it's called "advanced" doesn't mean it's better to use; in fact, the technique we discussed above ("basic event registration") is perfectly suitable most of the time. Using the basic technique has one key limitation though; you cannot bind more than one function to an event. This isn't so bad actually, because you can just call any number of other functions from within that single function, but if you need more control then there is another way to register handlers, enter the "advanced event registration model".

This model allows you to bind multiple handlers to a single event, meaning that multiple functions will run when an event occurs. Additionally, this model allows you to easily remove any of the bound event handlers.

Strictly speaking, there are two different models in this category; the W3C's and Microsoft's. The W3C model is supported by all modern browsers apart from IE, and Microsoft's model is only supported by IE. Here's how you would use the W3C's model:

// FORMAT: target.addEventListener( type, function, useCapture );
// Example:
var myIntro = document.getElementById('intro');
myIntro.addEventListener('click', introClick, false);

And here's the same, but for IE (Microsoft's model):

// FORMAT: target.attachEvent ( 'on' + type, function );
// Example:
var myIntro = document.getElementById('intro');
myIntro.attachEvent('onclick', introClick);

And here's the 'introClick' function:

function introClick() {
    alert('You clicked the paragraph!');
}

Because of the fact that neither model works in all browsers it's a good idea to combine them both in a custom function. Here's a very basic 'addEvent' function, which works cross-browser:

function addEvent( elem, type, fn ) {
    if (elem.attachEvent) {
        elem.attachEvent( 'on' + type, fn);
        return;
    }
    if (elem.addEventListener) {
        elem.addEventListener( type, fn, false );
    }
}

The function checks for the 'attachEvent' and 'addEventListener' properties and then uses one of the models dependent on that test. Both models make it possible to remove event handlers as well, as shown in this 'removeEvent' function:

function removeEvent ( elem, type, fn ) {
    if (elem.detachEvent) {
        elem.detachEvent( 'on' + type, fn);
        return;
    }
    if (elem.removeEventListener) {
        elem.removeEventListener( type, fn, false );
    }
}

You would use the functions like this:

var myIntro = document.getElementById('intro');
addEvent(myIntro, 'click', function(){
    alert('YOU CLICKED ME!!!');
});

Notice that we passed a nameless function as the third parameter. JavaScript allows us to define and execute functions without naming them; functions of this type are called "anonymous functions" and can be very useful, especially when you need to pass a function as a parameter to another function. We could have just put our 'introClick' function (defined earlier) as the third parameter but sometimes it's more convenient to do it with an anonymous function.

If you want an action to occur on an event only the first time it's clicked you could do something like this:

// Note that we've already defined the addEvent/removeEvent functions
// (In order to use them they must be included)

var myIntro = document.getElementById('intro');
addEvent(myIntro, 'click', oneClickOnly);

function oneClickOnly() {
    alert('WOW!');
    removeEvent(myIntro, 'click', oneClickOnly);
}

We're removing the handler as soon as the event is fired for the first time. We haven't been able to use an anonymous function in the above example because we needed to retain a reference to the function ('oneClickOnly') so that we could later remove it. That said, it is actually possible to achieve with an unnamed (anonymous) function:

addEvent(myIntro, 'click', function(){
    alert('WOW!');
    removeEvent(myIntro, 'click', arguments.callee);
});

We're being quite cheeky here by referencing the 'callee' property of the 'arguments' object. The 'arguments' object contains all passed parameters of ANY function and also contains a reference to the function itself ('callee'). By doing this we completely eliminate the need to define a named function (e.g. the 'oneClickOnly' function shown earlier).

Apart from the obvious syntactic differences between the W3C's and Microsoft's implementation there are some other discrepancies worth noting. When you bind a function to an event the function should be run within the context of the element, and so the 'this' keyword within the function should reference the element; using either the basic event registration model or W3C's advanced model this works without fault, but, Microsoft's implementation fails. Here's an example of what you should be able to do within event handling functions:

function myEventHandler() {
    this.style.display = 'none';
}

// Works correctly, 'this' references the element:
myIntro.onclick = myEventHandler;

// Works correctly, 'this' references the element:
myIntro.addEventListener('click', myEventHandler, false);

// DOES NOT work correctly, 'this' references the Window object:
myIntro.attachEvent('onclick', myEventHandler);

There are a few different ways to avoid/fix this problem. By far the easiest option is to use the basic model - there are almost no cross-browser inconsistencies when using this model. If, however, you want to use the advanced model and you require the 'this' keyword to reference the element correctly then you should have a look at some of more widely adopted 'addEvent' functions, specifically John Resig's or Dean Edward's (his doesn't even use the advanced model, superb!).

The Event Object

One important aspect of event handling which we've yet to discuss is something called the "Event object". Whenever you bind a function to an event, i.e. whenever you create an event handler, the function will be passed an object. This happens natively so you don't need to take any action to induce it. This event object contains a variety of information about the event which just occurred; it also contains executable methods which have various behavioural effects on the event. But, unsurprisingly, Microsoft chose its own way to implement this "feature"; IE browsers do not pass this event object, instead you have to access it as a property of the global window object; this isn't really a problem, it's just a nuisance:

function myEventHandler(e) {

    // Notice the 'e' argument...
    // When this function is called, as a result of the event
    // firing, the event object will be passed (in W3C compliant agents)
    
    // Let's make 'e' cross-browser friendly:
    e = e || window.event;
    
    // Now we can safely reference 'e' in all modern browsers.
    
}

// We would bind our function to an event down here...

In order to check for the existence of the 'e' object (the "Event object") we use an OR (logical) operator which basically dictates the following: if 'e' is a "falsy" value (null, undefined, 0 etc.) then assign 'window.event' to 'e'; otherwise just use 'e'. This is a quick and easy way to get the real Event object in a cross-browser environment. If you're not comfortable with using logical operators outside of an IF statement then this construct might suit you more:

if (!e) {
	e = window.event;
} // No ELSE statement is needed as 'e' will
  // already be defined in other browsers

Some of the most useful commands and properties of this event object are, unfortunately, inconsistently implemented across browsers (namely IE vs. all others). For example, cancelling the default action of an event can be achieved using the 'preventDefault()' method of the Event object but in IE it can only be achieved using the 'returnValue' property of the object. So, again, we have to use both in order to accommodate all browsers:

function myEventHandler(e) {

    e = e || window.event;
    
    // Preventing the default action of an event:
    if (e.preventDefault) {
        e.preventDefault();
    } else {
        e.returnValue = false;
    }
    
}

The default action of an event is what normally happens as a result of that event firing. When you click on an anchor link the default action is for the browser to navigate to the location specified in the 'href' attribute of that link. But sometimes you'll want to disable this default action.

The 'returnValue'/'preventDefault' annoyance is not on its own; many other properties of the Event object are inconsistently implemented so this if/else/or checking model is a required task.

Many of today's JavaScript libraries normalise the event object, meaning that commands like 'e.preventDefault' will be available in IE, although, you should note that behind the scenes the 'returnValue' property is still being utilised.

Event bubbling

Event bubbling, also known as "event propagation", is when an event is fired and then that event "bubbles" up through the DOM. The first thing to note is that not all events bubble, but for those that do, here's how it works:

The event fires on the target element. The event then fires on each and every ancestor of that element - the event bubbles up through the DOM until it reaches the top-most element:

Event bubbling graphic
Event bubbling, illustrated

As shown in the above graphic, if an anchor within a paragraph is clicked the anchor's click event will fire first and then, following that, the paragraphs click event will fire etc. until the body element is reached (the body is the highest DOM element that has a click event).

These events will fire in that order, they don't all occur at the same time.

The idea of event bubbling may not make much sense at first but eventually it becomes clear that it's a fundamental part of what we consider "normal behaviour". When you bind a handler to the click event of the paragraph you expect it to fire whenever the paragraph is clicked, right? Well, that's exactly what "event bubbling" ensures - if the paragraph has multiple children, (<span>s, <anchors>s, <em>s) then even when they're clicked on the event will bubble up to the paragraph.

This bubbling behaviour can be stopped at ANY time during the process. So if you only want the event to bubble up to the paragraph but no further (not to the body node) then you can use another useful method found in the Event object, "stopPropagation":

function myParagraphEventHandler(e) {

    e = e || window.event;
    
    // Stop event from bubbling up:
    if(e.stopPropagation) {
        // W3C compliant browsers:
        e.stopPropagation();
    } else {
        // IE:
        e.cancelBubble = true;
    }
    
} 

// The function would be bound to the click event of the paragraph:
// Using our custom-made addEvent function:
addEvent( document.getElementsByTagName('p')[0], 'click', myParagraphEventHandler );

Event delegation

Let's say, for example, you have a massive table with many rows of data. Binding a click event-handler to every single <tr> can be a dangerous endeavour, mainly because of the negative effect it has on performance. A common way to combat this problem is to use "event delegation". Event delegation describes the process of applying an event handler to a container element and then using that as the basis for all child elements. By testing the 'target' ('srcElement' in IE) property of the event object we can determine the real clicked element.

var myTable = document.getElementById('my-table');

myTable.onclick = function() {

    // Dealing with browser incompatibilities:
    e = e || window.event;
    var targetNode = e.target || e.srcElement;
    
    // Test if it was a TR that was clicked:
    if ( targetNode.nodeName.toLowerCase() === 'tr' ) {
        alert ('You clicked a table row!');
    }
    
}

Event delegation relies on event bubbling. The above code wouldn't work if the bubbling was halted before reaching the 'table' node.

That's it for today!

We've covered how to manipulate DOM elements and we have discussed, in quite a lot of depth, the browser event model. I hope you've learnt something today! As usual, if you have any questions, please don't hesitate to ask.

  • Subscribe to the NETTUTS RSS Feed for more daily web development tuts and articles.


Advertisement