|
Introduction
Welcome to Advanced UI Design Using XML and XSL. Each article in my series will demonstrate the creation of a specific user interface (UI) component using eXtensible Markup Language (XML) and eXtensible Stylesheet Language (XSL).
This article expands on a previous article in this series, Folder Tree Creation (see http://www.15seconds.com/issue/010921.htm). The folder tree discussed in this article illustrates how to insert, modify, delete, and rename items within the tree. All operations are performed on the client, thus reducing the load on your server. The client used in this article is Internet Explorer 5.5+.
The actual folder tree demonstrated is available in the Download section below.
Folder Tree Updates
Since the last article, I have made the following updates.
- Automated IDs
- Unlimited Metadata Support
- Insert/Update Capability
- Rename Capability
- Delete Capability
Automated IDs
Each entity within our tree requires a unique ID. In the previous article the task of assigning these ID's was left to the user. In this article I have automated this task at runtime.
In this version of the folder tree, unique IDs are assigned via an XSL transformation immediately before rendering the tree.
<xsl:stylesheet version="1.1"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:dt="urn:schemas-microsoft-com:datatypes">
<xsl:template match="/tree">
<xsl:element name="tree">
<xsl:apply-templates select="entity"/>
</xsl:element>
</xsl:template>
<xsl:template match="entity">
<xsl:element name="entity">
<xsl:attribute name="id">
<xsl:value-of select="generate-id(.)"/>
</xsl:attribute>
<xsl:for-each select="*">
<xsl:if test="name() = 'contents'">
<xsl:element name="contents">
<xsl:apply-templates select="entity"/>
</xsl:element>
</xsl:if>
<xsl:if test="name() != 'contents'">
<xsl:element name="{name()}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:if>
</xsl:for-each>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
The style sheet traverses our tree.xml file and uses the generate-id() method to assign a unique ID to each individual entity.
We should note that if this were an n-tier application and our entities were being pulled out of a database, we would not need this process because each entity representing a row in our database would already have a unique identity field.
Unlimited Metadata Support
All tree operations allow for descriptive metadata specific to your requirements. This means elements of any name and value can be added to describe the entity.
For example, below is customer XML from the last article. Also, you'll notice the difference between the two is the customer-specific metadata, such as contact, address and phone. My folder tree will automatically read and interpret this new metadata. Also note that due to "image" being a reserved word I was required to change the "image" entities name to "imageBase".
<entity id="e2">
<description>Microsoft</description>
<imageBase>images/book.gif</imageBase>
<imageOpen>images/bookOpen.gif</imageOpen>
<onClick>displayCustomer(12345)</onClick>
<onContextMenu>context/contextCustomer.xml</onContextMenu>
<contents>
</contents>
</entity>
Below is sample customer XML from this article.
<entity id="e2">
<description>Microsoft</description>
<imageBase>images/book.gif</imageBase>
<imageOpen>images/bookOpen.gif</imageOpen>
<onClick>displayCustomer(12345)</onClick>
<onContextMenu>context/contextCustomer.xml</onContextMenu>
<contact>Bill Gates</contact>
<address1>1234 Microsoft Dr.</address>
<address2>Suite 123</address>
<city>Microsoft City</city>
<state>MS</state>
<zip>12345</zip>
<phone>(123)132-1234</phone>
<contents>
</contents>
</entity>
You'll notice the difference between the two is the customer-specific metadata, such as contact, address, and phone. My folder tree will automatically read and interpret this new metadata.
With the current folder tree architecture, child entities inherit from their parents. For instance, when you right-click on the Customer Folder and select Insert, you will get a child with all the metadata that the Customer Folder has.
Insert/Update Capability
Inserting and updating are very similar because both operations deal with the exact same set of data and use the exact same form. The exceptions are an insert form initializing blank, meaning with empty input elements, and an update form initializing with values. Because of these similarities I was able to combine both insert and update functionality in one XSLT style sheet and one display function, and then separate operational calls
Insert Update XSLT Forms
The actual XSLT style sheet that creates our forms follows.
<xsl:stylesheet version="1.1"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:dt="urn:schemas-microsoft-com:datatypes">
<xsl:param name="action"/>
<xsl:param name="selectedEntity"/>
<xsl:template match="entity">
<TABLE BORDER="0" CELLSPACING="0" CELLPADDING="1" WIDTH="100%">
<xsl:for-each select="*">
<xsl:if test="name() != 'contents'">
<TR>
<TD CLASS="dataLabel" STYLE="border-right: 1px solid black;
border-bottom: 1px solid black;">
<xsl:value-of select="name()"/>
</TD>
<TD STYLE="border-right: 1px solid black; border-bottom: 1px solid black;" WIDTH="100%">
<INPUT CLASS="dataInput"
ONFOCUS="document.body.onselectstart = null"
ONBLUR="document.body.onselectstart = returnFalse;">
<xsl:attribute name="NAME">
<xsl:value-of select="name()"/>
</xsl:attribute>
<xsl:if test="$action = 'update'">
<xsl:attribute name="VALUE">
<xsl:value-of select="."/>
</xsl:attribute>
</xsl:if>
</INPUT>
</TD>
</TR>
</xsl:if>
</xsl:for-each>
<TR>
<TD STYLE="padding-right: 0px;"/>
<TD STYLE="padding-top: 0px;padding-left: 0px;">
<TABLE BORDER="0" CELLSPACING="0" CELLPADDING="0">
<TR>
<TD>
<INPUT TYPE="button" CLASS="buttonOff" NAME="Save" VALUE="Save" ONFOCUS="this.blur();"
ONMOUSEOVER="swapClass(this, 'buttonOver')" ONMOUSEOUT="swapClass(this, 'buttonOff')">
<xsl:attribute name="ONCLICK">
<xsl:value-of select="$action"/>Entity('<xsl:value-of select="$selectedEntity"/>')
</xsl:attribute>
</INPUT>
</TD>
<TD STYLE="padding-left: 2px;">
<INPUT TYPE="button" CLASS="buttonOff" NAME="Cancel" VALUE="Cancel"
ONFOCUS="this.blur();" ONMOUSEOVER="swapClass(this, 'buttonOver')"
ONMOUSEOUT="swapClass(this, 'buttonOff')" ONCLICK="content.innerHTML = '';"/>
</TD>
</TR>
</TABLE>
</TD>
</TR>
</TABLE>
</xsl:template>
</xsl:stylesheet>
This XSLT style sheet accepts two parameters called "action" and "selectedEntity". Valid values for the action parameter are "insert" and "update". The selectedEntity parameter takes an entity ID value.

Figure 1. Resulting XSLT Transformation
Insert/Update Display Function
This function receives an action parameter of value "insert" or "update." It then uses the selectedEntity variable to figure out which entity the insert or update will be performed on. It uses the above XSLT style sheet to render the appropriate form.
function insertUpdateDisplay(action) {
var xslDoc
var xslTemplate;
var xslProc;
var entity;
xslDoc = new ActiveXObject('MSXML2.FreeThreadedDOMDocument')
xslDoc.async = false;
xslTemplate = new ActiveXObject('MSXML2.XSLTemplate')
xslDoc.load("admin/insertUpdate.xslt");
xslTemplate.stylesheet = xslDoc;
xslProc = xslTemplate.createProcessor();
entity = xmlDoc.documentElement.selectSingleNode("//entity[@id='" + selectedEntity +"']");
xslProc.input = entity;
xslProc.addParameter("action", action);
xslProc.addParameter("selectedEntity", selectedEntity);
xslProc.transform();
content.innerHTML = xslProc.output;
}
Insert Operation
This function receives the entity ID of the "parent to be." The function then creates a replica of the parent entity. Once the child is created, it is appended to the parent's contents collection.
function insertEntity(parentEntityID) {
var entity;
var newEntity;
var element;
var attribute;
var xslDoc;
var i;
xslDoc = new ActiveXObject('MSXML2.FreeThreadedDOMDocument')
xslDoc.async = false;
xslDoc.load("admin/tree.xslt");
entity = xmlDoc.documentElement.selectSingleNode("//entity[@id='" + parentEntityID +"']");
newEntity = xmlDoc.createElement("entity");
attribute = xmlDoc.createAttribute("id");
attribute.text = document.uniqueID;
newEntity.attributes.setNamedItem(attribute);
for(i=0; i < entity.childNodes.length; i++) {
element = xmlDoc.createElement(entity.childNodes(i).baseName);
if(entity.childNodes(i).baseName != "contents") {
element.text = eval(entity.childNodes(i).baseName + ".value")
}
newEntity.appendChild(element)
}
entity.selectSingleNode("contents").appendChild(newEntity);
document.all[parentEntityID].insertAdjacentHTML("beforeEnd", newEntity.transformNode(xslDoc));
document.all[parentEntityID].lastChild.style.display = "block";
if(document.all[parentEntityID].open == "false") {
document.all[parentEntityID].onclick();
}
saveXML();
}
Update Operation
This function receives the ID of the entity that needs updating. The function then loops through the entities while updating values pulled from the current form. Once the entity XML is updated, it is retransformed and swapped with the existing node in the browser DOM.
function updateEntity(entityID) {
var entity;
var xslDoc;
var container;
var i;
xslDoc = new ActiveXObject('MSXML2.FreeThreadedDOMDocument')
xslDoc.async = false;
xslDoc.load("admin/tree.xslt");
entity = xmlDoc.documentElement.selectSingleNode("//entity[@id='" + entityID +"']");
for(i=0; i < entity.childNodes.length; i++) {
if(entity.childNodes(i).baseName != "contents") {
entity.childNodes(i).text = eval(entity.childNodes(i).baseName + ".value")
}
}
container = document.createElement("DIV");
container.innerHTML = entity.transformNode(xslDoc)
container.childNodes(0).style.display = "block";
document.all[entityID].swapNode(container.childNodes(0));
container = null;
saveXML();
}
Rename Capability
When a user selects the rename option of an entity the renameEntityBegin() function is called. It then uses the selectedEntity variable to determine which entity is to be renamed.
The method first gains a reference called "name" to the browser object that displays the current name of the entity. Once this reference is established, we then set the contentEditable property to true. This allows the user to freely type within this space.
We also set the focus to this element, which moves the cursor to the beginning of the name. After setting several events that restrict keystrokes and allow selecting of text, the user is ready to rename the entity.
function renameEntityBegin() {
var name;
name = document.all[selectedEntity + "name"]
name.contentEditable = true;
name.focus();
name.style.cursor = "text";
name.onkeypress = function anonymous() { renameKeyPress(selectedEntity) };
name.onblur = function anonymous() { renameEntityEnd(selectedEntity) };
name.onselectstart = null;
document.all[selectedEntity].onclick = null;
}
If the user presses the Enter key or fires the onBlur event, the renameEntityEnd method is called.
This method receives the ID of the entity that is to be renamed. It makes the appropriate changes to both the XML Document Object Model (DOM) and the browser DOM.
function renameEntityEnd(entityID) {
var entity;
var name;
entity = xmlDoc.documentElement.selectSingleNode("//entity[@id='" + entityID +"']");
name = document.all[entityID + "name"]
name.style.cursor = "hand";
name.contentEditable = false;
name.onselectstart = function anonymous() { return false };
entity.selectSingleNode("description").text = name.innerText;
document.all[entityID].onclick = function anonymous() { clickOnEntity(document.all[entityID]) };
document.body.onselectstart = returnFalse;
saveXML();
}

Figure 2. Renaming an Entity
Delete Capability
The delete entity receives the ID of the entity that is to be deleted. The function first checks to make sure the entity does not contain children. If the entity contains children, an error will be displayed.
After performing this check, this function then removes the entities node in both the XML DOM and the browser DOM.
function deleteEntity() {
var entity;
entity = xmlDoc.documentElement.selectSingleNode("//entity[@id='" + selectedEntity +"']");
if(entity.selectSingleNode("contents").childNodes.length > 0) {
displayError("You cannot remove an entity that contains children. First remove all children."
., 8000);
}
else {
entity = entity.parentNode.removeChild(entity)
document.all[selectedEntity].removeNode(true)
saveXML();
}
}
Redirecting the User
The version of the tree available for download within this article includes a simple redirect method. This method receives a URL parameter of type string.
You must call the redirect method from within your tree XML file. Figure 3 highlights the location of the onClick element used to call the redirect method.

Figure 3. Location of onClick Element
Figure 4 highlights the line within tree.xslt that inserts your onClick event during transformation.

Figure 4 Inserting the onClick Event
Interfacing with a Database
If you are required to interface with a database, I can make the following recommendations. Use the XMLHTTP object within the init(), insertEntity(), updateEntity(), deleteEntity(), and renameEntityEnd() methods. These methods should contact your Web service and retrieve or send only the XML that is required to perform the requested operation.
This approach will reduce load on your server tremendously. Instead of having to do a round trip back to the server, request the whole tree, and then render the whole tree again, you can make one easy behind-the-scenes call that updates your data, then updates only the area of the client browser that is required for your operation.
Closing
I hope the contents of this article will help increase the quality of your Web applications interface. If you have any questions, comments, or suggestions, please feel free to email me at the address listed in the author section.
Demo and Download
View a live demo of the folder tree (Works only in IE 5.5+ browsers).
Download the newest version of the folder tree by clicking the example.zip hyperlink. This version includes both administration and viewing versions of the folder tree.
Other Articles in This Series
Advanced UI Design
Part 1 -- Folder Tree Creation
http://www.15seconds.com/Issue/010921.htm
Part 2 -- Custom Context Menu Creation
http://www.15seconds.com/issue/010927.htm
Part 4 - Folder Tree Drag and Drop
http://www.15seconds.com/issue/011129.htm
Part 5 - Progress Indicator Creations
http://www.15seconds.com/issue/011212.htm
Part 6 -- Progress Indicator Usage
http://www.15seconds.com/issue/020109.htm
Part 7 -- Relational Folder Tree Lines
http://www.15seconds.com/issue/020424.htm
About the Author
Joe Slovinski has been programming Web applications since 1993. For more information on the code in this article, contact the Joe Slovinski.
Credits
On a side note, I would like to thank Lee McGraw and Richard Spencer for helping me proof read and validate this document.
|