Page 1 of 1

XML Tree View The idea, obstacles and workarounds Rate Topic: -----

#1 ahmad_511  Icon User is offline

  • MSX
  • member icon

Reputation: 131
  • View blog
  • Posts: 722
  • Joined: 28-April 07

Posted 27 April 2010 - 12:36 PM

XML Tree View

Hello and welcome to this tutorial,
Here I will try to explain one of the ideas behind building an XML driven tree view and how to add some functionalities (firing events / reading attributes), also, how to get rid of some problems we usually face regarding the xml structure and variables scope.

Before you start:

This tutorial requires the following skills:
- Javascript, Intermediate.
- HTML, Intermediate.
- DOM, Intermediate.
- XML, Basics.
- Ajax, Basics.
- CSS, Basics.

Note: if you donít like to go through this and you just want to use it as a snippet, you can download attached files and refer to the Testing step for some instructions.

The Idea:

First thing came in to my mind was to use the unordered list elements because of the way it looks, but after a while I realized that any block-level element can work just fine, so I decided to use nested paragraphs elements with a little help from CSS to add left padding which gives the nested appearance.
The idea Iím working on is to fill the tree recursively, itís very simple when you know what youíre doing and why youíre doing it this way.
Anyways, we will read the xml nodes one by one, create/attach necessary HTML elements/ events / styles, after that we check if this elements has children, if so we call the same function again to build the tree based on the current html element and xml node.
So, basically the function must be able to process any node we pass.
We will end up with something like this:
Attached Image
Each parent/item is a paragraph,
Each paragraph contains two spans, one for the icon and the other for the display text.
Paragraphs may be contained in in each other.

Lab:
Due to the security applied on XMLHttpRequest, Local host / web host is required for testing.
Prepare your favorite text editor,
3 images 16x16 to represents (a parent node ďclosed bookĒ, opened parent node ďopened bookĒ, and an item within a parent ďfileĒ)
We will deal with 3 file types (XML as the data source, Javascript the core script, CSS to control the display and the html file for testing).
This code was tested on IE 8 standard mode, FF 3.6.3 and Chrome 5.0.375.17.

All needed files are attached, so no need to bother copying / pasting.

Ready?

XML file structure:
- It a basic xml file nothing special, only one attribute (text) is needed to be used as the node display text.
- Node names are just used to facilitate the human reading. (Unless you want to use them with something different, this requires a small modification to the code).
- It doesnít matter how much nested youíre tree is.
- All text nodes will be ignored.
- White spaces between opening and closing tags considered as text nodes in some browsers, and this will make the script think that this has children tags inside and it will look as a parent element in this case.

Letís start:

Letís first create the main function xmlTree() with its properties as following:

Main properties:
url: (string) the xml file url to be loaded.
parameters: (string, name=value pairs in URI format) parameters that you may want to pass to the xml file generator (in case of using server-side script to generate the xml).
container: (HTML element) the HTML element that will contain the Tree, the default Is the documentís body.

Event properties:
onParentClick: (function) function to handle a parent click event.
onItemClick: (function) function to handle an item click event.
onParentDblClick: (function) function to handle a parent double click event.
onItemDblClick: (function) function to handle an item double click event.

Style properties:
parentStyle, itemStyle: (string) CSS class name to be applied to the parents/items paragraph elements
parentIconStyle, parentOpenedIconStyle,itemIconStyle: (string) CSS class name to be applied to the icon span inside each paragraph.
parentSelectedTextStyle, itemSelectedTextStyle: (string) CSS class name to be applied to the selected parent/item text span inside each paragraph.
parentTextStyle, itemTextStyle: (string) CSS class name to be applied to the parent/item text span inside each paragraph.

Reference properties:
selectedNode: (Object contains a reference to the HTML element and a parent indicator as Boolean ) holds the last selected parent/item (for internal/external use).
Xml: (XML object) holds the loaded xml file (you can use it to pass an already exist xml document without calling build() function).

function xmlTree(){

// main properties
this.url=null;
this.parameters=null;
this.container=null;

// events handlers
this.onParentClick=null;
this.onItemClick=null;

this.onParentDblClick=null;
this.onItemDblClick=null;

// style properties
this.parentStyle="";
this.itemStyle="";

this.parentIconStyle="";
this.parentOpenedIconStyle="";
this.itemIconStyle="";

this.parentSelectedTextStyle="";
this.itemSelectedTextStyle="";

this.parentTextStyle="";
this.itemTextStyle="";

// reference to the last selected node
this.selectedNode=null;

// reference to the loaded xml document
this.xml=null;



Letís declare the magic property(me).
var me=this;



It looks weird!. Why do we need such thing?
In fact, this variable will be used when (this) statement will refer to something other than the function xmlTree().
Where?
Inside an event function, (this) statement will refer to the object that triggers the event, and in such situation we cannot access the xmlTree() properties using it, it just a variable scope issue.


Build() function:
This function uses Ajax to load an external XML file; it will do some validations and error checking,
Iím not going to add details about how Ajax works since itís not our objective, but all what we want to know is that after successful request, Ajax will supply us an xml object, and we will send this object to refresh() function.
Here is the code; I added few comments where I thought it may be useful.

// send a request to load the xml document and call buildTree() when loaded
this.build=function(){
	
	var xmlhttp;
	// check if Ajax Is supported
	try{xmlhttp=new ActiveXObject("Msxml2.XMLHTTP")}
	catch(e){
		try{xmlhttp=new ActiveXObject("Microsoft.XMLHTTP")}
		catch(e){
			try{xmlhttp=new XMLHttpRequest()}
			catch(e){
				alert("Your Browser Does Not Support XMLHTTP");
				return false;
			}
		}
	}

	// check if url property is initiated.
	if (me.url==null){alert("url property must be set");return false;}

// check for successful load
	xmlhttp.onreadystatechange=function(){
			// document is ready
			if (xmlhttp.readyState == 4){
				// xml cannot be loaded
			 	if(xmlhttp.status!=200){
					alert(xmlhttp.status+", "+xmlhttp.statusText);
					return false;
				}
			
				// xml is loaded, call builTree() function
				// some browsers don't recognize the xml if read as plain text, so we need to override the mimeType
				if(xmlhttp.overrideMimeType)xmlhttp.overrideMimeType('text/xml');
				me.xml=xmlhttp.responseXML;
				me.refresh();
				return false;
		}
	}

	// add the parameters if exist, and send the request using GET method
	var furl=me.url+"?"+((me.parameters==null?"":me.parameters+"&")+"rnd="+Math.random())
	xmlhttp.open("GET",furl,true)
	xmlhttp.send(null);

} 



refresh() function:
Itís very useful when we need to update the xml for an already built tree, so it will reset some properties/functions then it passes the buildTree() function two parameters: (me.xml, me.container).

// refresh
this.refresh=function(){
// set the default container if not specified
if(me.container==null)me.container=document.body;

// clear any existing contents
me.container.innerHTML="";

// disallow text selection
me.container.onselectstart=function(){return false};
me.container.onmousedown=function(){return false};

// reset the last selected node referrer
me.selectedNode=null;
// start building process
me.buildTree(me.xml,me.container);
}



buildTree() function:
The previous function passes the xml object and the container element (the documentís body if not specified).
Before we can loop all over the XML elements we have to get rid of the automatically generated node (#document) , replace it with the real document element and store it in the (dElem) variable for later use.
After that we get the child nodes of the document element (and later, for passed node)
We exit the functions if the passed xml object is null or no more children found.
// Build the tree recursively
var dElem;
this.buildTree=function(xmlObj,htmlNode){alert(xmlObj.nodeName)

	if (xmlObj.nodeName=="#document"){
		xmlObj=xmlObj.documentElement;
		dElem=xmlObj;
	}
if(xmlObj==null)return false;

	var xmlNodes=xmlObj.childNodes;


	if (xmlNodes.length==0)return false;



The loop:
I tried to use for-in loop but it always fails on IE browsers, so I used the regular for loop and access the xml nodes by its indexes (xn stands for xml node)
Ignore the auto generated text nodes (#text)
Check if (xn) is a parent node and store the result in (isParent).

	for (var i=0 ;i<xmlNodes.length;i++){
		var xn=xmlNodes[i];
		if (xn.nodeName=="#text")continue;
		var isParent=xn.hasChildNodes();



Create an HTML paragraph element (pn) to hold the icon and the text spans (and other paragraphs if itís a parent node), and attach the corresponding CSS class name to it (if itís a parent node or an item).
		var pn=document.createElement("p");
		pn.className=isParent?me.parentStyle:me.itemStyle



Create two HTML span elements (icon, txt) to hold the parent / item icon and parent / item text, then attach them the corresponding CSS class name.
Read the (text) attribute from the xml node (xn) to be the inner html of the (txt) span.
Append (txt, icon) to (pn)
Hide the html paragraph node (pn) if itís not directly nested in the document element (all other nodes will be collapsed)
Append (pn) to the passed argument (htmlNode) (container property for the first time) and store a reference to it in (hn) (hn stands for html node), in case of (hn) is a parent it will be passed to the function again as (htmlNode) argument.
Create a new property for the html element (pn) which is (xAttributes) to hold the (xn) attributes array, so we can access them later through the HTML paragraph element (using the object we pass to events handlers or the selectedNode.hnode property).

	for (var i=0 ;i<xmlNodes.length;i++){
		var xn=xmlNodes[i];
		if (xn.nodeName=="#text")continue;
		var isParent=xn.hasChildNodes();

		var pn=document.createElement("p");
		pn.className=isParent?me.parentStyle:me.itemStyle
		
		var icon=document.createElement("span");
		icon.className=isParent?me.parentIconStyle:me.itemIconStyle

		var txt=document.createElement("span");
		txt.className=isParent?me.parentTextStyle:me.itemTextStyle
		txt.innerHTML=xn.getAttribute("text");

		pn.appendChild(icon);
		pn.appendChild(txt);
		
		pn.style.display=(xn.parentNode==dElem)?"block":"none";
		var hn=htmlNode.appendChild(pn);
		pn.xAttributes=xn.attributes; //use getNamedItem("attribute name").value to access an attribute



Adding events / parent special treatment:
We will handle two types of events (click / double click), also two kinds of problems we will take care of (event bubbling / variable scope).

Because weíre using nested paragraphs (the same if we used a list element since it contains nested elements), clicking an element may trigger the click event of its parents (Bubbling), simply we can stop this by setting cancelBubble property to true.

We call select() function to change the text span style of the selected node depending on the node type (parent/item).

When we double click a parent node all its direct children must be expanded/ collapsed and the parent icon must be changed to the open / close mode depending on the parent current status.
Thatís why I added a new attribute to each parent paragraphs (isOpened) to hold the current parent status (0 closed, 1 opened), Call expandCollapse() function to do the necessary (weíll discuss it later).

Accessing the main xmlTree() properties/methods (styles, event handlersÖ) Using (this) statement is not applicable inside an event handler, (this) will refer to the element that triggers the event and not anymore the xmlTree() function itself,
But if you still remember, the (me) property has been declared to refer to (this) in the main function, and since the handlers are built inside the xmlTree()function scope, the (me) property will always refer to the xmlTree() function.

We call the event handler property if specified and pass it the clicked paragraph html element.

Finally, If the element weíre working on is a parent element, we call buildTree() function again and pass it the current xml node (xn) and the current html node(hn) to be processed, and this is the recursive part of the function.


		if (isParent){
			pn.setAttribute("isOpened",0);
			
			// Parent double click event
			pn.ondblclick=function(e){
				if (!e)e=window.event;
				e.cancelBubble =true;
				me.select(this,1)

				var isOpened=this.getAttribute("isOpened");
				me.expandCollapse(this,isOpened==1?0:1)	

				if (me.onParentDblClick!=null)me.onParentDblClick(this)
			}
			
			// Parent click event
			pn.onclick=function(e){
				if (!e)e=window.event;
				e.cancelBubble =true;
				me.select(this,1)
				if (me.onParentClick!=null)me.onParentClick(this)
			}
			
			me.buildTree(xn,hn);
		}else{
			// Item double click event
			pn.ondblclick=function(e){
				if (!e)e=window.event;
				e.cancelBubble =true;
				me.select(this,0)
				if (me.onItemDblClick!=null)me.onItemDblClick(this)
			}
			
			// Item click event
			pn.onclick=function(e){
				if (!e)e=window.event;
				e.cancelBubble =true;
				me.select(this,0)
				if (me.onItemClick!=null)me.onItemClick(this)
			}

		}

	}
}



Select() function:
This function needs the selected html node and if itís a parent node or not,
Here we use the selectedNode property and again we access it using (me),
We set the selected style for the text span to the corresponding one and we unselect the old select item if exist.
The selectedNode property is an object that has two properties: hnode, isParent to be internally used to unselect an item.


// Select / Unselect node
this.select=function(hnode,isParent){
	if (me.selectedNode!=null){
		if(me.selectedNode.hnode!=hnode){
			me.selectedNode.hnode.getElementsByTagName("span")[1].className=me.selectedNode.isParent?me.parentTextStyle:me.itemTextStyle
		}
	}
	hnode.getElementsByTagName("span")[1].className=isParent?me.parentSelectedTextStyle:me.itemSelectedTextStyle
	me.selectedNode={"hnode":hnode,"isParent":isParent}
} 



expandCollapse() function:
This function is called when double clicking a parent node or by calling it directly from outer script and it needs the html parent node to work on and the open/close status we want to execute (0 closed, 1 opened).
You may ask; why do we have to pass the open/close status since itís already an attribute in the parent node and all what we have to do is to read it and swap its value?
Youíre right but I thought using it this way give us the facility to expand/collapse the node we want by calling the expandCollapse() function and without the need to double click the node (maybe it useful when you refresh the page and you want to reopen the last node user selected).

So, here we check first if the passed html node is a parent or not by looking for the (isOpend) attribute (itís only available for the parent elements as we specify)
We loop to hide/show all of the parentís children except for the first two items (icon and text span elements) and change the style properties accordingly.

// Expand / Collapse a parent node
this.expandCollapse=function(pnode,isOpened){
	// exit if the passed html node is not a parent node (all parent nodes has the isOpend attribute)
	if(!pnode.hasAttribute("isOpened"))return false;
	pnode.setAttribute("isOpened",isOpened);
	var children=pnode.childNodes;

	// set the parent corresponding icon class name
	pnode.getElementsByTagName("span")[0].className=isOpened==1?me.parentOpenedIconStyle:me.parentIconStyle;

	// Ignore first 2 elements (the icon and text spans)
	for(var i=2;i<children.length;i++){
		if (children[i].nodeName=="#text")continue;
		children[i].style.display=(isOpened==1?"block":"none");
	}
} 
}




We almost done, so, how do you feel?

CSS & XML:
Iím not going to add any details about the xml and css files, itís all up to you and itís only will affect the way it looks and how itís categorized, so letís move on to testing step (finally :) ).

Testing:
As I mentioned before we need to do all tests on a local/web host, so move your file there (you can use attached files) and donít forget about the small icons, it gives a nice looking for the tree.

The example I will present assumes that the files on the server are named as: xmlTree.js, webref.xml, styles.css.

Open your text editor and start creating a basic html document.
At the header, link the CSS file (styles.css if you have it as an external file) and the (xmlTee.js)
Add a dive element (dvTree) to be the container for our tree object.

<html>
<head>
<link rel="stylesheet" href="styles.css" />
<script type="text/javascript" src="xmlTree.js"></script>
</head>
<body onload="load()">
<div id="dvTree"></div>
</body>
</html>


To make sure everything were loaded, we will start using the xmlTree() function when the body load event is fired.
Letís back again to the header part of the html document and letís create the function (load()) that it will called from the body load event to construct our tree.

Inside load() function we simply create an instance of the xmlTree(), add the necessary properties and call build() function.

I added some comments inside.

In the head part of the document
<script type="text/javascript">
function load(){
// get a new instance of the xmlTree
t=new xmlTree();

// add the url and the container properties
t.url="webref.xml";
t.container=document.getElementById("dvTree");

// add the necessary style class names
t.parentStyle="parent";
t.itemStyle="item";

t.parentSelectedTextStyle="selectedparenttext";
t.itemSelectedTextStyle="selecteditemtext";

t.parentTextStyle="parenttext";
t.itemTextStyle="itemtext";

t.parentIconStyle="picon";
t.parentOpenedIconStyle="poicon";
t.itemIconStyle="iicon";


// retreive an attribute when we double click an item (article here), obj is the clicked html element.
t.onItemDblClick=function(obj){

// xAttributes is the corresponding xml node attributes array we linked to each html parent/item node (paragraph here)
// use getNamedItem to access the attribute by its name (in some browsers you can access it directly from xAttributes array)
// since xAttributes is an array that's means you can retreive the attributes values by its indexes (but I don't think it's a good way)
alert(obj.xAttributes.getNamedItem("articleID").value)

}
t.build();
}
</script>



Try it.


Notes:
If you try to run xmlTree on IE7 or even IE8 but not in the standard mode youíll notice that the icons are invisible, and this is can be easily managed by not leaving the icon span empty, so adding any letter to it (space probably) will fix it (but maybe you have to adjust the css a little bit).

If you already have your xml document loaded or you made some changes to the xml on fly ( or even to the one of xmlTree properties) and you want these changes to be reflected on the tree, you can then reassign the new xml to the xmleTree.xml property (or whatever property you want to change) and call the xmlTree.refresh() function.

// trying to change the xml
function update(){
var a=t.xml.getElementsByTagName("topic")[0];
a.setAttribute("text","changed")
t.refresh();
}



What if for example you want to collapse the selected parent node?
//collapse selected node
function collapse(){
t.expandCollapse(t.selectedNode.hnode,0)
}



This was a basic xml tree view object; itís not targeting any special kind of processing /procedures,
A lot of customizations you may want to add, things to do to get it work with your needs.

Thatís all what I have for now,
Thanks for your patient and sorry about the headache :).

Check attached Attached File  xmlTree.zip (6.08K)
Number of downloads: 1194

Is This A Good Question/Topic? 0
  • +

Page 1 of 1