Extensible Stylesheet Language (XSL) is a family of languages that are designed to transform XML data. XSL refers to three main languages: XSL Transformations (XSLT), which is a language that transforms XML documents into other XML documents; XPath, which was discussed in the previous section; and XSL Formatting Objects (XSL-FO), which describes how the transformed data should be rendered when presented. Since no browser currently supports XSL-FO, any transformations must be accomplished through the use of XSLT.
XSLT is an XML-based language designed to transform an XML document into another data form. This definition may make XSLT to be a not-so-useful technology, but the truth is far from the matter. The most popular use of XSLT is to transform XML documents into HTML documents, which is precisely what this introduction covers.
XSLT documents are nothing more than specialized XML documents, so they must conform to the same rules as all XML documents: they must contain an XML declaration, they must have a single root element, and they must be well formed.
As an example, you'll once again be using books.xml. The information contained in this file can be transformed into HTML using XSLT, without the need to build the DOM structure manually. For starters, you need an XSLT document, books.xsl, which begins with an XML declaration and a root element:
<?xml version="1.0" encoding=" UTF-8" ?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="html" omit-xml-declaration="yes" indent="yes" /> </xsl:stylesheet>
The document element of an XSLT document is <xsl:stylesheet/>. In this element, the XSL version is specified, and the xsl namespace is declared. This required information determines the behavior of the XSLT processor; without it, an error will be thrown. The xsl prefix is also important, as this allows all XSL directives to be visibly and logically separate from other code in the document.
The <xsl:output/> element defines the format of the resulting output. In this example, the resulting transformation results in HTML data, with the XML declaration omitted, and the elements indented. You can specify the format to be plain text, XML, or HTML data.
Just like any application, a transformation must have an entry point. XSLT is a template-based language, and the processor works on an XML document by matching template rules. In this example, the first element to match is the root of the XML document. This is done by using the <xsl:template/> directive. Directives tell the processor to execute a specific function. The <xsl:template/> directive creates a template that is used when the pattern in the match attribute is matched:
<?xml version="1.0" encoding="UTF-8" ?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="/"> <html> <head> <link rel="stylesheet" type="text/css" href="books.css" /> <title>XSL Transformations</title> </head> <body> <xsl:apply-templates /> </body> </html> </xsl:template> </xsl:stylesheet>
The match attribute takes an XPath expression as its value to select the proper XML node. In this case, it is the root element of books.xml (the XPath expression / always selects the document element). Inside of the template, you'll notice HTML elements. These elements are a part of the transformation's output. Inside of the <body/> element, another XSL directive is found. The <xsl:apply-templates /> element tells the processor to start parsing all templates within the context of the document element, which brings the next template into play:
<?xml version="1.0" encoding="UTF-8" ?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="html" omit-xml-declaration="yes" indent="yes" /> <xsl:template match="/"> <html> <head> <link rel="stylesheet" type="text/css" href="books.css" /> <title>XSL Transformations</title> </head> <body> <xsl:apply-templates /> </body> </html> </xsl:template> <xsl:template match="book"> <div class="bookContainer"> <xsl:variable name="varIsbn" select="@isbn" /> <xsl:variable name="varTitle" select="title" /> <img class="bookCover" alt="{$varTitle}" src="{$varIsbn}.png" /> <div class="bookContent"> <h3><xsl:value-of select="$varTitle" /></h3> Written by: <xsl:value-of select="author" /><br /> ISBN #<xsl:value-of select="$varIsbn" /> <div class="bookPublisher"><xsl:value-of select="publisher" /></div> </div> </div> </xsl:template> </xsl:stylesheet>
This new template matches all <book/> elements, so when the processor reaches each <book/> in the XML document, this template is used. The first two XSL directives in this template are <xsl:variable/>, which define variables.
Variables in XSL are primarily used in XPath expressions or attributes (where elements cannot be used without breaking XML syntax). The <xsl:variable/> element has two attributes: name and select. As you may have guessed, the name attribute sets the name of the variable. The select attribute specifies an XPath expression and stores the matching value in the variable. After the initial declaration, variables are referenced to with the $ sign (so the variable defined as varIsbn is later referenced as $varIsbn).
The first variable, $varIsbn, is assigned the value of the <book/> element's isbn attribute. The second, $varTitle, is assigned the value of the <title/> element. These two pieces of information are used in the attributes of the HTML <img/> element. To output variables in attributes, you surround the variable name in braces:
<img class="bookCover" alt="{$title}" src="{$isbn}.png" />
Without the braces, the output would use the string literals "$varTitle" and "$varIsbn" instead.
Important |
Using variables in attributes of XSL directives, like select, are the exception to this rule. Using curly braces in these types of attributes will cause an error, and the document transformation will fail. |
The remainder of XSL directives in this example are <xsl:value-of/> elements. These elements retrieve the value of the matched variable or node according to the select attribute. The select attribute behaves in the same way as the select attributes of <xsl:variable/> do: they take an XPath expression and select the node or variable that matches that expression. The first instance of <xsl:value-of/> in this template references the $varTitle variable (notice the lack of braces), so the value of the variable is used. Next, the value of the <author/> element is used; the same with $varTitle and <publisher/>.
In order for an XML document to transform in the browser, it must have a stylesheet specified. In books.xml, add the following line immediately after the XML declaration:
<?xml-stylesheet type="text/xsl" href="books.xsl"?>
This tells the XML processor to apply the stylesheet books.xsl to this document. Viewing this modified XML document in a web browser will no longer show the XML structure, but it will show the resulting transformation to HTML. However, using this directive won't work through JavaScript. For that, you'll need to use some special objects.
There are two ways to transform an XML document in IE, both of which require the use of MSXML. Starting with version 3.0, MSXML has full support for XSLT 1.0. If you don't have Windows XP or IE 6, it is time to upgrade. You can find the latest MSXML downloads at http://msdn.microsoft.com/XML/XMLDownloads/.
The first and easiest method loads both the XML and XSLT documents into separate XML DOM objects:
var oXmlDom = zXmlDom.createDocument(); var oXslDom = zXmlDom.createDocument(); oXmlDom.async = false; oXslDom.async = false; oXmlDom.load("books.xml"); oXslDom.load("books.xsl");
When both documents are loaded, you call the transformNode() method to start the transformation:
var sResults = oXmlDom.transformNode(oXslDom);
The transformNode() method takes an XML DOM object as an argument (in this case, the XSL document) and returns the transformed data as a string. But you don't have to call transformNode() at the document level; it can be called from any element in the XML document:
var sResults = oXmlDom.documentElement.firstChild.transformNode(oXslDom);
The transformNode() method will transform only the element it was called from and its children. In this example, the first <book/> element is transformed, as shown in Figure 4-3. This is because you transformed only one node by calling transformNode() on an element with no children.
The second method of transformations in IE is a bit more involved, but it also gives you more control and features. This process involves creating multiple objects in the MSXML library. The first step in this somewhat lengthy process is to create a thread-safe XML DOM object, which the XSL stylesheet is loaded into:
var oXmlDom = zXmlDom.createDocument(); oXmlDom.async = false; oXmlDom.load("books.xml"); var oXslDom = new ActiveXObject("Msxml2.FreeThreadedDOMDocument.3.0"); oXslDom.async = false; oXslDom.load("books.xsl");
The FreeThreadedDOMDocument class is yet another ActiveX class and a part of the MSXML library. You must use the FreeThreadedDomDocument class to create XSLTemplate objects, which this example does (the next example shows the creation of a XSLTemplate object). In early versions of MSXML, every call to the transformNode() method forced a recompile of the XSL stylesheet slowing the transformation process considerably. With a FreeThreadedDOMDocument, the compiled stylesheet is cached and ready to use until it's removed from memory.
After the XML DOM object creation, you must create another ActiveX object, an XSL template:
The XSLTemplate class is used to cache XSL stylesheets and create an XSLProcessor; so after the template is created, the XSL document is assigned to the XSLTemplate class's stylesheet property, which caches and loads the XSL stylesheet.
The next step in this process is to create an XSLProcessor, which is created by calling the createProcessor() method of the XSLTemplate class:
var oXslProcessor = oXslTemplate.createProcessor(); oXslProcessor.input = oXmlDom;
After creation of the processor, its input property is assigned oXmlDom, the XML DOM object containing the XML document to transform. At this point, everything the processor requires is in place, so all that remains is the actual transformation and the retrieval of the output:
oXslProcessor.transform(); document.body.innerHTML = oXslProcessor.output;
Unlike transformNodes(), the transform() method does not return the resulting output as a string. To retrieve the output of the transformation, use the output property of the XSLProcessor object. This entire process requires much more coding than the transformNode() method and yields the same result. So why use this process?
MSXML provides a few extra methods that can be used in these transformations. The first is addObject(). This method adds a JavaScript object to the stylesheet, and can even call methods and output property values in the transformed document. Consider the following object:
var oBook = { propertyOne : "My Current Books", methodOne : function () { alert("Welcome to my Book List"); return ""; } };
What if you wanted to use this information in the transformation? Using the addObject() method, you can pass this information into the XSLT stylesheet passing in two arguments: the oBook object and a namespace URI to identify it. So, to add this object with a namespace URI of "http://my-object", you could do the following:
var oXmlDom = zXmlDom.createDocument(); oXmlDom.async = false; oXmlDom.load("books.xml"); var oXslDom = new ActiveXObject("Msxml2.FreeThreadedDOMDocument.3.0"); oXslDom.async = false; oXslDom.load("books.xsl"); var oXslTemplate = new ActiveXObject("Msxml2.XSLTemplate.3.0"); oXslTemplate.stylesheet = oXslDom; var oXslProcessor = oXslTemplate.createProcessor(); oXslProcessor.input = oXmlDom; oXslProcessor.addObject(oBook, "http://my-object"); oXslProcessor.transform(); document.body.innerHTML = oXslProcessor.output;
The oBook object is now passed to the XSLProcessor, meaning that the XSLT stylesheet can use it. Now the XSL document must be changed to look for this object and use its information. The first requirement is to add a new namespace to the root element, <xsl:stylesheet/>. This namespace will match the one used in addObject():
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:bookObj="http://my-object">
The prefix bookObj will be used to access this information. Now that the namespace and prefix are ready to go, some <xsl:value-of/> elements should be added to the document to retrieve the object's members:
<xsl:template match="/"> <html> <head> <link rel="stylesheet" type="text/css" href="books.css" /> </head> <body> <xsl:value-of select="bookObj:methodOne()" /> <div align="center"> <b><xsl:value-of select="bookObj:get-propertyOne()" /></b> </div> <xsl:apply-templates /> </body> </html> </xsl:template>
Remember that the <xsl:value-of/> XSL directive retrieves the value of an element, or in this case, an object. The first <xsl:value-of/> directive retrieves (or calls) methodOne(), which sends an alert welcoming the user to the page. The second <xsl:value-of/> directive is similar to the first, except that it retrieves the value of the propertyOne property of the oBook object. When the transformed output is displayed in the browser, the user will see the phrase My Current Books at the top of the page.
Important |
When using an object in transformations, all properties and methods must return a value that the XSLProcessor can understand. String, number, and Boolean values all work as expected; returning any other value will throw a JavaScript error when the transformation executes. |
The next useful feature of the XSLProcessor is the addParameter() method. Unlike sending an object into a transformation, parameters are a standard part of XSLT. Parameters are passed to the XSL stylesheet and used as variables. To specify a parameter, pass the name and its value, like this:
var oXslProcessor = oXslTemplate.createProcessor();
oXslProcessor.input = oXmlDom;
oXslProcessor.addParameter("message", "My Book List");
This code adds the "message" parameter to the XSLProcessor. When the XSL transformation executes, the processor uses the value of the parameter, "My Book List", and places it in the according location. Parameters in XSL use the <xsl:param/> directive:
<xsl:param name="message" />
Notice that the name attribute matches the name passed in addParameter(). This parameter receives the value "My Book List" which is retrieved using the variable syntax you learned earlier:
<xsl:value-of select="$message" />
In this example, the <xsl:value-of/> directive retrieves the parameters value. The updated XSL stylesheet would look like this:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:param name="message" /> <xsl:template match="/"> <html> <head> <link rel="stylesheet" type="text/css" href="books.css" /> </head> <body> <xsl:value-of select="$message" /> <xsl:apply-templates /> </body> </html> </xsl:template>
The updated stylesheet adds two new lines of code. The first is the addition of the <xsl:param/> directive, and the second is the <xsl:value-of/> directive that retrieves the value of the parameter. Parameter declarations can exist anywhere in the XSL document. This code shows the parameter declaration at the top of the document, but you are not limited to this location.
One final feature of using an XSLProcessor is its speed; it compiles the XSL stylesheet, so subsequent transformations using the same stylesheet result in faster transformations (compared to using transformNodes()). To do this, use the reset() method of the XSLProcessor object. This method clears the input and output properties but not the stylesheet property. This readies the processor for the next transformation with the same stylesheet.
Like XML and XPath, the Firefox implementation of XSLT transformations varies from the IE implementation. Firefox does implement an XSLTProcessor class to perform transformations, but the similarities end there.
The first step in performing a transformation in Firefox is to load the XML and XSL documents into a DOM object:
var oXmlDom = zXmlDom.createDocument(); var oXslDom = zXmlDom.createDocument(); oXmlDom.async = false; oXslDom.async = false; oXmlDom.load("books.xml"); oXslDom.load("books.xsl");
The XSLTProcessor class exposes the importStylesheet() method, which takes an XML DOM object containing the XSLT document as an argument:
var oXsltProcessor = new XSLTProcessor(); oXsltProcessor.importStylesheet(oXslDom);
Last, the transformation methods are called. There are two of these methods: transformToDocument() and transformToFragment(). The transformToDocument() method takes an XML DOM object as an argument and returns a new XML DOM document containing the transformation. Normally, this is the method you want to use:
var oNewDom = oXsltProcessor.transformToDocument(oXmlDom);
The resulting DOM object can be used like any other XML DOM object. You can select certain nodes with XPath, traverse the node structure with properties and methods, or even use it in another transformation.
The transformToFragment() method returns a document fragment, as its name suggests, to append to another DOM document. This method takes two arguments: the first is the XML DOM object you want to transform, and the second is the DOM object you intend to append the result to:
var oFragment = oXsltProcessor.transformToFragment(oXmlDom, document); document.body.appendChild(oFragment);
In this example, the resulting document fragment is appended to the body of the document object. Note that you can append the resulting fragment to any node within the DOM object passed to the transformToFragment() method.
But what if you wanted a string returned as the result of transformation like the transformNode() method implemented by Microsoft? You could use the XMLSerializer class you learned of earlier. Just pass the transformation result to the serializeToString() method:
var oSerializer = new XMLSerializer();
var str = oSerializer.serializeToString(oNewDom);
If using the zXml library, this is simplified by using the xml property:
var str = oFragment.xml;
The XSLTProcessor class also enables you to set parameters to pass to the XSL stylesheet. The setParameter() method facilitates this functionality; it accepts three arguments: the namespace URI, the parameter name, and the value to assign the parameter. For example:
oXsltProcessor.importStylesheet(oXslDom);
oXsltProcessor.setParameter(null, "message", "My Book List");
var oNewDom = oXsltProcessor.transformToDocument(oXmlDom);
In this example, the parameter message is assigned the value "My Book List". The value of null is passed for the namespace URI, which allows the parameter to be used without having to specify a prefix and corresponding namespace URI in the stylesheet:
<xsl:param name=" message" />
The setParameter() method must be called before the calling of transformToDocument() or transformToFragment(), or else the parameter value will not be used in the transformation.
In the previous sections, you've seen how the zXml library makes handling XML data across both main platforms easier. Now you will use the library to perform XSLT transformations. There is only one method for XSLT in the library: transformToText(). This method, which returns text from a transformation, takes two arguments: the XML document to transform and the XSL document:
var sResult = zXslt.transformToText(oXmlDom, oXslDom);
As the name of the method suggests, the returned result is a string. You can then add the result of the transformation (sResult) to an HTML document:
var oDiv = document.getElementById("transformedData"); oDiv.innerHTML = sResult;
This is perhaps the simplest object in the zXml library.
Imagine once again that you run an online bookstore. Your visitors like the Best Picks feature you implemented, but you start to receive feedback that they want the picks of the previous week as well. You decide to roll with an Ajax solution.
Using XMLHttp, the browser retrieves the book list and the request's responseText is loaded into an XML DOM object. The stylesheet also is loaded into its own XML DOM object, and the XML data from the book list is transformed into HTML, which is then written to the page. To provide some usability, you provide a link in the upper right-hand corner to change from one list to another.
The first step in this solution is to retrieve the XML file with XMLHttp. This is the beginning of the code and the entry point for the mini application, so you'll encapsulate the code in a function called init():
function init(sFilename) { var oReq = zXmlHttp.createRequest(); oReq.onreadystatechange = function () { if (oReq.readyState == 4) { // only if "OK" if (oReq.status == 200) { transformXml(oReq.responseText); } } }; oReq.open("GET", sFilename, true); oReq.send(); }
The init() function accepts one argument: the file name of the XML file to load. For cross-browser compatibility (not to mention easier coding for you) you create an XMLHttp object using the zXml library. This is an asynchronous request, so the readyState property must be checked using the onreadystatechange event handler. When the request returns as OK, the responseText is sent to the transformXml() function:
function transformXml(sResponseText) { var oXmlDom = zXmlDom.createDocument(); oXmlDom.async = false; oXmlDom.loadXML(sResponseText); var oXslDom = zXmlDom.createDocument(); oXslDom.async = false; oXslDom.load("books.xsl"); var str = zXslt.transformToText(oXmlDom,oXslDom); document.getElementById("divBookList").innerHTML = str; }
Calling transformXml() loads the passed response text into an XML DOM object using the loadXML() method. The XSL stylesheet is also loaded, and both objects are passed to the transformToText() method in the zXml library. The transformation's result, a string, is then added to an element in the document via the innerHTML property. As a result of this function, this week's book list is visible to the user.
A good portion of the code is written, but you still lack the list-changing feature. To facilitate this ability, another function needs writing, but first, the application needs to know what list to load as the user clicks the link. This is easily handled by Boolean variable called bIsThisWeek. When this week's book list is loaded, bIsThisWeek becomes true, otherwise it's false. Since this week's list is already loaded, bIsThisWeek is set to true:
var bIsThisWeek = true;
The link that the user clicks to change the list uses the onclick event, so the next function will handle that event:
function changeList() { var aChanger = document.getElementById("aChanger"); if (bIsThisWeek) { aChanger.innerHTML = "This Week's Picks"; init("lastweekbooks.xml"); bIsThisWeek = false; } else { aChanger.innerHTML = "Last Week's Picks"; init("thisweekbooks.xml"); bIsThisWeek = true; } return false; }
In this code, the link (aChanger) is retrieved with the getElementById() method. The variable bIsThisWeek is checked. According to its value, the proper list is loaded by sending the file name to the init() function. This retrieves the new list, transforms the data, and writes it to the page. Also, note that the link text changes to cue users of what happens the next time they click the link. The bIsThisWeek variable also changes so that the correct list is loaded the next time the user clicks the link. Last, the function returns false. Since this function is an event handler for a link, returning any other value would cause the link to behave as a link and could take the user away from the application.
Finally, you can complete the mini application with the HTML, and here is the entire document:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="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"> function init(sFilename) { var oReq = zXmlHttp.createRequest(); oReq.onreadystatechange = function () { if (oReq.readyState == 4) { // only if "OK" if (oReq.status == 200) { transformXml(oReq.responseText); } } }; oReq.open("GET", sFilename, true); oReq.send(); } function transformXml(sResponseText) { var oXmlDom = zXmlDom.createDocument(); oXmlDom.async = false; oXmlDom.loadXML(sResponseText); var oXslDom = zXmlDom.createDocument(); oXslDom.async = false; oXslDom.load("books.xsl"); var str = zXslt.transformToText(oXmlDom,oXslDom); document.getElementById("divBookList").innerHTML = str; } var bIsThisWeek = true; function changeList() { var aChanger = document.getElementById("aChanger"); if (bIsThisWeek) { aChanger.innerHTML = "This Week's Picks"; init("lastweekbooks.xml"); bIsThisWeek = false; } else { aChanger.innerHTML = "Last Week's Picks"; init("thisweekbooks.xml"); bIsThisWeek = true; } return false; } </script> </head> <body onload=" init('thisweekbooks.xml')"> <a id=" aChanger" href="#" onclick=" changeList();">Last Week's Picks</a> <div id=" divBookList"></div> </body> </html>
To run this mini application, you must run it from a web server because XMLHttp is used. Any web server software will work fine. Just place this HTML file, the zXml library, and the CSS file into a directory called booklists on your web server. Then fire up your browser and point it to http://localhost/booklists/book.htm.