
Content
Social
| DOM XML Wrapper for JavaScript |
|
|
| Written by Bruno Grange | ||||||||||||||||||
| Wednesday, 03 September 2008 00:36 | ||||||||||||||||||
|
By Nicholas C. Zakas ( This e-mail address is being protected from spambots. You need JavaScript enabled to view it ) IntroductionIn my opinion, one of the most useful parts of JavaScript is the DOM Document object. With this object, a developer not only can build a DOM Document from scratch, but also can load a file via HTTP into it. The result is the possibility of having client-side JavaScript being loaded with server-side information. What could possibly be better? That answer is simple: if there was a common interface for the DOM Document between Internet Explorer and Netscape. This article will focus on creating a single DOM Document interface for developers to use in both Internet Explorer 5.0+ (Windows) and Mozilla (The Mozilla browser itself, or Netscape 6.1+). This interface will use both IE's ActiveX approach as well as Mozilla's standards-compliant approach. Because the IE implementation uses ActiveX technology, this will not work on IE for the Mac. A Little BackgroundDevelopers have access to a full library of XML-related objects through Microsoft's MSXML Parser. Each of these objects is an ActiveX object. You can create a DOM Document in Internet Explorer by doing the following:
Mozilla follows the W3C's DOM standard (http://www.w3.org/DOM/). As such, you create a DOM Document in the following way:
The interesting thing to note is that because an ActiveX object is compiled code on the client machine (Note: not the client Web browser), you cannot have
What this means for us is that if we want to extend one interface to be more compatible with the other, it will have to be the Mozilla interface that is customized to be more compatible with the IE interface. The FactoryAs part of this process, we are going to make a JavaScript factory. A factory is a type of object that serves only as a method to access a group of functions. For instance, the following uses the native JavaScript
Note that we didn't instantiate a
A Cross-Browser DOM Document Wrapper
The
|
|
Making Interfaces Play Together
We have just finished our common creation method for a DOM Document, but we are not done yet. While both the IE and Mozilla interfaces have the same standards-compliant attributes and methods, both have also added proprietary attributes and methods that we need in order to protect the future of our interface.
The load() and loadXML() Methods
To begin, both IE and Mozilla support a proprietary load() method, which allows developers to load an XML document via HTTP in the following way:
objDOM.load("http://www.nczonline.net/test.xml");
IE adds the additional loadXML() method (which we used earlier). The loadXML() method allows the developer to pass in an XML string to load into the DOM Document. Mozilla doesn't provide a simple way to pass in an XML string, but we can create our own loadXML() method for the Mozilla version.
The name of the DOM Document class in Mozilla is simply called Document. We can extend the Document class just as we can extend any other native JavaScript class. So, we will check for Mozilla, and then extend the Document class to add the loadXML() method:
if (isMoz) {
//add the loadXML() method to the Document class
Document.prototype.loadXML = function(strXML) {
}
}
But how can we take the XML string and get it parsed into a DOM Document? Mozilla provides the DOMParser class to do just this. The DOMParser class is part of the XMLExtras module of Mozilla (click here for more information) and has a method called parseFromString() that will take an XML string and a content-type (in our case, "text/xml") and produce a Document object.
First, let's add in the DOMParser:
if (isMoz) {
//add the loadXML() method to the Document class
Document.prototype.loadXML = function(strXML) {
//create a DOMParser
var objDOMParser = new DOMParser();
//create new document from string
var objDoc = objDOMParser.parseFromString(strXML, "text/xml");
} //End: function
} //End: if
The additional problem for us is how to transfer the data in the Document object produced by the DOMParser into the current Document. We need to be sure to clear the Document of all data already in it. To do this, we need to remove every top-level child node of the Document, like so:
//make sure to remove all nodes from the document
while (this.hasChildNodes())
this.removeChild(this.lastChild);
Now that the Document is cleared of data, we need to add in the data from the DOMParser Document. To do this, we need to iterate through all of the top-level nodes of the DOMParser Document. Then, we need to add each of those nodes into the current Document. The added wrinkle is that you cannot add a node from one Document object into another directly. You need to first import the node into the Document, using the importNode() method and then add the node into the current Document.
The importNode() method takes two parameters: the node to import, and a boolean indicating if the node's children should also be imported. Since we want to copy all of the data, we set the boolean to true. This method returns the imported node, which then can be added into the current Document. This is done as follows:
//add the nodes from the new document
for (var i=0; i < objDoc.childNodes.length; i++) {
//import the node
var objImportedNode = this.importNode(objDoc.childNodes[i], true);
//append the child to the current document
this.appendChild(objImportedNode);
} //End: for
We can now use the loadXML() method in both IE and Mozilla, which will make many people happy.
The xml Attribute
Microsoft provided the loadXML() method to allow the developer to input an XML string into a DOM Document. Likewise, they provided a way to get that XML string back. The xml attribute of the DOM Document will return the complete XML string being represented by the DOM Document. The xml attribute is also on every node in the DOM Document in order to return XML from any specific location in the DOM Document. This is the natural complement to the loadXML() method, so let's add it into the Mozilla interface.
Normally, if you wanted to add an attribute to a class, you would do it as follows:
Document.prototype.xml = "";
However, in this case, we won't be doing that. Instead, we will define an attribute "getter;" or the function that is called when a developer tries to get the value of an attribute. Doing this will automatically create the attribute for the class.
An important point to make before going on is that we want the xml attribute to be on every node in the Document, as well as on the Document itself. To do this, we could add the xml attribute on the Document and Element classes, which is not optimal. The other option is to just add the xml attribute to the Node class. Every type of node in a DOM Document, whether it is Document, Element, or anything else, is inherited from the Node class. So, we will just add the xml attribute to the Node class, and all of the others will automatically inherit it.
Adding a getter is done using the hidden method __defineGetter__(). The __defineGetter__() method takes two parameters: a string indicating the attribute to watch and a function to call when the attribute is accessed. Let's assume we will define a function called _Node_getXML() to handle this, so we will define the getter for the Node as follows:
Node.prototype.__defineGetter__("xml", _Node_getXML);
Now, all that is left is to create the _Node_getXML() function. In order to convert a Node to an XML string, Mozilla provides an XMLSerializer class. The XMLSerializer has a method called serializeToString(), which takes a Node (which is what both Element and Document are extended from) and returns the XML string representing the Node. Our function will use the XMLSerializer and pass in this (for those unaware, this represents the object that is calling the method). The XML string must then be returned:
function _Node_getXML() {
//create a new XMLSerializer
var objXMLSerializer = new XMLSerializer;
//get the XML string
var strXML = objXMLSerializer.serializeToString(this);
//return the XML string
return strXML;
Error Handling
We've now come to the last part of our project, error handling. As I'm sure you can guess, IE and Mozilla do it two different ways. IE's DOM Document has a parseError attribute that is filled with an error code if an error occurs. However, an error in the DOM Document does not throw a JavaScript error, so execution does not stop. In Mozilla, any error inside the load() method is thrown as a JavaScript error, and execution stops cold. So once again, we will make Mozilla more like IE and create a parseError attribute.
The parseError attribute starts out as 0, which indicates that there is no error. Whenever someone wants to check if there has been an error, a simple check to see if the parseError attribute is not equal to 0 does the trick. So let's start out by adding our parseError attribute with a default value of 0:
Document.prototype.parseError = 0;
Next, we will add a try...catch block in our _Document_load() method. Because it would be impossible to map all of the errors in Mozilla to their IE counterparts, we'll just set the parseError attribute to –9999999 to indicate that there is an error (we will also change the readyState attribute to indicate the loading action is done). Important thing to note: we must set the parseError attribute to 0 every time the load() method is called, because it is essentially wiping the slate clean for the Document object:
function _Document_load(strURL) { //set the parseError to 0 this.parseError = 0; //change the readyState changeReadyState(this, 1) //watch for errors try ( //call the original load method this.__load__(strURL); } catch (objException) { //set the parseError attribute this.parseError = -9999999; //set readyState to 4, we are done loading changeReadyState(this, 4); } // End: try...catch }
And here's another interesting wrinkle: if an error is caused while parsing, Mozilla doesn't throw an exception. Instead, it creates an XML string containing the details of the error, for example:
<parsererror xmlns="http://www.w3.org/1999/xhtml">XML Parsing Error: mismatched tag. Expected: </root>. Location: file:///C:/DOCUME~1/Nicholas/MYDOCU~1/MYARTI~1/USINGT~1/Example/Invalid.xml Line Number 3, Column 37:<sourcetext> <root image="root.gif">My Root</boot> ------------------------------------^</sourcetext></parsererror>
So, we actually need to check for an error after the XML is loaded to see if one of these XML error strings has been generated (or if there is no documentElement for some reason). Because we have incredible foresight, we will create a function called handleOnLoad() to encapsulate this process. The handleOnLoad() function will take one parameter, the Document to check. It will also set the readyState attribute to 4 (regardless of any errors, the loading is over):
function handleOnLoad(objDOMDocument) { //check for a parsing error if (!objDOMDocument.documentElement || objDOMDocument.documentElement.tagName == "parsererror") objDOMDocument.parseError = -9999999; //change the readyState changeReadyState(objDOMDocument, 4); }
Now, we need to add this into two places, the _Document_onload() function and the loadXML() method (note that we aren't adding it to the _Document_load() function, because we can only determine parsing errors after the XML has been loaded):
function _Document_onload() { //handle the onload event handleOnLoad(this); } //add the loadXML() method to the Document class Document.prototype.loadXML = function(strXML) { //change the readystate changeReadyState(this, 1); //create a DOMParser var objDOMParser = new DOMParser(); //create new document from string var objDoc = objDOMParser.parseFromString(strXML, "text/xml"); //make sure to remove all nodes from the document while (this.hasChildNodes()) this.removeChild(this.lastChild); //add the nodes from the new document for (var i=0; i < objDoc.childNodes.length; i++) { //import the node var objImportedNode = this.importNode(objDoc.childNodes[i], true); //append the child to the current document this.appendChild(objImportedNode); } //End: for //we can't fire the onload event, so we fake it handleOnLoad(this); } //End: function
Now the parseError attribute should be fairly accurate for Mozilla.
Conclusion
After a somewhat lengthy trek, we have created our cross-browser (IE5/NS6) DOM Document wrapper. To review, you can now create a DOM Document object by doing the following:
var objDOMDocument = jsXML.createDOMDocument(strNamespaceURI, strRootTagName);
You can load an XML string into the DOM Document by doing:
objDOMDocument.loadXML("<myroot />");
You can retrieve the XML string by doing:
var strXML = objDOMDocument.xml;
To load an external XML file, we do the following:
objDOMDocument.async = false; objDOMDocument.load("myfile.xml");
To load it asynchronously:
objDOMDocument.async = true; objDOMDocument.load("myfile.xml");
You can set an event handler to determine when an XML file has been fully loaded by doing:
objDOMDocument.onreadystatechange = myFunction; function myFunction() { if (this.readyState == 4) alert("Loaded"); }
And to check for errors, you can do the following:
if (objDOMDocument.parseError != 0) alert("Error");
We are now ready to use a DOM Document the same way across Internet Explorer 5.0+ (Windows) and Mozilla (Mozilla or Netscape 6.1+). This example illustrates all of these uses. I ask that you try it in both browsers. There are slight differences, but the overall functionality is the same.
About the Author
Nicholas C. Zakas is a user interface designer for web applications at MatrixOne, Inc. in Massachusetts. Nicholas works primarily as a client-side developer using JavaScript, DHTML, XML and XSLT. He can be reached via e-mail at This e-mail address is being protected from spambots. You need JavaScript enabled to view it or at his Web site, http://www.nczonline.net.
}
Now, the Mozilla interface can use the xml attribute in the same way that IE uses it.




