Working with HTML - Getting Good with JavaScript (2011)

Getting Good with JavaScript (2011)

Working with HTML

Up until now, I've tried to talk about JavaScript in a very open way, meaning that pretty much everything you've learned so far will work in any JavaScript environment. However, there's no hiding the fact that you'll probably be writing most of your JavaScript for websites—that is, for running in a browser. So that's what we'll talk about here.

Kids, Meet the DOM

Sounds simple enough, no? The tough part is that "a browser" could mean

· Internet Explorer

· Firefox

· Google Chrome

· Safari and Mobile Safari

· Opera

· ad finitum

Unfortunately, there are at least five major browsers your code has to run in, not to mention the different versions of those browsers.

So what? Well, here's how it all plays out: every web browser implements an interface by which you can interact with the HTML elements that are in the webpage that your JavaScript is running on. But let's back up one more step. When the browser loads a page, it converts the HTML text that you have written into a tree of nodes.

ROCKSTAR TIP

Not sure what a node is? Think of a family tree: each person is a node. In fact, the idea of a family is quite prevalent in the browser: nodes can have siblings, parents and children.

We'll come back to nodes soon; but for now, realize that the browser turns every HTML element into a node. This is half of what the browser brings to the table. The other half is the functionality to access all those things: it's a JavaScript object that allows you to find and manipulate this tree of nodes.

These two parts together are called the Document Object Model or DOM, for short.

So, why is this all so unfortunate? Well, the main issue is that all the different browsers and versions aren't anywhere near harmonious in their implementation of the DOM standard … especially Internet Explorer. This makes it rather difficult to program, as you're often checking for one feature or another, and performing the same action in two or more ways to get one job done. This isn't fun.

The other problem is that the DOM is a pretty clumsy interface to work with; it's not designed nicely at all. Yes, this is definitely an opinion, but it's the opinion of the healthy majority of professional JavaScript developers. However, it's probably not going anywhere anytime soon, so you'll have to get used to it.

Actually, you don't have to get used to it … but it's a good idea if you do. Because if it's awkwardness, stacks of JavaScript libraries have been created to ease the pain. You could jump immediately to something like jQuery or Mootools, but it's better to learn the "real" way first.

So, let's do it!

Nodes

We talked a bit about nodes previously. Every element in your HTML becomes a node in the DOM. Also, every chunk of text in your HTML becomes a node as well (a text node, obviously). Also, the attributes of elements (like the id or class) are nodes. So are the comments in a file.

So, if we had this HTML:

<p>We're learning about the <em>Document Object Model</em>, or <strong>DOM</strong>.</p>

We would have a DOM tree like this:

Fig. 5-1. DOM Tree of Nodes

The element nodes are bold and the text nodes are italic. As you can see, each element and bunch of sequential text is a node.

Finding Elements

We've had enough theory for now; let's look at the interface to the HTML that the DOM gives us.

ROCKSTAR TIP

But first, a word about compatibility . . . because remember, not all browsers support everything. We're going to be looking at a limited set of DOM functionality in this book, but there will still be some stuff that's not supported everywhere. So, rule of thumb: if I don't mentioned browser support, assume the functionality is supported in the following browsers

· Internet Explorer 6+

· Firefox 3+

· Opera 10+

· Safari 4+

· Chrome 5+

I think it's fairly safe to assume that site visitors on Firefox, Opera, Chrome, and Safari will be using a recent version of the browser. If there's an issue with one of the above browsers, I'll point that out and try to give a solution.

Often, you'll want to find one or more elements and so something with them. There are a couple of ways to do this.

By Id

Most of the time, you'll want to get a single element with a given id. Here's the way we do it:

Example 5.1

console.log( document.getElementById("id_name") );

Pretty much every interaction with the DOM starts with the document object. Here, we're calling the getElementById method, and passing it a single string parameter: the id of the object we want to find. If the method finds the element in the DOM, it will return it. If not, it will return null.

By Class

If you want to get a group of elements, there are probably two things these elements have in common: the class name, or the HTML tag name (not always, but often). Hence, we can find elements based on those criteria, too. Try this:

Example 5.2

console.log( document.getElementsByClassName("warning") );

Pretty obvious name, right? However, now the discrepancies start. Internet Explorer 6, 7, and 8 don't have the document.getElementByClassName function. Although this is unfortunate, this is actually where JavaScript shines: it's relatively easy (if you're familiar with the cross-browser DOM idiosyncrasies) to create your own getElementsByClassName function. You can find the different ways to do it and how their speeds compare in an article by John Resig. With your current knowledge, you might have a bit of trouble understanding the code, but give it a try. At the very least, you can copy it and use it in your projects.

By Tag Name

If you want to get all the elements with the same tag, you can probably guess what to use:

Example 5.3

console.log( document.getElementsByTagName("p") );

Predictable, well-supported; nothing to dislike here.

Traversing the DOM

Most often, you'll get your elements via one of the above functions, do what you want to do to then and move on. However, occasionally you'll want to get a node and work with other nodes close to it. For example, You might have something like this:

<div class="product">

<h2> Product Name </h2>

<img src="pic.jpg" />

<p> Description </p>

</div>

You'll have, say, a dozen or so of these on a given page. Let's say you want to sort the products alphabetically by name. To do this, you'll need to get the h2 elements. However you'll also have other h2s on the page. This means you need to get all the divs with a class "product," and then find the h2 within each. This requires some DOM traversal skills.

Before we continue with out scenario, let's go back to that idea of a node tree. Trees are actually a very important part of computer science. You know that for a tree to be a tree, there have to be links between parent nodes and child nodes; these are one-way links. It's inefficient to have a link from the parent node to every child node; so instead, trees have a link from every parent node to its first child node. Then, every child node has a link to it's next sibling.

The DOM works exactly this way: every node has a link to its first child and its next sibling; it also has a link to its parent node. That means our HTML snippet above looks like this:

Fig. 5-2. DOM Tree snippet

Now let's move back to our scenario. With the theory we've just learned, we can see that what we need to do is find all the elements with the class "product" and then find the h2 within it. You might think the h2 would be the first child, but it actually isn't (yes, I intentionally mis-drew the chart for simplicities sake). This is because any whitespace between the opening div tag and the opening h2 tag is captured in a text node. Then, if there are any HTML comments between those two opening tags, that gets put in a comment node. So there could possibly be many nodes to jump over before we get to the h2. What's a developer to do?

Thankfully, each of these different types of nodes has a property called nodeType; they are as follows:

· HTML elements = 1

· HTML element attributes = 2

· Text nodes = 3

· Comment nodes = 8

· The document = 9

So, to find that h2, we just have to get the first child of the div node; then, we check the nodeType property of that node. If it's not 1, we get the next sibling node and check that. Here's the code for that:

Example 5.4

var products = document.getElementsByClassName("product"),

i = 0,

child;

for ( ; i< products.length; i++) {

child = products[i].firstChild;

while (child.nodeType !== 1) {

child = child.nextSibling;

}

console.log(child);

}

We start by getting all the elements with that class "product"; remember, since we've getting more than one element, this returns an array (Well, it's not really an array; it's a NodeList, which can be iterated over like an array, as we see here). For each node in our list, we get the first child via the firstChild property and assign it to the variable child. Then, we're using a while loop to find the first node with a nodeType of 1 (meaning an HTML element). Our condition says "while the node type of child node is not 1"; this means that if the firstChild is an element, the loop won't run. If it's not, we'll run the loop, which simply reassigns the child variable to child.nextSibling (the next sibling of child). When we find the first element in the list of children, we can do what we want to do. We're not going to do any more than print it out to the console right now, so you can see that is, in fact, the h2.

Those are the basics of traversing the DOM. There are a few more links between nodes that we'll wrap up this discussion with.

childNodes

Every node has a property called childNodes; it's a NodeList (remember, an array-like object) containing all the child nodes of the node. Don't forget, it's zero-based.

Example 5.5

// <ul id="container">

// <li>one</li>

// <li>two</li>

// </ul>

var my_node = document.getElementById("container");

my_node.childNodes[1]; // gets the first list item (the second child node)

children

While childNodes holds all node types, children just includes the element nodes. These are usually what you want, so this is probably more useful that childNodes. However, IE 6 - 8 includes comment nodes for some reason, so beware of that.

lastChild

Works just like firstChild, except it gets—wait for it—the last child node.

previousSibling

Just as you'd (hopefully) expect, the previousSibling property returns the previous sibling of the node.

parentNode

This is the one-way link every node has to its parent. Think back to our bunch of product divs. We wanted to sort them by product name. This meant we needed to find the h2 element, to sort by. But if we actually got to the sorting step, we would have used parentNode to get the div parent for each h2.

Example 5.6

// <ul>

// <li id="first">one</li>

// <li>two</li>

// </ul>

var my_node = document.getElementById("first");

console.log( my_node.parentNode); // ul element

Others

There are a bunch of properties for traversing the DOM that are incredibly useful, but not incredibly well-supported. They are as follows:

· firstElementChild

· lastElementChild

· nextElementSibling

· previousElementSibling

You can probably guess what they do; because most of the time you just want to work with element nodes, these properties do exactly as their already-seen counterparts do, except they ignore all those other types of nodes that just get in the way.

That's the good news. The bad news is that they aren't supported in IE 6 - 8 or Firefox 3.0 (they are in IE 9 and Firefox 3.5).

Adding, Removing, and Modifying Elements

Obviously, there's more to using the DOM than finding the right elements. Once you're where you need to be, you want to do something. When you boil it down, that something could be one of two things:

· Change the existing DOM tree (by adding, removing, or shuffling nodes)

· Change the information surrounding a given node or set of nodes. That information could be the nodes style, events, attributes, etc.

We'll come back to that second one soon. For now, let's talk about shaking things up in the DOM. Thankfully, everything here has had rock-solid support for years.

Creating Nodes

If we want to add a node to the DOM, we must first create it. To do that, we just use methods of that document object.

To create an element, we use document.createElement, passing it the tag name of the element we want to create.

var paragraph = document.createElement('p');

It's just as easy to create a text node: this time we use the document.createTextNode:

var text_node = document.createTextNode("Put your text here");

Once you've created your nodes, you'll want to put them into the DOM. Let's see how!

Adding Nodes to the DOM

Now that you're somewhat familiar with the notion of trees, you'll realise that to insert a node into the tree, you'll have to do so in reference to another node. Think about it for a moment, and you'll see that you describe any insertion positioning in one of two ways:

· As the last child element of a given node

· As the previous sibling node of a given node

If you want to make your new node the last child of its soon-to-be parent node, use the appendChild method of nodes.

Assume we have the paragraph and text_node elements from above. While they aren't in the DOM yet, their DOM incarnations would look like this, respectively: <p></p> and Put your text here.

Example 5.7

// <div id="container"></div>

var paragraph = document.createElement("p"),

textNode = document.createTextNode("This is some text");

paragraph.appendChild(textNode);

document.getElementById("container").appendChild(paragraph);

Now, the HTML will look like this:

<div id="container">

<p>This is some text.</p>

</div>

As you can see, this works for both text nodes and element nodes. To insert the text node we've created into the paragraph element node, we just use appendChild. We then use the same method to insert the paragraph into the DOM. This will make the paragraph appear on the page.

If you don't want your node appended as the last child of its parent, you'll have to get a reference to a child node you want it inserted before. Then, use the insertBefore method. Here's how we would insert another text node into our paragraph above, before the text node that's currently alone:

Example 5.8

// <div id="container">Text that is already here.</div>

var newTextNode = document.createTextNode("Hello World!"),

container = document.getElementById("container"),

textNode = container.firstChild;

container.insertBefore(newTextNode, textNode);

Of course, this works for elements nodes as well.

Removing Nodes

When working with the DOM, it's all give and take. This means you'll occasionally want to remove elements from the DOM.

Removing elements is a bit trickier than adding them; it makes sense that you need to know about the nodes surrounding your node to put it into the DOM. However, shouldn't you be able to just remove an element without worrying about where it is? That's unfortunately not the case. You need to know the element you want to remove and its parent.

the_parent.removeChild(child_to_remove);

Because of this, you'll probably see this a lot:

Example 5.9

// <div> Kill this: <span id="kill_me"> Kill me!</span> </div>

var child = document.getElementById("kill_me");

child.parentNode.removeChild(child);

Get the child, and use its parentNode property to find the parent; then, remove the child you started with. Ah, well; that's the DOM for you.

There's another method of removing DOM nodes, that you'll probably find useful on occasion. That's replaceChild. Predictably, you hand it the new node and the node you want to replace, and it will swap in the new one while destroying the old one.

Example 5.10

// <div id="container">

// <p id="remove_me"> Remove Me!</p>

// </div>

var container = document.getElementById("container"),

old_element = document.getElementById("remove_me"),

new_element = document.createElement("p");

new_element.appendChild(document.createTextNode("new element"));

container.replaceChild(new_element, old_element);

Modifying a DOM Element

Now that you're familiar with creating, adding, and removing DOM nodes, let's talk about modifying nodes. After all, there are three main actions you can perform on an individual node:

· You can change the attributes.

· You can change the styling.

· You can add and remove event handlers.

Let's start with attributes. If you're not sure what I'm talking about, check this out:

<div id="content" class="wide"></div>

<input type="text" name="first_name" value="Bob" />

<span class="warning"></span>

Attributes are the key="value" part of your HTML elements. Here, I'm using id, class, type, name, and value, but there are many more.

There are a couple of ways to work with attributes, but I'm just going to show you what the best, most cross-browser way to do it is. That way is using the methods setAttribute and getAttribute. They work as follows:

Example 5.11

// <div id="container"></div>

var elem = document.getElementById("container");

elem.setAttribute("class", "modal");

alert( elem.getAttribute("class") ); // "modal"

As you can see, we pass two parameters to the first method: the name of the attribute we want to set, and the value for the attribute. When we're getting the attribute, we just have to pass the attribute name to the getAttribute method. Obviously, there are methods of an element node.

This should work with any attribute you want to set on your element—even the new HTML5 data-* attributes. However, there's an easier way set and get some common attributes.

If you're working with the id or class, you can just do this:

elem.id = "content";

elem.id; // "content"

elem.className = "module";

elem.className; // "module";

This shorthand notation is the go-to way of setting styling on elements. Watch this:

Example 5.12

// <div id="container">container</div>

var elem = document.getElementById("container");

elem.style.border = "1px solid red";

elem.style.backgroundColor = "green";

Yup; it's all the CSS properties that you already know and love, right in your JavaScript. The key thing to note it this: JavaScript property names can't have dashes in them, like they do in CSS. Therefore, use camelCase for those, capitalizing every letter after a dash and removing the dashes. This way, border-bottom-width becomes borderBottomWidth, and so on.

Another important thing to note is that all the style properties are strings; this makes sense if you think about it, because even numerical values like borderBottomWidth need to be suffixed with a unit (px). So, it's all strings here.

One more thing: when you're using element.style, you're working with the inline style attribute on the element. When you set a styling property, that gets added to the style attribute. The logical following of this is that you can't get a style attribute that isn't in the inline style attribute (either coded into the HTML or previously set in your JavaScript).

Events

While events could be considered a part of modifying DOM elements, they deserve a section of their own. But first, what are events?

Events, put simply, are things that happen to elements in the DOM. Some examples of this might be as follows:

· The page loaded.

· An element was clicked.

· The mouse moved over an element.

When something like this happens, you may often want to do something. For example, run this script when the page loads. Or, execute this function when an element is clicked.

So, exactly what events are there? Well, you can find a comprehensive list on Wikipedia, but the ones you'll use most often are these:

· click

· mouseover

· mouseout

· keypress

· load

· submit

As you can see, there are two categories of events: things the user of your site does, and things that the browser does. In the above list, the user performs the click, mouseover, mouseout, and keypress events. The browser fires the load event when the page has finished loading, and the submit event when the user clicks a submit button (so it's almost a user-perfromed events).

Adding Event Handlers

An event is fired on a given DOM element. This means we can wire up an event handler (or event listener) to a given element. An event handler is a function that will be executed when a given event is fired. Let's look at an example:

Example 5.13

// <div id="some_button"> Click Me! </div>

var btn = document.getElementById("some_button");

function welcome_user(evt) {

alert("Hello there!");

}

btn.addEventListener("click", welcome_user, false);

(Note: you'll have to run this example in a browser other than Internet Explorer; we'll resolve this soon.)

What's going on here? Well, the first bit is stuff you're getting used to: getting an element and creating a function. Next, we're calling the element's method addEventListener. You can probably guess what an event listener does: it "listens" for an event and executes a function when that event occurs. The addEventListener takes three parameters. The first is the type of event we're listening for; in this case, it's a click event. The events in the sample list above are all valid events as well, and you can find the rest at that Wikipedia page.

The next parameter is the function to run when that event is fired. In this case, we're passing in the welcome_user function. We could also pass in an anonymous function, if we wanted to. The final parameter decides whether to use capturing or bubbling. This requires a short rabbit-trail on the difference between capturing and bubbling.

First off, note that Internet Explorer 8 and below support bubbling only (IE9 and up support both bubbling and capturing), so you'll probably use that 99% of the time. Now, check out this HTML:

<div>

<span> Click Here! </span>

</div>

Let's say I click the span. Because the span is inside the div, when that span gets clicked, the div is clicked as well. So both elements have a click event fire on them. If both have an event handler to harness that event, which one should be executed first? That's the background of capturing and bubbling. The DOM standard says that events should capture first, bubble second. The capture phase is when the event starts at the highest element and works down. In this case, the click handler of div will fire first. Once the event reaches the bottom—or the innermost element—it will then bubble. The bubble phase takes the event from the bottom and brings it back to the top, element by element. To remember which is which, remember that bubbles float up, just like events in the bubbling phase.

So, back to addEventListener. That last parameter is whether to handle the event in the capture phase or not. Because IE (before 9) doens't support capturing, it's usually false (which means bubbling).

But it's more than bubbling that IE doesn't support. Before version 9, IE had it's own event model (that model is still supported in IE 9, but it supports the standard model as well). Let's check that out.

It's actually pretty simple:

Example 5.14

// <div id="some_button"> Click Me! </div>

var btn = document.getElementById("some_button");

function welcome_user(evt) {

alert("Hello there!");

}

btn.attachEvent("onclick", welcome_user);

Instead of addEventListener, they user the method attachEvent. You'll notice that there's no third parameter (because it only supports bubbling). The other difference is that the event name must be prefixed with the word "on." Other than that, there's not much difference.

Removing Event Handlers

If you want to add event listeners, you'll eventually want to remove them. Here's how:

// assume the event we wired up above

btn.removeEventListener("click", welcome_user, false);

The method is removeEventListener. The important point is that the three parameters are all the same as the addEventListener parameters; that's how you target which event you're removing.

To remove events with the IE event model, try this:

// assumt the event we wired up above

btn.detachEvent("onclick", welcome_user);

It's pretty simple.

Writing Cross-Browser Event Handlers

The fact that there are two event models means that you need to wire up two event handlers for each event. The easiest way to do that is to write a function that wraps the event behaviour:

function addEvent(element, type, fn) {

if (element.addEventListener) {

element.addEventListener(type, fn, false);

} else {

element.attachEvent("on" + type, fn);

}

}

This is an extremely basic form of this function; there are a ton of ways to improve it. As an exercise, try creating a removeEvent function that removes events for both the standard model and the IE model.

Another Short Rabbit-Trail on Script Loading

Remember way back (when you were younger, and had less JavaScript in your blood), I recommended you put script tags at the end of the body, unless (and I quote) you "write your code to wait until the page has completed loading before it begins executing." I'm bringing this up for more than nostalgic reasons: events can help you do this.

What you want to do is listen for the load event on the window object. Here's how that's done (assume we have our addEvent function from above):

Example 5.15

addEvent(window, "load", function () {

alert("do something, for example.");

});

There is a lot more about events that we can't cover here. I didn't even mention the event object that gets passed to the function that is called; this object provides information about the event, such as which element the event from fired on. Check out Appendix A for further resources that will teach you about events.

The DOM, In Sum

As you can see, the DOM is occasionally confusing. You're constantly jumping over text nodes; IE has it's own mind for events and more. And we haven't even talked about AJAX or even mentioned the Browser Object Model (which isn't part of the DOM, but closely related). Does this mean you're doomed to awkwardly code JavaScript for the browser for the rest of your life?

Not really. The answer is to write a library: a collection of functions that do all the heavy, cross-browser work for you. But the great part about this is that there are already several incredible JavaScript libraries out there that can make your work simpler.

What are your options? Well, do any of these JavaScript buzzwords sound familiar?

· jQuery

· Mootools

· YUI

· Dojo

And that's only a few of them. These libraries give you a drop-dead simple way to work with the DOM. To whet your appetite, here's a snippet of raw JavaScript, followed by it's jQuery counterpart:

// Raw JavaScript

var leading_p = document.getElementById("content").firstChild;

while (leading_p.nodeType !== 1 || leading_p.nodeName !== 'P') {

leading_p = leading_p.nextSibling;

}

leading_p.style.backgroundColor = "red";

// jQuery

$("#content p:first").css("background-color", "red");

'Nuff said, eh? I think you can see the appeal of a library. All these libraries have great sites with tons of good documentation and tutorials; check them out to learn more!

A warning here: while using a JavaScript framework is much simpler than writing the raw JavaScript, you should really understand how to do things with the raw DOM functionality (which is much more than what we've covered here).

Wrapping Up

Well, That's all we're going to cover in this book. But that's definitely not all there is to learn about JavaScript. While you should now be comfortable writing functions, working with object, and (gently) manipulating the DOM, there's so much more to cover. JavaScript is growing as we speak, and there's a lot more to learn. Check out the Appendices for where to go from here.

Go forth and code JavaScript!