George Young
Microsoft Corporation
April 26, 1999
Download the source code for this article (zipped, 5.88K)
The following article was originally published in the MSDN Online Voices "Code Corner" column.
I really like integrative samples, those that pull together a few different technologies into one small app.
In this month's Code Corner, we'll try to do just that, tying together Extensible Markup Language (XML), Extensible Stylesheet Language (XSL), JScript®, and cascading style sheets (CSS) to create a hierarchical Dynamic HTML (DHTML) Table of Contents (TOC). Now that we've covered the acronyms, the sample is purposefully simple -- and you may already be intimately familiar with each of the technologies. If so, there will hopefully be some value in seeing them work together; if not, perhaps this will get you interested in exploring these further.
I'd like to know if you found this article interesting (or not) and useful (or not), as well as any questions, comments or suggestions you may have. Please drop me a line at corner@microsoft.com with your thoughts.
For Web developers, XML promises to make information more portable and flexible by containing data elements in "meaningful" tags. With the enhanced XSL support in Internet Explorer 5, it is now much easier to dispay XML data in the browser.
We've been using XML for some time in the Web Workshop to store TOC information, with an XSL style sheet to transform that information to HTML. The style sheet also "writes" links to a CSS and JScript file, so we have a one-step conversion of XML to DHTML. Storing the data in XML makes it easy to change the output format for all TOCs by modifying a single XSL style sheet.
Let's take a look at the four files -- XML, XSL, JScript, and CSS -- in order.
For this sample, we've created a list of articles, or "topics", relevant to web development. Each TOPIC element has a descriptive TITLE and a URL. Topics are grouped by TYPE within TOPICS elements. Note that the third TOPICS element contains TOPICS elements itself. The <?xml:stylesheet type="text/xsl" href="list.xsl"?> processing instruction at the top of the webdev.xml file tells Internet Explorer 5 to render the XML using this style sheet when the XML file is opened directly in the browser. (We'll cover doing this in ASP on the server at the end of the column.)
Here is the XML data:
<?xml version="1.0"?> <?xml:stylesheet type="text/xsl" href="list.xsl"?> <TOPICLIST TYPE="Web Dev References"> <TOPICS TYPE="DHTML"> <TOPIC> <TITLE>Objects</TITLE> <URL>/workshop/author/dhtml/reference/objects.asp</URL> </TOPIC> <TOPIC> <TITLE>Properties</TITLE> <URL>/workshop/author/dhtml/reference/properties.asp</URL> </TOPIC> <TOPIC> <TITLE>Methods</TITLE> <URL>/workshop/author/dhtml/reference/methods.asp</URL> </TOPIC> <TOPIC> <TITLE>Events</TITLE> <URL>/workshop/author/dhtml/reference/events.asp</URL> </TOPIC> <TOPIC> <TITLE>Collections</TITLE> <URL>/workshop/author/dhtml/reference/collections.asp</URL> </TOPIC> </TOPICS> <TOPICS TYPE="CSS"> <TOPIC> <TITLE>Attributes</TITLE> <URL>/workshop/author/css/reference/attributes.asp</URL> </TOPIC> <TOPIC> <TITLE>Length units</TITLE> <URL>/workshop/author/css/reference/lengthunits.asp</URL> </TOPIC> <TOPIC> <TITLE>Color table</TITLE> <URL>/workshop/author/dhtml/reference/colors/colors.asp</URL> </TOPIC> </TOPICS> <TOPICS TYPE="XML"> <TOPICS TYPE="XML DOM"> <TOPIC> <TITLE>Developer's guide</TITLE> <URL>/xml/XMLGuide/default.asp</URL> </TOPIC> <TOPIC> <TITLE>Objects</TITLE> <URL>/xml/reference/scriptref/XMLDOM_Objects.asp</URL> </TOPIC> </TOPICS> <TOPICS TYPE="XSL"> <TOPIC> <TITLE>Developer's guide</TITLE> <URL>/xml/XSLGuide/default.asp</URL> </TOPIC> <TOPIC> <TITLE>Elements</TITLE> <URL>/xml/reference/xsl/XSLElements.asp</URL> </TOPIC> <TOPIC> <TITLE>Methods</TITLE> <URL>/xml/reference/xsl/xslmethods.asp</URL> </TOPIC> <TOPIC> <TITLE>Pattern syntax</TITLE> <URL>/xml/reference/xsl/XSLPatternSyntax.asp</URL> </TOPIC> </TOPICS> </TOPICS> </TOPICLIST>
While we could manipulate the XML directly in code, XSL gives us a declarative approach that allows the transformation of XML into display output (in this case, HTML) with very little code (and heartache). XSL saves you from having to write a bunch of tree-walking code -- especially nice if you have a complicated nested hierarchy in your XML file. In this case, we can have an unknown number of TOPICS levels; XSL handles this nicely.
Let's take a look at the XSL code.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/TR/WD-xsl"> <xsl:template match="/"> <HTML> <HEAD> <TITLE>List <xsl:value-of select="TOPICLIST/@TYPE" /></TITLE> <LINK REL="stylesheet" TYPE="text/css" HREF="list.css" /> <SCRIPT TYPE="text/javascript" LANGUAGE="javascript" SRC="list.js"></SCRIPT> </HEAD> <BODY> <BUTTON ONCLICK="ShowAll('UL')">Show All</BUTTON> <BUTTON ONCLICK="HideAll('UL')">Hide All</BUTTON> <H1>List <xsl:value-of select="TOPICLIST/@TYPE" /></H1> <UL><xsl:apply-templates select="TOPICLIST/TOPICS" /></UL> <P><BUTTON ONCLICK="window.alert(document.body.innerHTML);">View HTML</BUTTON></P> </BODY> </HTML> </xsl:template> <xsl:template match="TOPICS"> <LI CLASS="clsHasKids"><xsl:value-of select="@TYPE" /> <UL> <xsl:for-each select="TOPIC"> <LI> <A TARGET="_new"> <xsl:attribute name="HREF"> http://msdn.microsoft.com<xsl:value-of select="URL" /> </xsl:attribute> <xsl:value-of select="TITLE" /> </A> </LI> </xsl:for-each> <xsl:if test="TOPICS"><xsl:apply-templates /></xsl:if> </UL> </LI> </xsl:template> </xsl:stylesheet>
As you can see, we have a bunch of HTML tags, and a few "xsl:" elements sprinkled about. We start off the XSL file by explicitly telling the processor that this is an XSL file, and then declare two xsl:template element blocks, <xsl:template match="/"> and <xsl:template match="TOPICS">.
The first block, <xsl:template match="/">, tells the processor to transform starting from the root of the XML file, indicated by the single forward slash. In this block, we do two things: we "write" the structure and generic elements of our HTML output, and we conditionally hook up to any additional template blocks. We take the value of the TYPE attribute of the TOPICLIST element -- Web Dev References -- and drop it into the <TITLE> and <H1> HTML elements. We also hook up the script (list.js) and style sheet (list.css) files, which will provide our DHTML look and behavior. At the top, we draw a couple of buttons that allow the user to show all TOC nodes or hide them, making use of two functions in the script file, and we add another button at the bottom to view the HTML source in an alert box.
The most interesting element in this first block is <xsl:apply-templates select="TOPICLIST/TOPICS" />. This tells the processor to transform all of the XML document's TOPICS nodes that are children of the root TOPICLIST node. If you take a look at the XML file above, you'll note that this consists of the TOPICS nodes of TYPE "DHTML", "CSS", and "XML". At this point, the XSL processor will now branch off from this first block and look for another xsl:template block in the stylesheet, one which matches our select="TOPICLIST/TOPICS" attribute. This, as you might guess, our second block.
Our second template block, <xsl:template match="TOPICS"> is where the fun starts. Each of our 3 TOPICS nodes whose context was passed to this block from the first, gets processed here. First, we open an HTML list-item element (LI) and give it a CLASS attribute of "clsHasKids", which we'll use later in our DHTML.We then output the TYPE of the TOPICS node we are processing, using <xsl:value-of select="@TYPE" />. To grab node values in XSL, the xsl:value-of element is used. If grabbing the value of an atribute, an "@" is appended to the beginning of the attribute name.
Next, we open an HTML unordered list (<UL>) which will contain all the children of our current TOPICS element. Using the xsl:for-each looping construct, which also changes the context to that element (TOPIC, in this case) for each TOPIC child, we create an <LI> element, and output its TITLE wrapped in an A link, whose HREF attribute is set to the value of the URL element. To dynamically add an attribute to an HTML output element in XSL, we use xsl:attribute, with its name attribute set to the HTML attribute we want to create; thus: <xsl:attribute name="HREF">. We populate the HREF value with our server name, http://msdn.microsoft.com, and the virtual root URL contained in the TOPIC URL, <xsl:value-of select="URL" />.
So, we have all the links in our top groups written out, but we haven't dealt with child TOPICS of TOPICS -- nested lists of topics. This is one area where XSL really shines; it will handle all our recursion for us in one single statement! Before closing our container <UL> and its parent <LI>, which we opened when we first started processing the TOPICS element upon entering this second block, we declare <xsl:if test="TOPICS"><xsl:apply-templates /></xsl:if>. Using the xsl:if conditional, which changes our context to the element for which we are testing, we ask the tree if there is a child TOPICS node. If there is, we <xsl:apply-templates />, which tell the processor to match whetever our current context may be. Because we've asked about TOPICS, if this child exists, we are then again matched to this second block an we loop right back into it. So, in the case of our third TOPICS element, "XML", we match two child TOPICS elements, and we re-enter this XSL block to output these to HTML. The approriate nesting is automatically preserved. Cool, no?
Here is the straight HTML output from the transformation.
That's it for the XSL code. We've generated a set of nested HTML lists by recursing through our XML document. Now that we have our that output, let's take a quick peek at the .js and .css code that makes our TOC dynamic.
Four functions handle all of the "action" in our HTML TOC. ShowAll() and HideAll() just take a tagName parameter, build a collection of all elements of that tag (except the first) on the page and then set the style.display property of each to either "block" (show it) or "none" (hide it). The first two functions, document.onclick() and GetChildElem(), work together to show or hide a <UL> whose parent <LI> is clicked. We could have actually written an ONCLICK attribute on each <LI> in our XSL, but like our colleague DHTML Dude, we love the abstraction, power, and flexibility of Internet Explorer's event bubbling.
document.onlclick() binds the onclick() event for the document to this function, so every click on the document gets handled here. For now, we want to do something only if an <LI> with a child <UL> is clicked, so we first check whether the user clicked on an element that has a "clsHasKids" className. Then, doing some preventative exception-handling, we use GetChildElem() to return either a reference to a valid child <UL>, or to return false if none exists. Finally, we use the ternary JScript conditional to set style.display to the complement of its current setting
function GetChildElem(eSrc,sTagName) { var cKids = eSrc.children; for (var i=0;i<cKids.length;i++) { if (sTagName == cKids[i].tagName) return cKids[i]; } return false; } function document.onclick() { var eSrc = window.event.srcElement; if ("clsHasKids" == eSrc.className && (eChild = GetChildElem(eSrc,"UL"))) { eChild.style.display = ("block" == eChild.style.display ? "none" : "block"); } } function ShowAll(sTagName) { var cElems = document.all.tags(sTagName); var iNumElems = cElems.length; for (var i=1;i<iNumElems;i++) cElems[i].style.display = "block"; } function HideAll(sTagName) { var cElems = document.all.tags(sTagName); var iNumElems = cElems.length; for (var i=1;i<iNumElems;i++) cElems[i].style.display = "none"; }
Finally, we use a smattering of CSS to fine-tune the look of our TOC. The CSS in this sample is pretty simple; you could easily do something more complicated (and better-looking), such as our TOCs in the Web Workshop. The only style rules of any consequence here are for <UL> and <LI>. We set the initial display of all <UL> elements that are children of an <LI> to "none", we give a "hand" cursor to those <LI> elements that are of className "clsHasKids" to indicate that clicking it will do something (in this case, show or hide the child <UL>), and we tweak the list-style-type and margins of the <UL> and <LI>.
BODY { font-family:verdana; font-size:70%; } H1 { font-size:120%; font-style:italic; } UL { margin-left:0px; margin-bottom:5px; } LI UL { display:none; margin-left:16px; } LI { font-weight:bold; list-style-type:square; cursor:default; } LI.clsHasKids { list-style-type:none; cursor:hand; } A:link, A:visited, A:active { font-weight:normal; color:navy; } A:hover { text-decoration:none; } BUTTON { font-family:tahoma; font-size:100%; }
As mentioned above, one of the great things about storing your data in XML is that you can easily generate a different view of the same data by transforming it with a different style sheet. The downloadable sample code includes four additional XSL stylesheets (and script and style files): divs.xsl, which renders the TOPICs in visually-hierarchical DIVs, flat.xsl, which renders all TOPICs at the same level, links.xsl, which prints out the actual URLs in a TABLE, and list_pp.xsl, which slightly modifies list.xsl to generate cleaner HTML source.
The sample was written to work in Internet Explorer 5 on the client, making use of the XML mime-type. However, in cross-browser situations, it's really easy to transform the XML on the server and send it down. The code below, which is also in the downloadable .zip file, takes care of that. Just drop the sample files on your Internet Information Server (IIS) server and load webdev.asp, passing an XSL filename as an optional query string parameter; for example, http://<yourmachinename>/codecorner/xml/webdev.asp?xsl=divs.xsl. You can also load the default.asp page, which has a list of links to the various XML/XSL combinations.
<% @LANGUAGE="JScript" %> <% var sXml = "webdev.xml" var sXsl = new String(Request.QueryString("xsl")); if ("undefined" == sXsl) sXsl = "list.xsl" var oXmlDoc = Server.CreateObject("MICROSOFT.XMLDOM"); var oXslDoc = Server.CreateObject("MICROSOFT.XMLDOM"); oXmlDoc.async = false; oXslDoc.async = false; oXmlDoc.load(Server.MapPath(sXml)); oXslDoc.load(Server.MapPath(sXsl)); Response.Write(oXmlDoc.transformNode(oXslDoc)); %>
For more on creating DHTML from XML, or other interesting uses of XML, visit the XML area of the MSDN Online Web Workshop, especially the links highlighted in this sample. And be sure to catch my colleague Charlie Heinemann's regular Extreme XML column in MSDN Online Voices.
George Young is the development lead on Microsoft's MSDN Online site, and previously worked on the Site Builder Network site. In his spare time, he listens to Mexican radio stations over Windows Media Player, and commutes to Redmond, Washington from New Orleans in his Caddy.