Dynamic HTML: The Definitive Reference, 2rd Ed.Dynamic HTML: The Definitive ReferenceSearch this book

5.9. Client-Side Includes

With recent browsers, you have at your disposal the power to bring together multiple HTML sources into what the user sees as a single page. Rectangular segments of the page can be swapped en masse without disturbing the rest of the page. Another blending of multiple data sources comes in the form of a kind of virtual document that can be used to quietly load other external documents, including XML data that is returned by a server query. From that data, your client-side scripts can inspect the XML document tree and perform scripted transforms of the data to generate familiar HTML constructs, such as tables.

5.9.1. The iframe Element

IE 4 (and later) and Netscape 6 (and later) support the iframe element, which is an embeddable rectangular space that accepts HTML content from a different URL. Just like a frame in a frameset, an iframe acts as a window to the document loaded into it. That document has its own document object and node tree, completely separate from the host page.

Note that although the HTML 4.01 specification suggests that the object element can also be used to embed an external document into a page, most browsers do not yet support this possibility. Moreover, for scripting purposes, there is a significant incentive to embed external content in an iframe because of the access to the document node tree (within the confines of same-domain security policies, of course). Be aware, however, that the iframe element validates in XHTML only for the Transitional and Frameset doctypes.

By assigning a new URL to the src property of an iframe element, a script can load a new document into the frame without reloading or touching the containing page. Depending on your design goals and the variations in content length between documents shuffled in and out of an iframe, you will need some script help in most browsers to adjust the size of the iframe's rectangle.

On the design side, the attributes available to you have a significant impact on the look of the iframe element. By default, an iframe element renders itself with a border style that makes the frame appear to be carved out of the page's surface. Unless otherwise specified by attribute values, the dimensions of an iframe in IE/Windows and Netscape 6 or later is in the range of 300 pixels wide by 150 pixels high, with any overflow causing the display of scrollbars in the frame's rectangle. The exception to this behavior is IE/Mac, which automatically sizes a default iframe to accommodate its content height.

If you want to make the included document appear as if it were part of the containing page, you can eliminate the border and the possibility of scrollbars appearing for overflow content. But if a script changes the content after the initial loading, you will probably want to adjust the size of the rectangle to allow overflow content to be visible or to remove extra white space around smaller content. The following tag is an example of an attribute set that embeds an iframe into a document as a horizontal band in the document:

<iframe id="myFrame" frameborder="0" vspace="0" hspace="0" marginwidth="0" 
  marginheight="0" width="100%" src="external.html" scrolling="no" 
  style="overflow:visible"></iframe>

The style attribute can also be specified as a style sheet in the head of the document. By setting the width to 100% and assigning zero to the other attributes, the content of the iframe flows into a document with the same look as a block element of the page. What your scripts must take care of, however, is the height of the iframe. If you specify a height of 100%, the percentage is calculated from the containing document, not the content being loaded into the iframe.

To adjust the height of the iframe, a script must evaluate the height of the iframe content after it loads, and then adjust it accordingly. IE and Netscape have different ways of reading this information from an iframe document. The following function should be invoked whenever content of unknown or variable height is loaded into an iframe:

function adjustIFrameSize(id) {
    var myIframe = document.getElementById(id);
    if (myIframe) {
        if (myIframe.contentDocument && 
            myIframe.contentDocument.body.offsetHeight) {
            // W3C DOM syntax for NN 6
            myIframe.height = myIframe.contentDocument.body.offsetHeight;    
        } else if (myIframe.Document && myIframe.Document.body.scrollHeight) {
            // IE DOM syntax
            myIframe.height = myIframe.Document.body.scrollHeight;
        }
    }
}

The ideal place for this function to be invoked is an onload event handler of the iframe element. IE supports this event, but Netscape and the W3C standard do not. To trigger this function when the host page loads, use the host page's onload event handler to do the job.

If a script assigns a new URL to an existing iframe, the script needs to give the browser time to load the new content before invoking the frame resizing function. When deploying this in an IE-only environment, you can use the onload event handler of the iframe element because you won't have to worry about network latency. But for Netscape 6 or cross-browser applications, you'll have to experiment with suitable timeout delays. The following example assigns a URL to an iframe and then invokes the resizing function after a brief delay:

function loadIFrame(id, url) {
    document.getElementById(id).src = url;
    setTimeout("adjustIFrameSize('" + id + "')", 1500);
}

A more reliable approach entails coding the individual documents that get loaded into the iframe. For example, you could insert the following event handler into the <body> tag of each document:

onload="loaded=true;"

This sets a global variable called loaded in the window object that is the iframe. You must access this property in a roundabout way because it is not a property of the iframe element. Also, the syntax for reaching this property is different for the IE and W3C DOMs (the W3C version is not implemented in IE through Version 6). Here is one way to reach the loaded variable across browsers:

var loaded = (myIframe.contentDocument && myIframe.contentDocument.defaultView) ? 
  myIframe.contentDocument.defaultView.loaded : ((myIframe.Document) ?  
  myIframe.Document.parentWindow.loaded : false);

You can then use setTimeout( ) to inspect this property every 500 or 1,000 milliseconds for a maximum number of tries (to avoid getting trapped in an infinite loop in the event of a bad network connection or missing document). The following function is an expanded version of adjustIFrameSize( ) that implements the timeout:

var timeoutID;
var attempts = 0;
function adjustIFrameSize(id) {
    var myIframe = document.getElementById(id);
    var loaded;
    if (myIframe) {
        loaded = (myIframe.contentDocument && 
                 myIframe.contentDocument.defaultView) ?
                 myIframe.contentDocument.defaultView.loaded : 
                 ((myIframe.Document) ?
                 myIframe.Document.parentWindow.loaded : false);
        if (!loaded) {
            if (attempts++ < 5) {
                timeoutID = setTimeout("adjustIFrameSize('" + id + "')", 500);
            } else {
                attempts = 0;
            }
        } else {
            if (myIframe.contentDocument && 
               myIframe.contentDocument.body.offsetHeight) {
                myIframe.height = myIframe.contentDocument.body.offsetHeight;
            } else if (myIframe.Document && myIframe.Document.body.scrollHeight) {
                myIframe.height = myIframe.Document.body.scrollHeight;
            }
            attempts = 0;
        }
    }
}

Note that none of the previous code interferes with IE/Mac's automatic iframe element resizing.

Scripts in the host document can reach DOM objects in the iframe document through references just like those in the previous examples. In the W3C DOM syntax (Netscape 6 only), the contentDocument property of iframe refers to the root of the document tree inside the frame. For IE, the Document property (with an uppercase D) of iframe does the same. Conversely, scripts in documents that load into an iframe can use traditional parent frame references to reach the containing window and its scripts or document (e.g., parent.functionName( )).

5.9.2. Embedding XML Data

An iframe is suitable for rendering HTML content because the browser takes over the object model, ensuring that documents lacking head or body elements have them. For XML documents, however, you don't want the browser to perform any HTML rendering. For example, if you load an XML document into an IE window or frame, the browser self-generates a ton of HTML around the data so that it displays in a pretty-printed format. This makes it nearly impossible (or not worth the effort) for scripts to extract the XML data or document node tree structure. Instead, you need to load the XML data into a special container that both preserves the data as-is and permits script access to the document tree. Both IE/Windows and Netscape 6 or later provide these facilities, but, again, in their divergent ways.

The Netscape approach utilizes the W3C DOM document.implementation object to generate a nonrendering document, into which you can load documents (with the help of the Mozilla extension for the load( ) method). The Netscape 6 script sequence is as follows:

var xDoc = document.implementation.createDocument("", "doc", null);
xDoc.load("myXMLDocument.xml");

On the IE side, you invoke an ActiveX control that creates a similar kind of document object. Microsoft has released multiple generations of this control, but the most backward-compatible version is suitable:

xDoc = new ActiveXObject("Msxml.DOMDocument");
xDoc.load("myXMLDocument.xml");

The URL you load into the document object can also be a call to a server process that returns XML-formatted data.

Once the XML document is loaded, your scripts can use DOM node referencing properties and methods to access the elements, attributes, and text nodes from which they can assemble HTML for rendering. Because whitespace in a formatted XML document gets treated as text nodes in some implementations, your scripts must be sure to work around unneeded text nodes. Example 5-12 provides a version of Example 5-11 that pulls its data from an external XML file, rather than a JavaScript array of objects embedded within the HTML document. The structure of the XML file for this demonstration is as follows:

<?xml version="1.0"?> 
<season>
    <bowl>
        <number>I</number>
        <year>1967</year>
        <winner>Packers</winner>
        <winscore>35</winscore>
        <loser>Chiefs</loser>
        <losscore>10</losscore>
    </bowl>
    <bowl>
        ...
    </bowl>
...
</season>

The code execution sequence in Example 5-12 fires initially from an onload event handler in the body. The initialization routine invokes a function that verifies support for the loading of external XML documents. Object detection plays a large role here. On the IE side, it verifies that the <object> tag in the document has successfully loaded the ActiveX object, thus verifying that it is available for scripting.

Example 5-12. Embedding external XML data

<html>
<head>
<title>Embedding External XML Data</title>
<style type="text/css">
body {background-color:#ffffff}
table {table-collapse:collapse; border-spacing:0}
td {border:2px groove black; padding:7px}
th {border:2px groove black; padding:7px}
.ctr {text-align:center}
</style>
<script language="JavaScript" type="text/javascript">
// global reference to XML document object
var xDoc;
  
// Draw table from xDoc document tree data
function drawTable(tbody) {
    var tr, td, i, j, oneRecord;
    tbody = document.getElementById(tbody);
    // node tree
    var data = xDoc.getElementsByTagName("season")[0];
    // for td class attributes
    var classes = ["ctr","","","","ctr"];
    for (i = 0; i < data.childNodes.length; i++) {
        // use only 1st level element nodes
        if (data.childNodes[i].nodeType == 1) {
            // one bowl record
            oneRecord = data.childNodes[i];
            tr = tbody.insertRow(tbody.rows.length);
            td = tr.insertCell(tr.cells.length);
            td.setAttribute("class",classes[tr.cells.length-1]);
            td.innerHTML = 
              oneRecord.getElementsByTagName("number")[0].firstChild.nodeValue;
            td = tr.insertCell(tr.cells.length);
            td.setAttribute("class",classes[tr.cells.length-1]);
            td.innerHTML = 
              oneRecord.getElementsByTagName("year")[0].firstChild.nodeValue;
            td = tr.insertCell(tr.cells.length);
            td.setAttribute("class",classes[tr.cells.length-1]);
            td.innerHTML = 
              oneRecord.getElementsByTagName("winner")[0].firstChild.nodeValue;
            td = tr.insertCell(tr.cells.length);
            td.setAttribute("class",classes[tr.cells.length-1]);
            td.innerHTML = 
              oneRecord.getElementsByTagName("loser")[0].firstChild.nodeValue;
            td = tr.insertCell(tr.cells.length);
            td.setAttribute("class",classes[tr.cells.length-1]);
            td.innerHTML = 
              oneRecord.getElementsByTagName("winscore")[0].firstChild.nodeValue + 
              " - " + 
              oneRecord.getElementsByTagName("losscore")[0].firstChild.nodeValue;
        }
    }
}
// verify that browser supports XML features and load external .xml file
function verifySupport(xFile) {
    if (document.implementation && document.implementation.createDocument) {
        // this is the W3C DOM way, supported so far only in NN6
        xDoc = document.implementation.createDocument("", "theXdoc", null);
    } else if (typeof ActiveXObject != "undefined") {
        // make sure real object is supported (sorry, IE5/Mac)
        if (document.getElementById("msxml").async) {
            xDoc = new ActiveXObject("Msxml.DOMDocument");
        }
    }
    if (xDoc && typeof xDoc.load != "undefined") {
        // load external file (from same domain)
        xDoc.load(xFile);
        return true;
    } else {
        var reply = confirm("This example requires a browser with XML " + 
                    "support, such as IE5+/Windows or Netscape 6+.\n \n" +
                    "Go back to previous page?");
        if (reply) {
            history.back( );
        }
    }
    return false;
}
  
// initialize first time -- invoked onload
function init(xFile) {
    // confirm browser supports needed features and load .xml file
    if (verifySupport(xFile)) {
        // let file loading catch up to execution thread
        setTimeout("drawTable('bowlData')", 1000);
    }
}
</script>
</head>
<body onload="init('superBowls.xml');">
<h1>Super Bowl Games</h1>
<hr>
<table id="bowlGames">
<thead>
<tr><th>Bowl</th>
    <th>Year</th>
    <th>Winner</th>
    <th>Loser</th>
    <th>Score (Win - Lose)</th>
</tr>
</thead>
<tbody id="bowlData"></tbody>
</table>
<!-- Try to load Msxml.DOMDocument ActiveX to assist support verification -->
<object id="msxml" WIDTH="1" HEIGHT="1" 
classid="CLSID:2933BF90-7B36-11d2-B20E-00C04F983E60" ></object>
</body>
</html>

After a delay to allow the XML data to arrive, the drawTable( ) function traverses the document tree of the XML data and uses table-related methods to create the rows and cells, stuffing the cells with text node values for the XML elements. It is during this process of traversing the tree in a for loop that each valid bowl element (and not any possible newline character text nodes) becomes a data source for each row.

One point you can deduce from comparing Examples 5-11 and 5-12 is that if you wish to re-sort the table data without reloading the page, it is far easier and more efficient to use the sorting facilities of JavaScript arrays than it is to manipulate XML data. As a result, you may find it more convenient to convert external XML data into more convenient arrays of JavaScript objects. The typical regularity of XML data greatly simplifies and speeds the creation of the JavaScript counterparts. Example 5-13 shows a function that converts the XML data file from Example 5-12 to corresponding JavaScript data objects.

Example 5-13. XML to JavaScript array function

// Global holder of JS-formatted data
var jsData = new Array( );
// Convert xDoc data into JS array of JS objects
function XML2JS( ) {
    var rawData = xDoc.getElementsByTagName("season")[0];
    var i, j, oneRecord, oneObject;
    for (i = 0; i < rawData.childNodes.length; i++) {
        if (rawData.childNodes[i].nodeType == 1) {
            oneRecord = rawData.childNodes[i];
            oneObject = jsData[jsData.length] = new Object( );
            for (j = 0; j < oneRecord.childNodes.length; j++) {
                if (oneRecord.childNodes[j].nodeType == 1) {
                    oneObject[oneRecord.childNodes[j].tagName] = 
                      oneRecord.childNodes[j].firstChild.nodeValue;
                }
            }
        }
    }
}

With the data in this format, you can apply the sorting facilities from Example 5-11 to the data.



Library Navigation Links

Copyright © 2003 O'Reilly & Associates. All rights reserved.