Welcome!

AJAX & REA Authors: Chris Fleck, Marek Miesiac, Loraine Antrim, Liz McMillan, Yeshim Deniz

Related Topics: AJAX & REA

AJAX & REA: Article

Real-World AJAX Book Preview: XMLToDataSet Class

Real-World AJAX Book Preview: XMLToDataSet Class

This content is reprinted from Real-World AJAX: Secrets of the Masters published by SYS-CON Books. To order the entire book now along with companion DVDs for the special pre-order price, click here for more information. Aimed at everyone from enterprise developers to self-taught scripters, Real-World AJAX: Secrets of the Masters is the perfect book for anyone who wants to start developing AJAX applications.

XMLToDataSet Class
In the previous sections, we've discussed the HTML template and a component class called "NavBar." Next we'll discuss more templates and the component classes that are bound to the application data. But before that, we'll discuss how the data is converted to the internal form. The XML document received by the client is converted to the JavaScript object tree. The XMLToDataSet class is responsible for the conversion.

The code below shows how to convert the response XML of an AJAX call to a JavaScript object tree using XMLToDataSet.

var xmlToDS = new XMLToDataSet();
xmlToDS.fromXml(response.responseXML);

Listing 5.9 and embedded comments explain the XMLToDataSet class in detail. Also see the section above on Model for details of the XML and JavaScript object tree.

Listing 5.9 - XMLToDataSet Class

/**
The following function (class) converts XML to a tree of JS objects. "this.data" gives
the top-level object that corresponds to the first element of the XML (excluding header).
Each nested element is represented by a JavaScript object. By default a nested element
is treated as a list type (one-to-many association). Therefore it is added to an array,
which in-turn is stored as the attribute of the parent object using "tag name" as the
attribute name. If a nested element has a special attribute called association="one-toone"
then the object is stored directly as the attribute of the parent object. Simple
nested elements are stored as the scalar attributes of the corresponding JavaScript object.

'class' and 'id' are treated as special attributes. 'class' indicates the class of the
object. 'id' represents object identifier. They are stored using __class and __id as attribute
names.
*/
function XMLToDataSet() {
     this.data = new Object();
}

XMLToDataSet.prototype.fromXml = function(xml) {
   // Special case when JavaScript is loaded from the file system during the development and
   // data is loaded from a WEB server. Firefox requires that the privilege is enabled.
   // It is not required if JavaScript and data are accessed from the same server.
   if (!is_ie && is_ns) {
       try {
       netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
       } catch (e) {
       alert("Permission UniversalBrowserRead denied.");
       }
     }

     // top level object
     this.data = new Object();

     var nodes = xml.childNodes;

     var rootNode = nodes[nodes.length-1];

     // store top level objects class and attributes
     if (rootNode.attributes != null) {
       var attr = rootNode.attributes.getNamedItem('class');
       if (attr != null) {
       this.data.__className = attr.value;
       }
       attr = rootNode.attributes.getNamedItem('id');
       if (attr != null) {
       this.data.__id = attr.value;
       }
   }

   this.processXMLNode(this.data, rootNode);
   return this.data;
}

XMLToDataSet.prototype.processXMLNode = function(data, parent) {
   var nodes = parent.childNodes;
   // process the child nodes of the give parent node.
     for (var i = 0; i < nodes.length; i++) {
       var node = nodes[i];

       var doProcess = node.nodeType == Node.ELEMENT_NODE ||
       node.nodeType == Node.DOCUMENT_NODE
       || node.nodeType == Node.DOCUMENT_FRAGMENT_NODE;

       if (!doProcess) {
       continue;
       }

       var a = node.childNodes;
       if (a.length == 1) {
       var gchild = a[0];
       // This is a simple element case. Store as the attribute of the given object (data).
       if (gchild.nodeType == Node.TEXT_NODE) {
         data[node.tagName] = gchild.data;
         continue;
       }
       }

     // This is a nested element case.
     // Extract special attributes, class, id and association.
     var myClass = null;
     var id = null;
     var assoc = null;
     var attrs = node.attributes;

     if (attrs != null) {
     for (var j = 0; j<attrs.length; j++) {
       var attr = attrs[j];
       data[attr.name] = attr.value;
       if (attr.name == 'class') {
         myClass = attr.value;

         } else if (attr.name == 'id') {
         id = attr.value;
         } else if (attr.name == 'association') {
         assoc = attr.value;
       }
     }
     }
     // tag name of the nested element that will be used as the attribute name
     var s = nodes[i].tagName;

     // JavaScript object is created for the nested element.

     var data2 = new Object();

     // store reference of the parent usign special attribute name
     data2.__parent = data;
     // store class and id using special attribute names
     data2.__className = myClass;
     data2.__id = id;

     if (assoc == 'one_to_one') {
     // it is an one-to-one association case.
     // add the object as an attribute of the parent.
     data[s] = data2;
     } else {
     // it is a one-to-many association case
     // retrieve the list (array) that stores the multiple nested elements of the same
     // tag name. This will be present if this is not the first nested element.
     var c = data[s];

     if (c) {
       // if the list (array) is present, simply add to it.
       c.push(data2);
       } else {
       // this is the first nested element of the tag name given by 's'.
       // constuct a new list
       c = new Array();
       // list parent is the parent of the objects held by it
       c.__parent = data;
       // the class of the parent is same as the class of objects held by it
       c.__className = myClass;
       // point to the list from the parent using tag name as the attribute
       data[s] = c;
       c.push(data2);
     }
     }
     // recursively process the nested elements of the 'data2'.
     this.processXMLNode(data2, node);
   }
}

Project List Page
Refer to MainPage_4.html for the discussion in this section.

So far we've discussed the navigation bar using the HTML template and component class and how to use XMLToDataSet to convert the response XML to a JavaScript object tree. Our controllers attached to the navigation bar did nothing but display a "not yet implemented" message. In this section we'll extend one of the controllers to display a table, use an AJAX call to retrieve data from the server, and use XMLToDataSet to convert it to JavaScript objects to be bound to the table.

Our goal is to display a list of projects in the "MainArea." The list of projects is also our default page so it's displayed at startup. Later, it's displayed every time the user clicks on the "Project List" item on the navigation bar.

Say we ask our UI designer to design a page for the project list with some dummy data. She designs the page and names it ProjectList.html. The screenshot below shows the page when it's loaded in a browser.

Next, we want to use ProjectList.html as a template and attach it to the "MainArea" of the MainPage-4.html. In ProjectList.js, we write the following JavaScript code to do so.

Listing 5.10 - Displaying ProjectList.html Template

var projectList = null;
var projectListContainer = null;
function displayProjectList() {
   if (this.projectListContainer == null) {
     // project list template is not yet loaded
     // load the template
     loadProjectListElement();
   } else {
     // project list template has been loaded
     // simply attach it
     // See Util.js for the details of attachElement function.
     attachElement('MainArea', projectListContainer);
   }
}

function loadProjectListElement() {
   var req = new DataRequest(basePageURL + 'ProjectList.html',
       requestCompletedProjectListElement,
       requestFailedProjectListElement,
       requestTimedoutProjectListElement);
   ajaxEngine.processRequest(req);
}

function requestCompletedProjectListElement(response) {
   projectListContainer = setElementFromHtml(response.responseText,
       'MainArea',
       'projectListContainer');
}

When we invoke "displayProjectList" from the "initialize" method of the MainPage-4.html, the MainPage-4.html shown below is the result.

function initialize() {
     loadTopNaviagation();
     displayProjectList();
}

To display the project list when a user selects the Project List item from the navigation bar, we change the event listener in the TopNavBar.js as follows:

navBar.setHandler("projectList", displayProjectList);

SimpleTable
In the last section, we saw how to load the ProjectList.html template and display it in the "MainArea" of the page. We also modified the controller's event listener to display it when the user clicks on the "Project List" item of the navigation bar. The data displayed inside is some dummy data that was added by the page designer to the HTML template. When we click on a row in the table, we don't notice any selection behavior. To provide data-binding functionality and any additional behavior, we implement a component class called SimpleTable. This section discusses the design of this class.

The SimpleTable component design assumes that:

  • The table element has a THREAD element containing column elements.
  • The column header ID is the same as the attribute name.
  • The table has a TBODY element where the data is displayed.
  • A special column name "_rowHeader" is used to display the row heading. If it's not there, then no row header is displayed.
Listing 5.11 shows the implementation of the SimpleTable. The comments in the code explain its two methods: "setHtmlElement" and "setData." The method "setData" assumes that it is passed an array of JavaScript objects in which each object corresponds to one row.

Listing 5.11 - SimpleTable Class

function SimpleTable() {
   // list of columns
   this.columns = null;
   // the body element where data is shown
   this.tbody = null;
   // the actual table element
   this.tableElement = null;

   this.currentRow = -1;
   this.savedColor = new Array();
   this.dlist = null;
   this.cellSelectedListeners = new Array();
}

SimpleTable.prototype.setHtmlElement = function(e) {
   this.tableElement = e;
   var collection = new ElementCollection(e);
   var thead = collection.getFirstElement('THEAD');
   this.tbody = collection.getFirstElement('TBODY');
   if (thead == null) {
     alert("SimpleTable requires THEAD element inside TABLE element");
     return;
   }
   if (this.tbody == null) {
     alert("SimpleTable requires TBODY element inside TABLE element");
     return;
   }
   // makes a list of columns
   this.columns = new ElementCollection(thead).getElements('TH');
}

SimpleTable.prototype.setData = function(dlist) {
   this.dlist = dlist;
   // removes all nodes user the body element, cleans it up
   removeAll(this.tbody);

   this.currentRow = -1;

   // click handler is added to each cell
   var self = this;
   self.clickHandler = function(event) {self.cellClicked(event);};
   // for each object is the list creates a TR element
   for (var i=0; i<dlist.length; i++) {
     var row = dlist[i];
     // creates a TR element
     var tr = document.createElement('tr');
     // for each column, creates TD element
     for (j=0; j<this.columns.length; j++) {
     var col = this.columns[j];
     var td = document.createElement('td');
     // assign id that can give row, column of the cell
     td.id = "Cell_" + i + "_" + j;
     // set onclick handler for the cell
     td.onclick = self.clickHandler;
     // looks up value of the attribute with the same
     // name as the column id
     var v = row[col.id];
     if (v == null) {
       v = ' ';
     }
     var className = 'tdContent';
     // check if it is a special column, sets it style using
     // className 'tableCellRowHeader'
     if (col.id == '_rowHeader') {
       className = 'tableCellRowHeader';
       } else {
       if (j==0) {
         className = 'tdContent1st';
       }
     }
     // sets style of TD element
     td.className = className;
     // set the value of TD element
     td.appendChild(document.createTextNode(v));
     // add the TD to TR element
     tr.appendChild(td);
     }
     // add TR to body element
     this.tbody.appendChild(tr);
     }
   }

Now that we have our SimpleTable class, we can associate it with our table element. In the PlanList. js, the following code associates the table element with the SimpleTable component.

function requestCompletedProjectListElement(response) {
   projectListContainer = setElementFromHtml(response.responseText,
'MainArea', 'projectListContainer');
     // our projectListContainer element contains the projectListElement
     projectListElement = document.getElementById("projectListElement");
   // create an instance of SimpleTable component
     projectList = new SimpleTable();
   // set the table element
     projectList.setHtmlElement(projectListElement);
     ... ... ...
}

Loading Table Data
Our next step is to retrieve data from the server and display it inside the project list table. Refer to the MainPage-5.html for the following discussion.

Since we don't have our application server ready yet, we'll use the XML file as the data source. We assume that the XML file will be accessed from a Web server. The example XML file is called ProjectList.xml and its partial contents are shown in Listing 5.12.

Listing 5.12 - ProjectList.xml

<ProjectList class="ProjectList">
   <name>ProjectList</name>
   <project class="Project">
     <name>Marketing Campaign</name>
     <resource>Monti</resource>
     <startDate>10/3/2005</startDate>
     <endDate>11/4/2006</endDate>
     <status>Green</status>
     <description>Marketing Campaign Project</description>
   </project>
    <project class="Project">
     ... ... ...
   </project>

    <project class="Project">
     ... ... ...
   </project>

    <project class="Project">
     ... ... ...
   </project>
   </ProjectList>

Next we implement the following functions in ProjectList.js to load the ProjectList.xml and display it inside the table.

Listing 5.13 - Load ProjectList.xml

// load ProjectList.xml
function loadProjectList() {
   var req = new DataRequest(baseURL + 'ProjectList.xml',
     requestCompletedProjectList,
     requestFailedProjectList,
     requestTimedoutProjectList);
     ajaxEngine.processRequest(req);
}

   // the file is loaded successfully
   // convert it to dataset (JavaScript objects)
   function requestCompletedProjectList(response) {
   var xmlToDS = new XMLToDataSet();
   var data = xmlToDS.fromXml(response.responseXML);
   // the array of project is given by the attribute name 'project'
   // notice that in the result XML, it is the tag name of the objects.
   // bind the 'projectList' component with this array
   projectList.setData(data["project"]);
}

In Listing 5.13 we used an XML file as the data source. Once we have our application server ready, we simply have to change the request URL to point to the application server, and the client will continue to work without any change as long as the XML format is the same.

The last step, which displays the data inside the table, is to invoke the "loadProjectList" method. Notice that in the ProjectList.js, after loading the project list template, we call a function of the "projectTemplateLoaded()" as follows:

function requestCompletedProjectListElement(response) {
   ... ... ...
     projectTemplateLoaded();
}

The function "projectTemplateLoaded" is defined in the MainPage-5.html. Here we call "loadProjectList," i.e., we call "loadProjectList" for the first time after the project list template is loaded.

function projectTemplateLoaded() {
     loadProjectList();
}

This content is reprinted from Real-World AJAX: Secrets of the Masters published by SYS-CON Books. To order the entire book now along with companion DVDs, click here to order.

About Anil Sharma

Anil Sharma is a founder of Vertex Logic and architect of its AjaxFace product. He is working with Vertex Logic's customers and helping them deploy WEB 2.0 applications built using AjaxFace. These applications are in the areas of online printing, community WEB sites and social networking. He is driving future products of Vertex Logic. Prior to Vertex Logic, he was CTO and founder of Softrock Systems and Component Plus. There he built a model driven application platform using J2EE. His primary interests include user interface infrastructures and model driven applications. He is one of the contributors of the book "Real World AJAX - Secrets of the Master".

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.