CSS Friendly AJAX Tree Control






2.20/5 (2 votes)
A very simple and lightweight AJAX hierarchical tree control.
Introduction
I am developing a WebPart and I needed a client-side hierarchical tree control. I also wanted a CSS-friendly specific markup. I then found this was great, and got me started. I have this setup now as is, and it works great. The only thing was that some datasets that I may possibly be dealing with will be very large. This script does some initial setup that is costly with large data. Therefore, I spawned this bit of code. I do not use the tree control JavaScript anymore, but I do still use the CSS and the code was the basis of my final code.
Background
I just want to point out a few things that I think could be helpful in all aspects of web development.
- Namespaces
- JavaScript classes
- Logging
- Simplistic AJAX calls
- Server-side code
Please see this link for a more elaborate discussion on why this works. Dustin also has a really helpful book that is on my desk right now.
var DE = {
data : {},
net : {},
client : {},
util : {
coll: {}
},
widget : { },
example : { }
};
Usage: With today's code being subsets of frameworks, it is essential to use namespaces in your code.
////DOM class
DE.util.DOM = function() {
//create private member vars here.
//Only protected methods have access to these vars
//this all falls in the realm of js classes and is not
//discussed fully in this article but
//can be found all over the web.
//protected method with access to private member vars
this.addClass = function() {
.......
}
this.anotherMethod = function() {...........}
};
//public method
DE.util.DOM.prototype.$ = function() {
using the above class:
var varName = new DE.util.DOM();
This is very helpful and needed. Alerts get old very fast!
var log = new DE.util.Logger();
log.show( true );
log.showTimeStamp(true);
Now, logging is a simple as:
log.Log("log this now");
You also need a div
on your page with id="Log"
.
<div id="Log"></div>
var _cb = new DE.util.HierFilterCB('filterdata' );
ajax1.setAsyncCall(false); or not Async jax
ajax1.request('GET',"HierFilterPage.aspx?rnd="+ Math.random(), _cb );
HierFilter hier = null;
String parent = Request.QueryString["parent"];
if (!String.IsNullOrEmpty(parent))
{
//if index is not null then a specific request for a node has been made
//index is null only on intial load
hier = new HierFilter(@"C:/Downloads/CssFriendly" +
@"AjaxTreeCtrl/App_Data/hierdata.xml", Int32.Parse(parent));
Response.Write( hier.ToString() );
}
In short
The client makes an initial call to the server for XML data at depth 0.
var ajax1 = new DE.util.Ajax();
var _cb = new DE.util.HierFilterCB('filterdata' );
ajax1.setAsyncCall(false);
ajax1.request('GET',"HierFilterPage.aspx?rnd="+ Math.random(), _cb );
The code above is not async because we need to ensure that all data is returned because the callback is responsible for parsing the data returned by the server. The client calls the following code block. The server instantiates a HierFilter
type by passing it the location of the data (XML).
//intial load
hier = new HierFilter(@"C:/Downloads/" +
@"CssFriendlyAjaxTreeCtrl/App_Data/hierdata.xml");
Response.Write(hier.ToString());
The HierFilter
class inherits from FilterBase
. In this example, FilterBase
is responsible for formatting the processed data back to the client.
HierFilter.cs
public HierFilter( String file) : base()
{
selectedIndicies = "";
XmlWriter w = base.getXmlWriter();
XmlDocument doc = new XmlDocument();
doc.Load( file );
CreateTree(w, doc);
}
FilterBase.cs
The following code block will help create well-formed XHTML data to send back to the client.
public XmlWriter getXmlWriter()
{
//uses class variable sb
XmlWriterSettings settings = new XmlWriterSettings();
XmlWriter w;
//start first <ul> tag
//tell the xml parser that i am not abiding by strigent xml rules
settings.ConformanceLevel = ConformanceLevel.Fragment;
//pretty print - not necessary but good for debugging
settings.Indent = true;
//_settings.Encoding = Encoding.Unicode;
//dont show xml info in header
settings.OmitXmlDeclaration = true;
settings.CloseOutput = false;
//XmlWriter factory push xml to string builder with these settings
w = XmlWriter.Create(sb, settings);
return w;
}
HierFilter.cs
The following creates the tree data at level 0. The CSS-friendly tree markup is a pattern of nested unordered list elements.
<ul id="mktree">
<li>......</li>
<li>......</li>
</ul>
The code:
private void CreateTree(XmlWriter w, XmlDocument doc )
{
//get node list
// InitSettings();
w.WriteStartElement("ul"); // beginning <ul> tag
w.WriteStartAttribute("class");
w.WriteValue("mktree");
w.WriteStartAttribute("id");
w.WriteValue("hierfilter");
GoDeeper(w , doc , 0 );
w.WriteEndElement(); //</ul> outermost ul tag
w.Flush();
}
private void GoDeeper(XmlWriter w , XmlDocument doc, int level )
{
bool state =false;
int index = 0;
bool hasChildren = false;
//create a tree based on the level provided
XmlNodeList nodes = doc.SelectNodes("//n[@d='"+ level +"']");
foreach (XmlNode node in nodes)
{
if (node.Attributes.Count == 4)
{
//get state from xml
if (node.Attributes[3].Value == "0")
state = false;
else
state = true;
}
index = Int32.Parse(node.Attributes[0].Value);
hasChildren = Boolean.Parse(node.Attributes[2].Value);
w.WriteStartElement("li"); // beginning <ul> tag
if (hasChildren)
{
//if the node has children place a plus gif next to it using css
w.WriteStartAttribute("class");
w.WriteValue("liClosed");
}
w.WriteStartElement("span");
w.WriteStartAttribute("class");
w.WriteValue("index");
w.WriteEndAttribute();
w.WriteValue(index.ToString());
w.WriteEndElement();
w.WriteStartElement("span");
w.WriteStartAttribute("class");
w.WriteValue("bullet");
w.WriteEndAttribute();
w.WriteValue( node.InnerText );
w.WriteEndElement();
//w.WriteValue(node.InnerText );
w.WriteEndElement();
}
//close all oustanding xml tags
//CloseOut(w, elementStack);
}
The following is called when a node is clicked. This passes in the parent ID and returns all of its children in well formatted XHTML.
public void GetChildren( XmlWriter w, XmlDocument doc, int parent )
{
bool state = false;
int index = 0;
bool hasChildren = false;
//w.WriteStartElement("ul"); // beginning <ul> tag
//w.WriteStartAttribute("class");
//w.WriteValue("liOpen");
//create a tree based on the level provided
XmlNodeList nodes = doc.SelectNodes("//n[@p='" + parent + "']");
foreach (XmlNode node in nodes)
{
if (node.Attributes.Count == 4)
{
//get state from xml
if (node.Attributes[3].Value == "0")
state = false;
else
state = true;
}
index = Int32.Parse(node.Attributes[0].Value);
hasChildren = Boolean.Parse(node.Attributes[3].Value);
w.WriteStartElement("li"); // beginning <ul> tag
if (hasChildren)
{
//if the node has children place a plus gif next to it using css
w.WriteStartAttribute("class");
w.WriteValue("liClosed");
}
else
{
w.WriteStartAttribute("class");
w.WriteValue("liBullet");
}
w.WriteStartElement("span");
w.WriteStartAttribute("class");
w.WriteValue("index");
w.WriteEndAttribute();
w.WriteValue(index.ToString());
w.WriteEndElement();
w.WriteStartElement("span");
w.WriteStartAttribute("class");
w.WriteValue("bullet");
w.WriteEndAttribute();
w.WriteValue(node.InnerText);
w.WriteEndElement();
//w.WriteValue(node.InnerText );
w.WriteEndElement();
}
// w.WriteEndElement(); //end of ul of liOpen
w.Flush();
}
Back to the client callback that started it all. The following is in the success method. This method will be called if the AJAX call completed without error. The data returned is well formed, so we parse it and look for certain criteria that will allow the CSS to style our growing tree. If the element has a class name liClosed
, then we want to attach an OnClick
event to it. This translates to a node having children or not. If a node does not have children, then there is no need for us to make a call to the server. How do we know if the node has children? The XML has an attribute 'hc
', and this is processed on the server and delivered to the client.
As the tree expands by level, the pattern grows as nested unordered lists.
<ul>
<li>
<ul>
<li>....../<li>
</ul>
</li>
</ul>
The code:
if ( n.className == 'liClosed')
{
n.onclick = function() {
log.Log("classname inside HierFilterCB " + n.className );
//check to see if node has been loaded
var ul = n.getElementsByTagName('ul');
if ( ul.length > 0 )
{
if ( n.className == 'liClosed')
{
n.className = 'liOpen';
}else{n.className = 'liClosed';}
}else
{
var spans = n.getElementsByTagName('span');
//get the index from here
for ( var j =0; j<spans.length;j++)
{
if ( spans[j].className == 'index')
{
var index = spans[j].innerText;
var child = new DE.util.ChildItemCB( n );
var childrenAjax = new DE.util.Ajax();
childrenAjax.request('GET',
'HierFilterPage.aspx?parent='+index,child);
}
log.Log("spans " + spans[j] );
}
}
}
}
When a node is clicked, the following code is called, if it has not already made a trip to the server. If it has, then there is some code above that will just modify the CSS to expand or collapse the tree instead.
//callback method called when
//a node is clicked after intial loading is complete.
//see also DE.util.HierFilterCB for more info
//parsing is better understood by inspecting hierdata.xml
DE.util.ChildItemCB = function( child ) {
this.success = function(val){
log.Log("child className " + child.className ) ;
var children = document.createElement('ul')
children.innerHTML = val;
var li = children.getElementsByTagName('li');
for ( var i=0; i < li.length; i++ )
parseTree(li[i]);
The above parse tree is called on every <li>
item returned by the server. Again, the pattern is, if a node has childrenn then we need to apply an OnClick event to it and modify its class name, therefore allowing the CSS to properly style our tree with the appropriate plus/minus signs.
if ( n.className == 'liClosed')
{
log.Log("n "+ n.innerText);
log.Log("n.className " + n.className );
n.onclick = function(e) {
e = e || (window.event || {});
e.cancelBubble = true;
...................
The above code is similar to the first call to the server. Accept must cancel any event bubbling that was inherited from its parent nodes.
Here is a sample XML dataset used:
<n i='0' d='0' hc='true' s='0'>1 - Scope</n>
<n i='1' p='0' d='1' hc='false' s='0'>1.1 - Determine project scope</n>
<n i='2' p='0' d='1' hc='false' s='0'>1.2 - Secure project sponsorship</n>
<n i='3' p='0' d='1' hc='false' s='0'>1.3 - Define preliminary resources</n>
<n i='4' p='0' d='1' hc='false' s='0'>1.4 - Secure core resources</n>
<n i='5' p='0' d='1' hc='false' s='0'>1.5 - Scope complete</n>
<n i='6' p='0' d='1' hc='true' s='0'>1.6 - Program Group</n>
<n i='7' p='6' d='2' hc='false' s='0'>1.6.1 - Milestone 1</n>
<n i='8' p='6' d='2' hc='false' s='0'>1.6.2 - Milestone 2</n>
<n i='9' p='6' d='2' hc='false' s='0'>1.6.3 - Milestone 3</n>
Using the code
Download the code and change the path to the XML file inside HierFilterPage.aspx and then run from Default.aspx.
Points of interest
This only works in IE6, not sure about IE7, but I would like feedback on how to make it work with Firefox. So, please, if anyone gets this to work, I would like to see it. Cheers!
This was developed in WebDeveloper 2008. However, using any Visual Studio based IDE should be as easy as eliminating the solution file and just opening the project from the IDE of your choice.
History
First release - Aug 13, 2008.