Many web browsers are available today, but few have as complete support for XML and its related technologies as Internet Explorer (IE) and Mozilla Firefox. Although other browsers are starting to catch up, with Safari and Opera now able to open XML documents, the functionality is either incomplete or incorrect. Because of these issues, this chapter focuses primarily on the implementations in IE and Firefox.
When Microsoft added XML support to IE 5.0, they did so by incorporating the MSXML ActiveX library, a component originally written to parse Active Channels in IE 4.0. This original version wasn't intended for public use, but developers became aware of the component and began using it. Microsoft responded with a fully upgraded version of MSXML, which was included in IE 4.01. MSXML was primarily an IE-only component until 2001 when Microsoft released MSXML 3.0, a separate distribution available through the company's web site. Later that year, version 4.0 was released and MSXML was renamed Microsoft XML Core Services Component. Since its inception, MSXML has gone from a basic, non-validating XML parser to a full-featured component that can validate XML documents, perform XSL transformations, support namespace usage, the Simple API for XML (SAX), and the W3C XPath and XML Schema standards, all while improving performance with each new version.
To create an ActiveX object in JavaScript, Microsoft implemented a new class called ActiveXObject that can be used to instantiate a number of ActiveX objects. Its constructor takes one argument, a string containing the version of the ActiveX object to create; in this case, it is the version of the XML document. The first XML DOM ActiveX object was called Microsoft.XmlDom, whose creation looks like this:
var oXmlDom = new ActiveXObject("Microsoft.XmlDom");
The newly created XML DOM object behaves like any other DOM object, enabling you to traverse the DOM tree and manipulate DOM nodes.
At the time of this writing, there are five different versions of the MSXML DOM document, and their version strings are as follows:
Microsoft.XmlDom
MSXML2.DOMDocument
MSXML2.DOMDocument.3.0
MSXML2.DOMDocument.4.0
MSXML2.DOMDocument.5.0
Important |
MSXML is an ActiveX implementation; therefore, it is available only on Windows platforms. IE 5 on the Mac has no XML DOM support. |
Because there are five different versions, and you presumably always want to use the latest, it is helpful to use a function to determine which version to use. Doing so ensures you the most up-to-date support and the highest performance. The following function, createDocument(), enables you to create the correct MSXML DOM document:
function createDocument() { var aVersions = [ "MSXML2.DOMDocument.5.0", "MSXML2.DOMDocument.4.0"," MSXML2.DOMDocument.3.0", "MSXML2.DOMDocument"," Microsoft.XmlDom" ]; for (var i = 0; i < aVersions.length; i++) { try { var oXmlDom = new ActiveXObject(aVersions[i]); return oXmlDom; } catch (oError) { //Do nothing } } throw new Error("MSXML is not installed."); }
This function iterates through the aVersions array, which contains the version strings of MSXML DOM documents. It starts with the latest version, MSXML2.DOMDocument.5.0, and attempts to create the DOM document. If the object creation is successful, it is returned and createDocument() exits; if it fails, an error is thrown and then caught by the try…catch block, so the loop continues and the next version is tried. If the creation of an MSXML DOM document fails, a thrown error states that MSXML is not installed. This function is not a class, so its usage looks like any other function that returns a value:
var oXmlDom = createDocument();
Using createDocument() will ensure that the most up-to-date DOM document is used. Now that you have an XML document at your disposal, it is time to load some XML data.
MSXML supports two methods to load XML: load() and loadXML(). The load() method loads an XML file at a specific location on the Web. As with XMLHttp, the load() method enables you to load the data in two modes: asynchronously or synchronously. By default, the load() method is asynchronous; to use synchronous mode, the MSXML object's async property must be set to false, as follows:
oXmlDom.async = false;
When in asynchronous mode, the MSXML object exposes the readyState property, which has the same five states as the XMLHttp readyState property.
Additionally, the DOM document supports the onreadystatechange event handler, enabling you to monitor the readyState property. Because asynchronous mode is the default, setting the async property to true is optional:
oXmlDom.async = true; oXmlDom.onreadystatechange = function ( ) { if (oXmlDom.readyState == 4) { //Do something when the document is fully loaded. } }; oXmlDom.load("myxml.xml");
In this example, the fictitious XML document named myxml.xml is loaded into the XML DOM. When the readyState reaches the value of 4, the document is fully loaded and the code inside the if block will execute.
The second way to load XML data, loadXML(), differs from the load() method in that the former loads XML from a string. This string must contain well-formed XML, as in the following example:
var sXml = "<root><person><name>Jeremy McPeak</name></person></root>";
oXmlDom.loadXML(sXml);
Here, the XML data contained in the variable sXml is loaded into the oXmlDom document. There is no reason to check the readyState property or to set the async property when using loadXML() because it doesn't involve a server request.
Navigating an XML DOM is much like navigating an HTML DOM: it is a hierarchical node structure. At the top of the tree is the documentElement property, which contains the root element of the document. From there, you can access any element or attribute in the document using the properties listed in Table 4-1.
Property |
Description |
---|---|
attributes |
Contains an array of attributes for this node. |
childNodes |
Contains an array of child nodes. |
firstChild |
Refers to the first direct child of the node. |
lastChild |
Refers to the last child of the node. |
nextSibling |
Returns the node immediately following the current node. |
nodeName |
Returns the qualified name of the node. |
nodeType |
Specifies the XML DOM node type of the node. |
nodeValue |
Contains the text associated with the node. |
ownerDocument |
Returns the root element of the document. |
parentNode |
Refers to the parent node of the current node. |
previousSibling |
Returns the node immediately before the current node. |
text |
Returns the content of the node or the concatenated text of the current node and its descendants. It is an IE-only property. |
xml |
Returns the XML of the current node and its children as a string. It is an IE-only property. |
Traversing and retrieving data from the DOM is a straight-forward process. Consider the following XML document:
<?xml version="1.0" encoding=" utf-8"?> <books> <book isbn="0471777781">Professional Ajax</book> <book isbn="0764579088">Professional JavaScript for Web Developers</book> <book isbn="0764557599">Professional C#</book> <book isbn="1861002025">Professional Visual Basic 6 Databases</book> </books>
This simple XML document includes a root element, <books/>, with four child <book/> elements. Using this document as a reference, you can explore the DOM. The DOM tree is based on the relationships nodes have with other nodes. One node may contain other nodes, or child nodes. Another node may share the same parent as other nodes, which are siblings.
Perhaps you want to retrieve the first <book/> element in the document. This is easily achieved with the firstChild property:
var oRoot = oXmlDom.documentElement; var oFirstBook = oRoot.firstChild;
The assignment of the documentElement to oRoot will save space and typing, although it is not necessary. Using the firstChild property, the first <book/> element is referenced and assigned to the variable oFirstBook because it is the first child element of the root element <books/>.
You can also use the childNodes collection to achieve the same results:
var oFirstBook2 = oRoot.childNodes[0];
Selecting the first item in the childNodes collection returns the first child of the node. Because childNodes is a NodeList in JavaScript, you can retrieve the amount of children a node has by using the length property, as follows:
var iChildren = oRoot.childNodes.length;
In this example, the integer 4 is assigned to iChildren because there are four child nodes of the document element.
As already discussed, nodes can have children, which means they can have parents, too. The parentNode property selects the parent of the current node:
var oParent = oFirstBook.parentNode;
You saw oFirstBook earlier in this section, but as a quick refresher, it is the first <book/> element in the document. The parentNode property of this node refers to the <books/> element, the documentElement of the DOM.
What if your current node is a book element and you want to select another book element? The <book/> elements are siblings to each other because they share the same direct parent. Two properties, nextSibling and previousSibling, exist to select adjacent nodes to the current node. The nextSibling property references the next occurring sibling, whereas the previousSibling property selects the preceding sibling:
var oSecondBook = oFirstBook.nextSibling; oFirstBook2 = oSecondBook.previousSibling;
In this code, the second <book/> element is referenced and assigned to oSecondBook. The oFirstBook2 variable is then reassigned to reference the oSecondBook sibling immediately before it, resulting in oFirstBook2 to contain the same value as it did before. If a node has no siblings after it, then nextSibling is null. The same holds true for previousSibling; if there is no sibling immediately before the current node, previousSibling is null.
Now that you know how to traverse through the document hierarchy, you should know how to retrieve data from nodes in the tree. For example, to retrieve the text contained within the third <book/> element, you could use the text property, as follows:
var sText = oRoot.childNodes[2].text;
The text property retrieves all the text nodes contained within this node and is a Microsoft proprietary property, but it is extremely helpful. Without the text property, you would have to access the text node as follows:
var sText = oRoot.childNodes[2].firstChild.nodeValue;
This code achieves the same results as using the text property. Like the previous example, the third <book/> element is referenced using the childNodes collection; the text node of the <book/> element is then referenced with the use of firstChild because a text node is still a node in the DOM. The text is then retrieved by using the nodeValue property that retrieves the value of the current node.
The results from these two examples are identical; however, the text property behaves in a different way than using the nodeValue property on a text node. The text property retrieves the value of all text nodes contained within the element and its children, whereas the nodeValue property gets only the value of the current node. It is a helpful property, but it has the potential to return more text than desired. For example, consider this modified XML document:
<?xml version="1.0" encoding=" utf-8"?> <books> <book isbn="0471777781"> <title>Professional Ajax</title> <author>Nicholas C. Zakas, Jeremy McPeak, Joe Fawcett</author> </book> <book isbn="0764579088">Professional JavaScript for Web Developers</book> <book isbn="0764557599">Professional C#</book> <book isbn="1861002025">Professional Visual Basic 6 Databases</book> </books>
This new XML document adds two new children to the first <book/> element: the <title/> element, which contains the title of the book, and the <author/> element, which holds the author data. Once again, use the text property:
alert(oFirstChild.text);
There is nothing new in this code, as you have already seen it. However, look at the results, as shown in Figure 4-1.
Notice that the text nodes from the <title/> and <author/> elements are retrieved and concatenated. This is how text differs from nodeValue. The nodeValue property retrieves only the value of the current node, whereas the text property retrieves all text nodes contained in the current node and its children.
MSXML also provides a number of methods to retrieve specific nodes or values; the two most often used are getAttribute() and getElementsByTagName().
The getAttribute() method takes a string argument containing the name of the attribute and returns that attribute's value. If the attribute does not exist, the value returned is null. Using the same XML document introduced earlier in this section, consider the following code:
This code retrieves the value of the isbn attribute of the first <book/> element and assigns it to the sAttribute variable. This value is then used in the alert() method to display the value.
The getElementsByTagName() method returns a NodeList of child elements with the name specified by its argument. This method searches for elements within the given node only, so the returned NodeList does not include any external elements. For example:
var cBooks = oRoot.getElementsByTagName("book"); alert(cBooks.length);
This code retrieves all <book/> elements within the document and returns the NodeList to cBooks. With the sample XML document, an alert box will display that four <book/> elements were found. To retrieve all child elements, pass "*" as the parameter to getElementsByTagName(), as follows:
var cElements = oRoot.getElementsByTagName("*");
Because the example XML document contains only <book/> elements, the resulting NodeList of this code sample matches that of the previous example.
Retrieving XML data is as simple as using a property, the xml property. This property serializes the XML data of the current node. Serialization is the process of converting objects into an easily storable or transmittable format. The xml property converts XML into a string representation, complete with tag names, attributes, and text:
var sXml = oRoot.xml; alert(sXml);
This code serializes the XML data starting with the document element, which is then passed to the alert() method. A portion of the serialized XML looks like this:
<books><book isbn="0471777781">Professional Ajax</book></books>
You can load serialized data into another XML DOM object, send to a server application, or pass to another page. The serialized XML data returned by the xml property depends on the current node. Using the xml property at the documentElement node returns the XML data of the entire document, whereas using it on a <book/> element returns only the XML data contained in that <book/> element.
The xml property is read-only. If you want to add elements to the document, you will have to use DOM methods to do so.
Until this point, you have learned how to traverse the DOM, extract information from it, and convert XML into string format. You also have the ability to add to, delete from, and replace nodes in the DOM.
You can create a variety of nodes using DOM methods, the first of which is an element with the createElement() method. This method takes one argument, a string containing the tag name of the element to create, and returns an XMLDOMElement reference:
var oNewBook = oXmlDom.createElement("book"); oXmlDom.documentElement.appendChild(oNewBook);
This code creates a new <book/> element and appends it to documentElementl with the appendChild() method. The appendChild() method appends the new element, specified by its argument, as the last child node. This code, however, appends an empty <book/> element to the document, so the element needs some text:
var oNewBook = oXmlDom.createElement("book"); var oNewBookText = oXmlDom.createTextNode("Professional .NET 2.0 Generics"); oNewBook.appendChild(oNewBookText); oXmlDom.documentElement.appendChild(oNewBook);
This code creates a text node with the createTextNode() method and appends it to the newly created <book/> element with appendChild(). The createTextNode() method takes a string argument specifying the value applied to the text node.
At this point, you have programmatically created a new <book/> element, provided it a text node, and appended it to the document. One last piece of information is required to get this new element on par with its other siblings, the isbn attribute. Creating an attribute is as simple as using the setAttribute() method, which is available on every element node:
var oNewBook = oXmlDom.createElement("book");
var oNewBookText = oXmlDom.createTextNode("Professional .NET 2.0 Generics");
oNewBook.appendChild(oNewBookText);
oNewBook.setAttribute("isbn","0764559885");
oXmlDom.documentElement.appendChild(oNewBook);
The new line of code in this example creates an isbn attribute and assigns it the value of 0764559885. The setAttribute() method takes two string arguments: the first is the name of the attribute, and the second is the value to assign to the attribute. IE also provides other methods to add attributes to an element; however, they hold no real advantage over setAttribute() and require much more coding.
If you can add nodes to a document, it seems only natural to be able to remove them as well. The removeChild() method does just that. This method has one argument: the node to remove. Suppose, for example, that you want to remove the first <book/> element from the document. You could use the following code:
var oRemovedChild = oRoot.removeChild(oRoot.firstChild);
The removeChild() method returns the removed child node, so oRemoveChild now references the removed <book/> element. You now have a reference to the old node, so you can place it anywhere else into the document.
Perhaps you want to replace the third <book/> element with oRemovedChild. The replaceChild() does that and returns the replaced node:
var oReplacedChild = oRoot.replaceChild(oRemovedChild, oRoot.childNodes[2]);
The replaceChild() method accepts two arguments: the node to add and the node to replace. In this code, the node referenced by oRemovedChild replaces the third <book/> element, and the replaced node is now referenced by oReplacedChild.
Because oReplacedChild references the replaced node, you can easily insert it into the document. You could use appendChild() to add the node to the end of the child list, or you can use the insertBefore() method to insert the node before another sibling:
oRoot.insertBefore(oReplacedChild, oRoot.lastChild);
This code inserts the previously replaced node before the last <book/> element. You'll notice the use of the lastChild property, which retrieves the last child node, much like firstChild selects the first child node. The insertBefore() method takes two arguments: the node to insert and the node to insert before. This method also returns the value of the inserted node, but it is not necessary for this example.
As you have seen, the DOM is a powerful interface from which you can retrieve, remove, and add data.
When XML data is loaded, errors can be thrown for a variety of reasons. For example, the external XML file may not be found or the XML may not be well formed. To handle these occasions, MSXML provides the parseError object, which contains the error information. This object is a property of every XML DOM document MSXML creates.
To check for errors, the parseError object exposes the errorCode property, which can be compared to the integer 0; if errorCode does not equal 0, an error has occurred. The following example is designed specifically to cause an error:
var sXml = "<root><person><name>Jeremy McPeak</name></root>"; var oXmlDom = createDocument(); oXmlDom.loadXML(sXml); if (oXmlDom.parseError.errorCode != 0) { alert("An Error Occurred: "+ oXmlDom.parseError.reason); } else { //Code to do for successful load. }
In the highlighted line, notice that the <person/> element is not closed. Because the XML being loaded is not well formed, an error occurs. The errorCode is then compared to 0; if they do not match (and they don't in this example), an alert will display what caused the error. To do this, it uses the reason property of the parseError object, which describes the reason for the error.
The parseError object provides the following properties to enable you to better understand an error:
errorCode: The error code as a long integer
filePos: A long integer specifying the position in the file where the error occurred
line: The line number that contains the error as a long integer
linePos: The character position in the line where the error occurred (long integer)
reason: A string specifying why the error happened
srcText: The text of the line where the error happened
url: The URL of the XML document as a string
Although all of these properties provide information about each error, it is up to you which ones make the most sense given your needs.
Important |
The errorCode property can be positive or negative; only when errorCode is 0 can you be sure that no error occurred. |
When it came time to implement the XML DOM in Mozilla Firefox, the developers took a more standards-centric approach in making it a part of the JavaScript implementation. In doing so, Mozilla ensured XML DOM support on all platforms in all Gecko-based browsers.
To create an XML DOM in Firefox, the createDocument() method of the document.implementation object is called. This method takes three arguments: the first is a string containing the namespace URI for the document to use, the second is a string containing the qualified name of the document's root element, and the third is the type of document (also called doctype) to create. To create an empty DOM document, you can do this:
var oXmlDom = document.implementation.createDocument("", "", null);
By passing in an empty string for the first two arguments, and null for the last, you ensure a completely empty document. In fact, there is currently no JavaScript support for doctypes in Firefox, so the third argument must always be null. To create an XML DOM with a document element, specify the tag name in the second argument:
var oXmlDom = document.implementation.createDocument("", "books", null);
This code creates an XML DOM whose documentElement is <books/>. You can take it a step further and specify a namespace in the creation of the DOM by specifying the namespace URI in the first argument:
var oXmlDom = document.implementation.createDocument("http://www.site1.com","books",null);
When a namespace is specified in the createDocument() method, Firefox automatically assigns the prefix a0 to represent the namespace URI:
<a0:books xmlns:a0="http://www.site1.com" />
From here, you can populate the XML document programmatically; generally, however, you will want to load preexisting XML documents into a blank XML DOM object.
Loading XML into an XML DOM is similar to Microsoft's approach with one glaring difference: Firefox supports only the load() method. Therefore, you can use the same code to load external XML data in both browsers:
oXmlDom.load("books.xml");
Also like Microsoft, Firefox implemented the async property, and its behavior matches that of Microsoft's: setting async to false forces the document to be loaded in synchronous mode; otherwise, the document is loaded asynchronously.
Another difference between the Firefox and Microsoft XML DOM implementations is that Firefox does not support the readyState property or the onreadystatechange event handler. Instead, it supports the load event and the onload event handler. The load event fires after the document is completely loaded:
oXmlDom.load("books.xml"); oXmlDom.onload = function () { //Do something when the document is fully loaded. };
As mentioned previously, the loadXML() method does not exist in the Firefox implementation; however, it is possible to emulate the loadXML() behavior through the Firefox DOMParser class. This class has a method called parseFromString(), which loads a string and parses it into a document:
var sXml = "<root><person><name>Jeremy McPeak</name></person></root>"; var oParser = new DOMParser(); var oXmlDom = oParser.parseFromString(sXml," text/xml");
In this code, a string of XML is created to pass to the DOMParser parseFromString() method. The two arguments for parseFromString() are the XML string and the content type of the data (typically set to "text/xml"). The parseFromString() method returns an XML DOM object, so you can treat oXmlDom in this code as one.
Despite all their differences, IE and Firefox do share many properties and methods used to retrieve XML data contained in the document. As in IE, you can retrieve the root element of the document by using the documentElement property, as follows:
var oRoot = oXmlDom.documentElement;
Firefox also supports the W3C standards properties of attributes, childNodes, firstChild, lastChild, nextSibling, nodeName, nodeType, nodeValue, ownerDocument, parentNode, and previousSibling. Unfortunately, Firefox does not support the Microsoft-proprietary text and xml properties, but thanks to its flexibility, you can emulate their behavior.
As a quick recap, the text property returns the content of the node or the concatenated text of the current node and its descendants. Therefore, not only does it return the text of the existing node, but also the text of all child nodes; this is easy enough to emulate. A simple function that takes a node as an argument can provide the same result:
function getText(oNode) { var sText = ""; for (var i = 0; i < oNode.childNodes.length; i++) { if (oNode.childNodes[i].hasChildNodes()) { sText += getText(oNode.childNodes[i]); } else { sText += oNode.childNodes[i].nodeValue; } } return sText; }
In getText(), sText is used to store every piece of text that is retrieved. As the for loop iterates through the oNode children, each child is checked to see if it contains children. If it does, the childNode is passed through getText() and goes through the same process. If no children exist, then the nodeValue of the current node is added to the string (for text nodes, this is just the text string). After all children have been processed, the function returns sText.
The IE xml property serializes all XML contained in the current node. Firefox accomplishes the same result by providing the XMLSerializer object. This object has a single method that is accessible using JavaScript called serializeToString(). Using this method, XML data is serialized:
function serializeXml(oNode) { var oSerializer = new XMLSerializer(); return oSerializer.serializeToString(oNode); }
The serializeXml() function takes an XML node as an argument. An XMLSerializer object is created, and the node is passed to the serializeToString() method. The result of this method, a string representation of the XML data, is returned to the caller.
Note |
Firefox shares the same DOM methods for manipulating nodes as IE. Refer to the "Manipulating the DOM in IE" section for a refresher. |
Firefox, unsurprisingly, handles errors differently from IE. When IE runs into an error, it populates the parseError object; when Firefox runs into an error, it loads an XML document containing the error into the XML DOM document. Consider the following example:
var sXml = "<root><person><name>Jeremy McPeak</name></root>";
var oParser = new DOMParser();
var oXmlDom = oParser.parseFromString(sXml," text/xml");
if (oXmlDom.documentElement.tagName != "parsererror") {
//No error occurred. Do something here.
} else {
alert("An Error Occurred");
}
In the highlighted line, you'll see what will cause the error: a malformed XML string (because the person/> element is not closed). When the malformed XML is loaded, the XML DOM object loads an error document with a documentElement of <parsererror/>. You can easily determine if an error occurred by checking the documentElement tagName property; if it's not parsererror, you can be assured that an error did not occur.
The error document created in this example looks like this:
<parsererror xmlns="http://www.mozilla.org/newlayout/xml/parsererror.xml">XML Parsing Error: mismatched tag. Expected: </person>. Location: http://yoda/fooreader/test.htm Line Number 1, Column 43:<sourcetext><root><person><name>Jeremy McPeak</name></root> ------------------------------------------^</sourcetext></parsererror>
All of the information about the error is available as text in the error document. If you want to use this information programmatically, you have to parse it first. The easiest way to do so is to use a rather lengthy regular expression:
var reError = />([\s\S]*?)Location:([\s\S]*?)Line Number (\d+), Column (\d+):<sourcetext>([\s\S]*?)(?:\-*\^)/;
This regular expression divides the error document into five sections: the error message, the file name where the error happened, the line number, the position in the line where the error occurred, and the source code that caused the error. Using the test() method of the regular expression object will enable you to use these pieces of data:
if (oXmlDom.firstChild.tagName != "parsererror") { //No error occurred. Do something here. } else { var oXmlSerializer = new XMLSerializer(); var sXmlError = oXmlSerializer.serializeToString(oXmlDom); var reError = />([\s\S]*?)Location:([\s\S]*?)Line Number (\d+), Column (\d+):<sourcetext>([\s\S]*?)(?:\-*\^)/; reError.test(sXmlError);
The first chunk of data captured by the regular expression is error message, the second is the file name, the third is the line number, the fourth is the position in the line, and the fifth is source code. You can now use this parsed information to create your own error message:
var str = "An error occurred!!\n" + "Description: "+ RegExp.$1 + "\n" + "File: "+ RegExp.$2 + "\n" + "Line: "+ RegExp.$3 + "\n" + "Line Position: "+ RegExp.$4 + "\n" + "Source Code: "+ RegExp.$5; alert(str);
If an error occurs, an alert box will display the relevant error information in an easy-to-read fashion.
In an Ajax application, and most JavaScript code, you always need to consider cross-browser differences. When using an XML-based solution in IE and Firefox, you have two options: create your own functions that use the correct code based on the browser, or use a ready-made library. Most of the time it's easiest to use a pre-existing library, such as the zXml library introduced in Chapter 2. Along with XMLHttp support, zXml also has common interfaces for XML operations.
For example, to create an XML DOM document, you can use zXmlDom.createDocument():
var oXmlDom = zXmlDom.createDocument();
This single line of code can be used instead of doing separate browser-dependent code each time a DOM document is needed. Additionally, zXml adds a host of IE functionality to the standard Firefox DOM document.
One of the major things zXml does for convenience is to add support for the readyState property and the onreadystatechange event handler. Instead of needing to use the separate onload event handler in Firefox, you can write one set of code without browser detection, such as:
oXmlDom.onreadystatechange = function () { if (oXmlDom.readyState == 4) { //Do something when the document is fully loaded. } };
The zXml library also adds the xml and text attributes to all nodes in Firefox. Instead of using an XMLSerializer or a standalone function to get these values, you can use them the same way as in IE:
var oRoot = oXmlDom.documentElement; var sFirstChildText = oRoot.firstChild.text; var sXml = oRoot.xml;
zXml also provides a loadXML() method for the Firefox DOM document, eliminating the need to use a DOMParser object.
var oXmlDom2 = zXmlDom.createDocument(); oXmlDom2.loadXML(sXml);
Last, the zXml library adds a parseError object to the Firefox implementation. This object emulates fairly closely the corresponding object in IE. The one major difference is the errorCode property, which is simply set to a non-zero number when an error occurs. Therefore, you shouldn't use this property to look for a specific error, only to see if an error has occurred. Other than that, you can use the other properties as you would in IE:
if (oXmlDom.parseError.errorCode != 0) { var str = "An error occurred!!\n" + "Description: "+ oXmlDom.parseError.reason + "\n" + "File: "+ oXmlDom.parseError.url + "\n" + "Line: "+ oXmlDom.parseError.line + "\n" + "Line Position: "+ oXmlDom.parseError.linePos + "\n" + "Source Code: "+ oXmlDom.parseError.srcText; alert(str); } else { //Code to do for successful load. }
You certainly aren't required to use a cross-browser XML library for your solutions, but it can definitely help. The following section develops an example using the zXml library.
XML is a semantic, describing language. Generally, the elements contained in any given XML document describe the data of that document, thus making it a decent data store for static information, or information that doesn't change often.
Imagine you run an online bookstore and have a list of Best Picks whose information is stored in an XML document, books.xml. You need to display this information to the user, but you want to do so without using a server component, so you turn to a JavaScript solution. You are going to write a JavaScript solution using the zXml library that will load the XML file, parse through it, and display the information in a web page using DOM methods.
The books.xml file contains the following XML data:
<?xml version="1.0" encoding=" utf-8"?> <bookList> <book isbn="0471777781"> <title>Professional Ajax</title> <author>Nicholas C. Zakas, Jeremy McPeak, Joe Fawcett</author> <publisher>Wrox</publisher> </book> <book isbn="0764579088"> <title>Professional JavaScript for Web Developers</title> <author>Nicholas C. Zakas</author> <publisher>Wrox</publisher> </book> <book isbn="0764557599"> <title>Professional C#</title> <author>Simon Robinson, et al</author> <publisher>Wrox</publisher> </book> <book isbn="1861006314"> <title>GDI+ Programming: Creating Custom Controls Using C#</title> <author>Eric White</author> <publisher>Wrox</publisher> </book> <book isbn="1861002025"> <title>Professional Visual Basic 6 Databases</title> <author>Charles Williams</author> <publisher>Wrox</publisher> </book> </bookList>
As you can see, the document element <bookList/> contains a few <book/> elements, which include information about a given book.
The first step is to create an XML DOM document and load the XML data into it. Because books.xml will be loaded asynchronously, you need to set the onreadystatechange event handler:
var oXmlDom = zXmlDom.createDocument(); oXmlDom.onreadystatechange = function () { if (oXmlDom.readyState == 4) { } };
When the readystatechange event fires and the event handler is called, you check the readyState property; a value of 4 lets you know that the document is completely loaded and the DOM is ready to use.
The next step is to check for errors because even though the document is loaded, that is not necessarily a sign that everything works as it should:
var oXmlDom = zXmlDom.createDocument(); oXmlDom.onreadystatechange = function () { if (oXmlDom.readyState == 4) { if (oXmlDom.parseError.errorCode == 0) { parseBookInfo(oXmlDom); } else { var str = "An error occurred!!\n" + "Description: "+ oXmlDom.parseError.reason + "\n" + "File: "+ oXmlDom.parseError.url + "\n" + "Line: "+ oXmlDom.parseError.line + "\n" + "Line Position: "+ oXmlDom.parseError.linePos + "\n" + "Source Code: "+ oXmlDom.parseError.srcText; alert(str); } } };
If no error occurred, the XML DOM document is passed to parseBookInfo(), the function that parses the book list. If an error did occur, the error information collected in the parseError object is displayed in an alert.
With the onreadystatechange event handler written, the load() method is used to load the XML data:
oXmlDom.load("books.xml");
The XML document is now loaded. The next step in the process is to parse the XML data.
The parseBookInfo() function is in charge of parsing the DOM document. This function accepts one argument, which is the DOM document itself:
function parseBookInfo(oXmlDom) { var oRoot = oXmlDom.documentElement; var oFragment = document.createDocumentFragment();
The variable oRoot is set to the documentElement of the XML document. This is merely a convenience, because it is far easier and faster to type oRoot than oXmlDom.documentElement. You also create a document fragment. The parseBookInfo() function generates many HTML elements and thus, many changes to the HTML DOM loaded in the browser. Adding each element to the HTML DOM individually is an expensive process in terms of the time it takes to display the changes. Instead, each element is added to a document fragment, which will be added to the document once all HTML elements are created. Doing so allows the HTML DOM to be updated only once instead of multiple times, resulting in faster rendering.
You know that only <book/> elements are children of the document element, so you can iterate through the childNodes collection:
var aBooks = oRoot.getElementsByTagName("book"); for (var i = 0; i < aBooks.length; i++) { var sIsbn = aBooks[i].getAttribute("isbn"); var sAuthor, sTitle, sPublisher;
Inside the for loop, the actual parsing begins. To start, the isbn attribute of the <book/> element is retrieved with getAttribute() and stored in sIsbn. This value is used to display the book cover as well as the actual ISBN value to the user. The variables sAuthor, sTitle, and sPublisher are also declared; these variables will hold the values of the <author/>, <title/>, and <publisher/> elements, respectively.
Next, you retrieve the book's data, which can be done in a number of different ways. You could use the childNodes collection and loop through the children, but this example uses a different approach. You can accomplish the same result using a do…while loop, which makes use of the firstChild and nextSibling properties:
var oCurrentChild = aBooks[i].firstChild; do { switch (oCurrentChild.tagName) { case "title": sTitle = oCurrentChild.text; break; case "author": sAuthor = oCurrentChild.text; break; case "publisher": sPublisher = oCurrentChild.text; break; default: break; } oCurrentChild = oCurrentChild.nextSibling; } while (oCurrentChild = oCurrentChild.nextSibling);
In the first line, the variable oCurrentChild is assigned the first child of the current <book/> element. (Remember, this occurs inside of the for loop.) The child tagName is used in a switch block to determine what should be done with its data. The switch block then does its work assigning the data variable of the corresponding tagName of the current node. To retrieve this data, you use the node's text property, which retrieves all text nodes within the element. The oCurrentChild variable is assigned the node immediately following the current node by using the nextSibling property. If a next sibling exists, the loop continues; if not, oCurrentChild is null and the loop exits.
When all data variables contain the needed data, you can start generating HTML elements to display that data. The HTML structure of the elements you create programmatically looks like this:
<div class=" bookContainer"> <img class=" bookCover" alt=" Professional Ajax" src="0471777781.png" /> <div class=" bookContent"> <h3>Professional Ajax</h3> Written by: Nicholas C. Zakas, Jeremy McPeak, Joe Fawcett<br /> ISBN #0471777781 <div class=" bookPublisher">Published by Wrox</div> </div> </div>
To add some readability to the list, the containing <div/> element will have alternating background colors. Books that are an odd number in the list will have a grayish background color and a class name of bookContainer-odd, whereas even books will have a white background defined by the bookContainer CSS class.
Generating this coding through DOM methods is an easy but lengthy process. The first step is to create the containing <div/>, the <img/>, and the content <div/> elements, which is done through the createElement() DOM method:
var divContainer = document.createElement("div"); var imgBookCover = document.createElement("img"); var divContent = document.createElement("div"); var sOdd = (i % 2)?"":"-odd"; divContainer.className = "bookContainer" + sOdd;
Along with the element creation, the differing class names are processed here as well. The current book is judged to be odd or even by the use of the modulus (%) operator. The sOdd variable is assigned the appropriate appendix, an empty string for even and "-odd" for odd, and used in the className assignment.
You can then assign the properties of the book cover image. These PNG images use the ISBN number as their file names:
imgBookCover.src = "images/" + sIsbn + ".png"; imgBookCover.className = "bookCover"; divContainer.appendChild(imgBookCover);
Here, the src and className properties are assigned and the image is appended to divContainer. With the image finished, you can add the content. The first piece of information to be added is the book's title, which is a heading level 3 element (<h3/>). Again, this element is created with createElement():
var h3Title = document.createElement("h3"); h3Title.appendChild(document.createTextNode(sTitle)); divContent.appendChild(h3Title);
To create a text node containing the title, you use the createTextNode() method, which is appended to the <h3/> element, and then append the completed heading to divContent.
The author and ISBN information are next to be added. These two pieces of information are text nodes and have no parent element other than divContent. There is, however, one breaking element in between the two text nodes:
divContent.appendChild(document.createTextNode("Written by: "+ sAuthor)); divContent.appendChild(document.createElement("br")); divContent.appendChild(document.createTextNode("ISBN: #" + sIsbn));
This code creates this information. First, the text node containing the author information is appended to divContent, followed by the creation and appending of the breaking element (<br/>). Last, you append the text node containing the ISBN information.
The last piece of information to add is the publisher:
var divPublisher = document.createElement("div"); divPublisher.className = "bookPublisher"; divPublisher.appendChild(document.createTextNode("Published by: "+ sPublisher)); divContent.appendChild(divPublisher);
The publisher is displayed in a <div/> element. After its creation, the className is assigned "bookPublisher" and the text node containing the publisher's name is appended to the element. The divPublisher element is complete, so you can append it to divContent.
At this point, all data operations are complete. However, divContent still lacks its class name and must be appended to divContainer, which in turn must be appended to the document fragment. The following three lines of code do this:
divContent.className = "bookContent"; divContainer.appendChild(divContent); oFragment.appendChild(divContainer);
The last step is to append the document fragment to the page body after the book nodes are iterated through:
document.body.appendChild(oFragment);
This code doesn't actually append the document fragment itself; instead, it appends all the child nodes of the document fragment, making all the changes to the HTML DOM at once. With this final line of code, parseBookInfo() is complete.
The body of this web page is generated entirely by JavaScript. Because of this, the element creation and insertion code must execute after the document is loaded. Remember, parseBookInfo() is called after books.xml is loaded, so the XML DOM object creation code needs to execute at the page load. Create a function called init() to house the XML DOM creation code:
function init() { var oXmlDom = zXmlDom.createDocument(); oXmlDom.onreadystatechange = function () { if (oXmlDom.readyState == 4) { if (oXmlDom.parseError.errorCode == 0) { parseBookInfo(oXmlDom); } else { alert("An Error Occurred: " + oXmlDom.parseError.reason); } } }; oXmlDom.load("book.xml"); }
You'll use init() to handle the window.onload event. This will help ensure that the JavaScript-generated elements are added to the page without causing errors.
This mini application houses itself in an HTML document. All that is required are two <script/> elements, a <link/> element for the CSS, and the assignment of the onload event handler:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <htmlxmlns="http://www.w3.org/1999/xhtml" > <head> <title>Book XML Exercise</title> <link rel=" stylesheet" type=" text/css" href=" books.css" /> <script type=" text/javascript" src=" zxml.js"></script> <script type=" text/javascript" src=" books.js"></script> </head> <body onload=" init()"> </body> </html>
When you run this code, you will see the result shown in Figure 4-2.