reptile7's JavaScript blog
Monday, May 30, 2011
 
Ajax 242
Blog Entry #216
Your retrieval of remote XML data was successful!
We continue today our analysis of HTML Goodies' "How to Develop Web Applications with Ajax: Part 1" tutorial. At the end of our last episode, we sent off a GET HTTP request for a data.xml XML document via an xmlObj XMLHttpRequest object. What next? How do we access that document and its contents?

In the Select and copy section of Blog Entry #180, we very briefly noted that the DOM HTMLIFrameElement interface features a contentDocument attribute that references an iframe's src document. The XMLHttpRequest object has an analogous responseXML property that will serve as our gateway to a requested XML document. The responseXML return implements the DOM Document interface, meaning that once we get our hands on that return, we can use the interface's properties and methods to probe the requested document and walk through the document's DOM tree.

However, we can't just follow the xmlObj.send(null); command with some sort of xmlObj.responseXML expression, as the browser will hit that expression before the data.xml data arrives and consequently the expression will return null - not what we want, needless to say. Even for an asynchronous request, it is necessary to monitor the server's response so as to ensure that the requested document is available before we attempt to act on it; fortunately, this is easily accomplished via the XMLHttpRequest readyState property.

Since IE 4, Microsoft has provided a readyState property for tracking the loading progress of an object - go here and here for its documentation - this property can be applied to most HTML element objects but is most directly applicable to objects that load data themselves: the document object, the image object, the frame object, and so on. As an object loads, it goes through a series of ≤5 readyStates, not necessarily in this order: uninitialized, loading, loaded, interactive, and complete; to detect changes in these values, a companion onreadystatechange event handler was also implemented.

At least for the loading and complete stages, non-IE browsers now support the readyState property for the document object - Mozilla's document.readyState page is here. The W3C has standardized document.readyState and equipped new "media" (audio and video) elements with a readyState property in HTML5.

When Microsoft introduced the XMLHTTP object in IE 5 for Windows, it brought on board a modified readyState property having a scale of integer values running from 0 to 4, 4 meaning all the data has been received; a corresponding onreadystatechange event handler is again used to monitor changes in these values. The XMLHTTP readyState property and onreadystatechange event handler were subsequently ported by Netscape to the XMLHttpRequest object.

At the top of the tutorial's second page, the author attempts to detail the XMLHttpRequest readyState property by correlating its 0-4 scale with the uninitialized-to-complete readyState scale for other objects. As it happens, the two scales really don't match very well. The 0 and 4 values do respectively correspond to uninitialized and complete; however:

• The 1 value means A request has been opened, but the send( ) method has not been called and has no counterpart on the non-XMLHttpRequest side.

• The 3 value maps onto the loading value, but the 2 value is also a loading-type value indicating that the HTTP headers and status of the server's response are available.

• The interactive value means User can interact with the object even though it is not fully loaded and has no counterpart on the XMLHttpRequest side.

• Moreover, the loaded value means Object has finished loading its data and seems to be equivalent to the complete value.

In fairness to the author, the Object Properties section of Apple's "Dynamic HTML and XML: The XMLHttpRequest Object" article also implies an equivalency between the two scales; Mozilla also does this in the Step 2 - Handling the server response section of its "Getting Started [with Ajax]" article. To get sorted out on the XMLHttpRequest readyState property, you should consult
(a) the aforelinked Microsoft XMLHttpRequest readyState page,
(b) Mozilla's XMLHttpRequest.readyState page, and
(c) the States section of the WHATWG's XMLHttpRequest specification-in-progress.

Getting back to the tutorial code, the xmlObj.open("GET", file); command is preceded by a function expression that listens for changes in xmlObj's readyStates:

xmlObj.onreadystatechange = function ( ) { ... }

Microsoft's XMLHttpRequest onreadystatechange page states that you must set the event handler after calling open( ); maybe this is true for recent versions of IE but it's not true for the XMLHttpRequest-supporting browsers on my computer. Mozilla's send( ) method documentation used to say Note: Any event listeners you wish to set must be set before calling send( ) but no longer does so; I find that the script isn't hurt at all if the open( ) and send( ) commands are placed before the onreadystatechange expression.

The onreadystatechange function first checks if the request operation is complete - 4 is the only xmlObj readyState that we are really interested in:

if (xmlObj.readyState == 4) { ... }

If the if condition is true, then the onreadystatechange function next calls an updateObj( ) function

updateObj("xmlObj", xmlObj.responseXML.getElementsByTagName("data")[0].firstChild.data);

that will update the "sample data" p element of the current document. The first updateObj( ) call parameter, xmlObj (not xmlData as stated in the tutorial text), is the id value of the to-be-updated p element. As for the second updateObj( ) call parameter, what a mouthful, huh? This lengthy expression scoops up the textual content, including leading and trailing white space, of the data.xml document's data element. To see what's going on here, let's recast the expression on a feature-by-feature basis:

var xmldoc = xmlObj.responseXML;
var dataNodeList = xmldoc.getElementsByTagName("data");
var dataNode1 = dataNodeList[0];
var dataTextNode = dataNode1.firstChild;
var dataText = dataTextNode.data;
updateObj("xmlObj", dataText);


Per the discussion at the outset of the post, xmlObj.responseXML; gets the data.xml document as a whole. xmldoc specifically returns [object XMLDocument] with Firefox and Opera and [object Document] with Safari and Chrome.

To refresh your memory, here's the data.xml document again:

<?xml version="1.0" encoding="UTF-8"?>
<root>
  <data>
    This is some sample data. It is stored in an XML file and retrieved by JavaScript.
  </data>
</root>


xmldoc.getElementsByTagName("data"); gets an in-source-order list of data element nodes in the data.xml document. dataNodeList specifically returns [object NodeList] with Safari, Opera, and Chrome but strangely returns [object HTMLCollection] with Firefox.

dataNodeList[0]; gets the first-in-source-order (and only) data element in the dataNodeList NodeList. This statement's syntax is not quite standard; the W3C-kosher way to access the dataNode1 node is to use the item( ) method of the DOM NodeList interface, i.e., var dataNode1 = dataNodeList.item(0);. dataNode1 specifically returns [object Element] with all of the aforementioned browsers.

You may be wondering, "Can't we give the data element an id="dataID" attribute and then use the getElementById( ) method to grab it?" This is possible, but then we'd have to write up for the data.xml document a custom DTD that assigns the ID type to the id attribute. If you'd like to do that, be my guest. I'll pass.

dataNode1.firstChild; gets the first (and only) child node of the dataNode1 data element: an object implementing the DOM Text interface and representing the textual content of the element. dataTextNode specifically returns [object Text] with all of the aforementioned browsers.

dataTextNode.data; gets the character data of the dataTextNode Text node:

This is some sample data. It is stored in an XML file and retrieved by JavaScript.

In the data.xml source, this string is preceded by an end-of-line character and four space characters and is followed by an end-of-line character and two space characters; all of this white space is picked up by the data property but will be entirely or mostly discarded when we paste dataText into the current document.

The DOM textContent property can be substituted for the firstChild.data part of the original expression.

xmlObj.responseXML.getElementsByTagName("data").item(0).textContent;

We move now to the updateObj( ) function:

function updateObj(obj, data) {
    document.getElementById(obj).firstChild.data = data; }


The xmlObj id and the dataText string are initially assigned respectively to obj and data identifiers. Subsequently, the obj element's firstChild.data - all of the character data between the p element start-tag and the anchor element start-tag for the View XML data link - is replaced by the data string. Recall that we earlier zeroed out the View XML data link via a this.style.display='none'; command; if you're not going to bring back the link and are willing to overwrite it, then firstChild.data can be replaced by textContent in this case as well.

The author conspicuously does not provide a demo for his handiwork. How does it all go in practice? I'll give you the skinny and roll out my own demo in the following entry.

reptile7

Comments: Post a Comment

<< Home

Powered by Blogger

Actually, reptile7's JavaScript blog is powered by Café La Llave. ;-)