Article
Script Smarter: Quality JavaScript from Scratch
Changing the Type of an Element
Are your ordered lists feeling a bit unordered? Do your headings have paragraph envy? Using a little JavaScript knowledge, it's possible to change the type of an element entirely, while preserving the structure of its children.
Solution
There's no straightforward, simple way to change the type of an element. In order to achieve this feat you'll have to perform a bit of a juggling act.
Let's assume that we want to change this paragraph into a div:
Example 5.17. change_type_of_element.js (excerpt)
<p id="starLinks">
<a href="sirius.html">Sirius</a>
<a href="achanar.html">Achanar</a>
<a href="hadar.html">Hadar</a>
</p>
We need to create a new div, move each of the paragraph's children into it, then swap the new element for the old:
Example 5.18. change_type_of_element.js (excerpt)
var div = document.createElement("div");
var paragraph = document.getElementById("starLinks");
for (var i = 0; i < paragraph.childNodes.length; i++)
{
var clone = paragraph.childNodes[i].cloneNode(true);
div.appendChild(clone);
}
paragraph.parentNode.replaceChild(div, paragraph);
The only unfamiliar line here should be the point at which a clone is created for each of the paragraph's children. The cloneNode method produces an identical copy of the node from which it's called. By passing this method the argument true, we indicate that we want all of that element's children to be copied along with the element itself. Using cloneNode, we can mirror the original element's children under the new div, then remove the paragraph once we're finished copying.
While cloning nodes is useful in some circumstances, it turns out that there's a cleaner way to approach this specific problem. We can simply move the child nodes of the existing paragraph into the new div. DOM nodes can belong only to one parent element at a time, so adding the nodes to the div also removes them from the paragraph:
Example 5.19. change_type_of_element2.js (excerpt)
var div = document.createElement("div");
var paragraph = document.getElementById("starLinks");
while (paragraphNode.childNodes.length > 0){
div.appendChild(paragraphNode.firstChild);
}
paragraph.parentNode.replaceChild(div, paragraph);
Take Care Changing the Node Structure of the DOM
The elements in a collection are updated automatically whenever a change occurs in the DOM -- even if you copy that collection into a variable before the change occurs. So, if you remove from the DOM an element that was contained in a collection with which you had been working, the element reference will also be removed from the collection. This will change the length of the collection as well as the indexes of any elements that appear after the removed element.
When performing operations that affect the node structure of the DOM -- such as moving a node to a new parent element -- you have to be careful about iterative processes. The code above uses a while loop that only accesses the first child of the paragraph, because each time a child is relocated, the length of the childNodes collection will decrease by one, and all the elements in the collection will shift along. A for loop with a counter variable would not handle all the children correctly because it would assume that the contents of the collection would remain the same throughout the loop.
Discussion
There's no easy way to copy the attributes of an element to its replacement. (If you look at the DOM specification, it looks like there is. Unfortunately, Internet Explorer's support for the relevant properties and methods is just not up to the task.) If you want the new element to have the same id, class, href, and so on, you'll have to copy the values over manually:
Example 5.20. change_type_of_element.js (excerpt)
div.id = paragraph.getAttribute("id");
div.className = paragraph.className;
Removing an Element or Text Node
Once an element has outlived its usefulness, it's time to give it the chop. You can use JavaScript to remove any element cleanly from the DOM.
Solution
The removeChild method removes any child node from its parent, and returns a reference to the removed object.
Let's start off with this HTML:
Example 5.21. remove_element.html (excerpt)
<p>
<a id="sirius" href="sirius.html">Sirius</a>
</p>
We could use removeChild to remove the hyperlink from its parent paragraph like so:
Example 5.22. remove_element.js (excerpt)
var anchor = document.getElementById("sirius");
var parent = anchor.parentNode;
var removedChild = parent.removeChild(anchor);
The variable removedChild will be a reference to the a element, but that element will not be located anywhere in the DOM: it will simply be available in memory, much as if we had just created it using createElement. This allows us to relocate it to another position on the page, it we wish, or we can simply let the variable disappear at the end of the script, and the reference will be lost altogether -- effectively deleting it. Following the above code, the DOM will end up like this:
<p>
</p>
Of course, you don't need to assign the return value from removeChild to a variable. You can just execute it and forget about the element altogether:
var anchor = document.getElementById("sirius");
var parent = anchor.parentNode;
parent.removeChild(anchor);
Discussion
If the element that you're deleting has children that you wish to preserve (i.e., you just want to "unwrap" them by removing their parent), you must rescue those children to make sure they stay in the document when their parent is removed. You can achieve this using the already-mentioned insertBefore method, which, when used on elements that are already contained in the DOM, first removes them, then inserts them at the appropriate point.
The paragraph in the following HTML contains multiple children:
Example 5.23. remove_element2.html (excerpt)
<div id="starContainer">
<p id="starLinks">
<a href="aldebaran.html">Aldebaran</a>
<a href="castor.html">Castor</a>
<a href="pollux.html">Pollux</a>
</p>
</div>
We can loop through the paragraph's childNodes collection, and relocate each of its children individually before removing the element itself:
Example 5.24. remove_element2.js (excerpt)
var parent = document.getElementById("starLinks");
var container = document.getElementById("starContainer");
while (parent.childNodes.length > 0)
{
container.insertBefore(parent.childNodes[0], parent);
}
container.removeChild(parent);
The page's DOM will now look like this:
<div id="starContainer">
<a href="aldebaran.htm">Aldebaran</a>
<a href="castor.htm">Castor</a>
<a href="pollux.htm">Pollux</a>
</div>
Reading and Writing the Attributes of an Element
The most frequently used parts of an HTML element are its attributes?its id, class, href, title, or any of a hundred other pieces of information that can be included in an HTML tag. JavaScript is able not only to read these values, but write them as well.
Solution
Two methods exist for reading and writing an element's attributes. getAttribute allows you to read the value of an attribute, while setAttribute allows you to write it.
Consider this HTML:
Example 5.25. read_write_attributes.html (excerpt)
<a id="antares" href="antares.html" title="A far away place">
Antares</a>
We would be able to read the attributes of the element like so:
Example 5.26. read_write_attributes.js (excerpt)
var anchor = document.getElementById("antares");
var anchorId = anchor.getAttribute("id");
var anchorTitle = anchor.getAttribute("title");
The value of the variable anchorId will be "antares", and the value of the variable anchorTitle will be "A far away place".
To change the attributes of the hyperlink, we use setAttribute, passing it the name of the attribute to be changed, and the value we want to change it to:
Example 5.27. read_write_attributes2.js (excerpt)
var anchor = document.getElementById("antares");
anchor.setAttribute("title", "Not that far away");
var newTitle = anchor.getAttribute("title");
The value of the variable newTitle will now be "Not that far away".
Discussion
In its journey from the free-roaming Netscape wilderness to the more tightly defined, standards-based terrain of the modern age, the DOM standard has picked up a fair amount of extra syntax for dealing with HTML. One of the most pervasive of these extras is the mapping between DOM properties and HTML attributes.
When a document is parsed into its DOM form, special attribute nodes are created for an element's attributes. These nodes are not accessible as "children" of that element: they are accessible only via the two methods mentioned above. However, as a throwback to the original DOM implementations (called DOM 0, where the zero suggests these features came prior to standards), current DOM specs contain additional functionality that's specific to HTML. In particular, attributes are accessible directly as properties of an element. So, the href attribute of a hyperlink is accessible through link.getAttribute("href") as well as through link.href.
This shortcut syntax is not only cleaner and more readable: in some situations it is also necessary. Internet Explorer 6 and versions below will not propagate changes made via setAttribute to the visual display of an element. So any changes that are made to the class, id, or style of an element using setAttribute will not affect the way it's displayed. In order for those changes to take effect, they must be made via the element node's attribute-specific properties.
To further confuse matters, the values that are returned when an attribute-specific property is read vary between browsers, the most notable variations occurring in Konqueror. If an attribute doesn't exist, Konqueror will return null as the value of an attribute-specific property, while all other browsers will return an empty string. In a more specific case, some browsers will return link.getAttribute("href") as an absolute URL (e.g., "http://www.example.com/antares.html"), while others return the actual attribute value (e.g., "antares.html"). In this case, it's safer to use the dot property, as it consistently returns the absolute URL across browsers.
So, what's the general solution to these problems?
The basic rule is this: if you are certain that an attribute has been assigned a value, it's safe to use the dot property method to access it. If you're unsure whether or not an attribute has been set, you should first use one of the DOM methods to ensure that it has a value, then use the dot property to obtain its value.
For reading an unverified attribute, use the following:
var anchor = document.getElementById("sirius");
if (anchor.getAttribute("title") &&
anchor.title == "Not the satellite radio")
{
...
}
This makes sure that the attribute exists, and is not null, before fetching its value.
For writing to an unverified attribute, use the following code:
var anchor = document.getElementById("sirius");
anchor.setAttribute("title", "");
anchor.title = "Yes, the satellite radio";
This code makes sure that the attribute is created correctly first, and is then set in such a way that Internet Explorer will not have problems if the attribute affects the visual display of the element.
This rule has a few exceptions for attributes whose existence you can guarantee. The most notable of these "must-have" attributes are style and class, which will always be valid for any given element; thus, you can immediately reference them as dot properties (element.style and element.className respectively).
class is one of two attributes that get a little tricky, because class is a reserved word in JavaScript. As a property, it is written element.className, but using getAttribute/setAttribute, we write element.getAttribute("class"), except in Internet Explorer, where we still use element.getAttribute("className").
The other attribute that we have to watch out for is the for attribute of a label. It follows the same rules as class, but its property form is htmlFor. Using getAttribute/setAttribute, we write element.getAttribute("for"), but in Internet Explorer it's element.getAttribute("htmlFor").
Getting all Elements with a Particular Attribute Value
The ability to find all the elements that have a particular attribute can be pretty handy when you need to modify all elements that have the same class or title, for example.
Solution
In order to find elements with a particular attribute value, we need to check every element on the page for that attribute. This is a very calculation-intensive operation, so it shouldn't be undertaken lightly. If you wanted to find all input elements with type="checkbox", you're better off limiting your search to input elements first:
var inputs = document.getElementsByTagName("input");
for (var i = 0; i < inputs.length; i++)
{
if (inputs.getAttribute("type") == "checkbox")
{
...
}
}
This will require less calculation than iterating through every element on the page and checking its type. However, the function presented in this solution -- getElementsByAttribute -- is ideal when you need to find a number of elements of different types that have the same attribute value.
The easiest way to check every element on a page is to loop through the collection returned by getElementsByTagName("*"). The only problem with this method is that Internet Explorer 5.0 and 5.5 do not support the asterisk wildcard for tag selection. Luckily, these browsers support the document.all property, which is an array containing all the elements on the page. getElementsByAttribute handles this issue with a simple code branch, then proceeds to check the elements for a given attribute value, adding matches to an array to be returned:
Example 5.28. get_elements_by_attribute.js (excerpt)
function getElementsByAttribute(attribute, attributeValue)
{
var elementArray = new Array();
var matchedArray = new Array();
if (document.all)
{
elementArray = document.all;
}
else
{
elementArray = document.getElementsByTagName("*");
}
for (var i = 0; i < elementArray.length; i++)
{
if (attribute == "class")
{
var pattern = new RegExp("(^| )" +
attributeValue + "( |$)");
if (pattern.test(elementArray[i].className))
{
matchedArray[matchedArray.length] = elementArray[i];
}
}
else if (attribute == "for")
{
if (elementArray[i].getAttribute("htmlFor") ||
elementArray[i].getAttribute("for"))
{
if (elementArray[i].htmlFor == attributeValue)
{
matchedArray[matchedArray.length] = elementArray[i];
}
}
}
else if (elementArray[i].getAttribute(attribute) ==
attributeValue)
{
matchedArray[matchedArray.length] = elementArray[i];
}
}
return matchedArray;
}
A lot of the code in getElementsByAttribute deals with the browser differences in attribute handling that were mentioned earlier in this chapter, in the section called "Reading and Writing the Attributes of an Element". The necessary techniques are used if the required attribute is class or for. As an added bonus when checking for a match on the class attribute, if an element has been assigned multiple classes, the function automatically checks each of these to see whether it matches the required value.
Adding and Removing Multiple Classes to/from an Element
Combining multiple classes is a very useful CSS technique. It provides a very primitive means of inheritance by allowing a number of different styles to be combined on the one element, allowing you to mix and match different effects throughout a site. They're particularly useful in situations like highlighting elements: a class can be added that highlights an element without disturbing any of the other visual properties that may have been applied to the element by other classes. However, if you are assigning classes in JavaScript you have to be careful that you don't inadvertently overwrite previously assigned classes.
Solution
The class for any element is accessible via its className property. This property allows you both to read and write the classes that are currently applied to that element. Because it's just one string, the most difficult part of working with className is that you need to deal with the syntax it uses to represent multiple classes.
The class names in an element's className property are separated by spaces. The first class name is not preceded by anything, and the last class name is not followed by anything. This makes it easy to add a class to the class list naively: just concatenate a space and the new class name to the end of className. However, you'll want to avoid adding a class name that already exists in the list, as this will make removing the class harder. You'll also want to avoid using a space at the beginning of the className value, because this will cause errors in Opera 7:
Example 5.29. add_remove_classes.js (excerpt)
function addClass(target, classValue)
{
var pattern = new RegExp("(^| )" + classValue + "( |$)");
if (!pattern.test(target.className))
{
if (target.className == "")
{
target.className = classValue;
}
else
{
target.className += " " + classValue;
}
}
return true;
}
First, addClass creates a regular expression pattern containing the class to be added. It then uses this pattern to test the current className value. If the class name doesn't already exist, we check for an empty className value (in which case the class name is assigned to the property verbatim), or we append to the existing value a space and the new class name.
Separating Classes
Some regular expression examples for finding classes use the word boundary special character (\b) to separate classes. However, this will not work with all valid class names, such as those containing hyphens.
The process for removing a class uses a regular expression pattern that's identical to the one we use to add a class, but we don't need to perform as many checks:
Example 5.30. add_remove_classes.js (excerpt)
function removeClass(target, classValue)
{
var removedClass = target.className;
var pattern = new RegExp("(^| )" + classValue + "( |$)");
removedClass = removedClass.replace(pattern, "$1");
removedClass = removedClass.replace(/ $/, "");
target.className = removedClass;
return true;
}
After removeClass has executed the replacement regular expression on a copy of the className property's value, it cleans up the resulting value by removing any trailing space (which is created when we remove the last class in a multiple class className), then assigns it back to the target's className.
Summary
This chapter introduced the basic but powerful tools that you'll need in order to manipulate the Document Object Model. It's important that you understand the DOM -- the skeleton beneath everything you see in a browser -- as you manipulate any web page. Knowing how to create, edit, and delete parts of the DOM is crucial to understanding the remainder of this book. Once you've mastered these techniques, you'll be well on your way to becoming a proficient JavaScript programmer.