REQUEST A DEMO

Hierarchical drop-downs in Dovetail Agent

Many customers use hierarchical (multi-level) drop-down lists in their Clarify/Dovetail implementations. So, when one level of a select list changes, the down-level lists change as well. Clarify allows up 5 levels for a list.

As an example, in baseline Clarify, there’s a list titled CR_DESC that is a three level list for CPU, Operating System, and Memory. When I change the CPU from PC to Sun, the operating system and memory lists need to change as well.

Default values show:

pc

Changing the CPU to "SUN" changes the O/S and Memory lists:

sun

In a client/server app, this is pretty easy to do, as the onchange event of one list can simply update the down-level lists, either by making a call back to the server, or by accessing data that has been cached on the client machine.

In a web application, this is more difficult. You don’t want to refresh the entire page, as this would be slow, and in general, would suck from a usability standpoint.

DdlHelper

For the past few years, we’ve used a helper utility called DdlHelper. This helper has a server-side component that builds up a multi-dimensional JavaScript array for all of the levels in the list, and then pushes this data down to the client. The client-side component then rebuilds the down-level lists when one changes. Because all of the data is already on the client, this approach is very quick from the client-side. However, the server-side code can be slow and processor intensive when there are a lot of levels and elements in the list. For example, we recently had a customer who had thousands of elements in a 4 level list. The DdlHelper utility could process the list, but it was very slow – slow to the point of unusable.

A more contemporary approach

An alternative approach is to follow an AJAX like pattern, using asynchronous JavaScript to refresh the down-level lists. For example, when the CPU changes, make an async request back to the web server, and just get the specific O/S and Memory elements based on the selected CPU value. Since the Dovetail SDK caches this list data on the web server in memory, this isn’t a database roundtrip – just a roundtrip to the web server. Because we’re only getting the specific list levels that we need (as opposed to all of the list levels), it’s much quicker.

In addition to using this pattern, I also used JSON to pass the data back from the web server to the client. JSON allows me to work with JavaScript objects. On the web server, I can build up an object with the data that I want, "stringify" it, and then return that back to the client.  On the client side, I can "parse" the response text back into the same object that I had on the server. Sweet. What’s even better is that there are a plethora of JSON libraries out there to use. I used a JavaScript one.

multiLevelListHelper Object

To make all of this easily reusable, I created a multiLevelListHelper object. The steps to add a new multi-level list to a page are:

1. Include the json.js and multiLevelListHelper.js code files:

<script language="javascript" src="../code/json.js"></script>
<script language="javascript" src="../code/multiLevelListHelper.js"></script>

2. Define the empty HTML select elements (I’ve also added in labels):

<label for="cpu">CPU:</label><select id = "cpu"></select>
<label for="os">O/S:</label><select id = "os"></select>
<label for="memory">Memory:</label><select id = "memory"></select>

3. Create a new multiLevelListHelper object in client-side code:

The constructor takes 3 arguments. The name of the list, an array of select element IDs, and an array of initial values. Note that here I am passing in empty strings for the initial values, so the default values (as defined in the database) will be used.

var cpuList = new multiLevelListHelper("CR_DESC", [‘cpu’,’os’,’memory’], [”,”,”]);

4. Call the populate method on your multiLevelListHelper object:

cpuList.populate();

That’s it. The list is rendered, populated, and wired up for onchange events.

pc

Multiple lists on one page

This was actually a little tricky to implement, but yes, you can have multiple hierarchical lists on one page. Create your HTML elements, and then create your multiLevelListHelper objects:

<fieldset>
  <legend>Diagnostics</legend>
  <label for="x_system">System:</label><select id = "x_system"></select>
  <label for="x_casetype">Case Type:</label><select id = "x_casetype"></select>
  <label for="x_problem">Problem:</label><select id = "x_problem"></select>
  <label for="x_symptom">Symptom:</label><select id = "x_symptom"></select>
  <label for="x_5th">5th level:</label><select id = "x_5th"></select>
</fieldset>

<fieldset>
  <legend>Family</legend>
  <label for="family">Family:</label><select id = "family"></select>
  <label for="line">Line:</label><select id = "line"></select>
</fieldset>

<fieldset>
  <legend>System Details</legend>
  <label for="cpu">CPU:</label><select id = "cpu"></select>
  <label for="os">O/S:</label><select id = "os"></select>
  <label for=&qu
ot;memory">Memory:</label><select id = "memory"></select>
</fieldset>

 

<script type="text/javascript">

var elementIds = [‘x_system’,’x_casetype’,’x_problem’,’x_symptom’,’x_5th’];
var values = [‘top element 6′,’2nd level element 5′,’3rd level element 4′,’4th level element 3’,”];
var bigList = new multiLevelListHelper("Big List", elementIds, values);
bigList.populate();

var familyList = new multiLevelListHelper("Family", [‘family’,’line’], [”,”]);
familyList.populate();

var cpuList = new multiLevelListHelper("CR_DESC", [‘cpu’,’os’,’memory’], [”,”,”]);
cpuList.populate();

</script>

Result:

multiple 

Performance

Overall, this is extremely quick. Running on our LAN, its instantaneous. I am considering adding some type of processing indicator [www.napyfab.com/ajax-indicators/ no longer available], for those operating in slower networks. A task for another day.

Code

For those that want to peek inside the code, here we go.

The constructor for the multiLevelListHelper Object. It’s a simple JavaScript object. It also wires up the onchange events for each select element.

function multiLevelListHelper(_listName, _elementIds, _values){
  this.listName = _listName;
  this.values = _values;
  this.elementIds = _elementIds; 
  this.pathToPopulateDropDowns = ‘../include/PopulateDropDowns.asp’;
  //Add an on change event to all but the last drop-down
  for( var i = 0; i < this.elementIds.length – 1; i++ )
    {
       var ddlObj = document.getElementById(this.elementIds[ i ]);                    
       ddlObj.listObject = this;
       ddlObj.level = i+1;
       ddlObj.onchange = onDropDownChange;
    }
}

The populate functions, which makes the async call to the web server, as well as the function for when the data is returned back to the client. Notice the JSON.parse method used to turn the response data back into a JavaScript object.

multiLevelListHelper.prototype.populate = function()
{
  var url=this.pathToPopulateDropDowns;
  url+="?listName=" + this.listName;
  url+="&level1value=" + this.values[0];
  url+="&level2value=" + this.values[1];
  url+="&level3value=" + this.values[2]; 
  url+="&level4value=" + this.values[3]; 
  url+="&level5value=" + this.values[4]; 
  url+="&selectElement1Id=" + this.elementIds[0];
  var self = this;

  this.xmlHttp=GetXmlHttpObject();
  this.xmlHttp.onreadystatechange=function()
            {
            if (self.xmlHttp.readyState==4 || self.xmlHttp.readyState=="complete")
              {               
               var jsonObject = JSON.parse(self.xmlHttp.responseText);
               var selectElement1 = document.getElementById(jsonObject.selectElement1Id);
               var listObject = selectElement1.listObject;
               addOptionsToSelectList(listObject.elementIds[0],jsonObject.level1,jsonObject.level1Value);
               addOptionsToSelectList(listObject.elementIds[1],jsonObject.level2,jsonObject.level2Value);
               addOptionsToSelectList(listObject.elementIds[2],jsonObject.level3,jsonObject.level3Value);
               addOptionsToSelectList(listObject.elementIds[3],jsonObject.level4,jsonObject.level4Value);           
               addOptionsToSelectList(listObject.elementIds[4],jsonObject.level5,jsonObject.level5Value);
               }
            } //end function
  this.xmlHttp.open("GET", url , true);
  this.xmlHttp.send(null);
}

Helper function for adding option elements to a select list and setting its value:

function addOptionsToSelectList(elementId,ArrayOfOptionValues,selectedValue)
{
     if (! document.getElementById(elementId)){
      return false;
     }
     document.getElementById(elementId).options.length = 0;
     for (var i = 0; i<ArrayOfOptionValues.length;i++)
     {
      var optionValue = ArrayOfOptionValues[ i ];
      var option = new Option(optionValue,optionValue);
      document.getElementById(elementId).add(option);
     }
     document.getElementById(elementId).value = selectedValue;
}

The onchange event handler function. When a select list changes, this function will get called.

Notice that it calls on the same populate method as when the lists are initially built.

function onDropDownChange()
{
  listObject = this.listObject;
  var whichLevelChanged = this.level;

  var selectElement1 = document.getElementById(listObject.elementIds[0]);
  var selectElement2 = document.getElementById(listObject.elementIds[1]);
  var selectElement3 = document.getElementById(listObject.elementIds[2]);
  var selectElement4 = document.getElementById(listObject.elementIds[3]);                 
  listObject.values[0] = ”;
  listObject.values[1] = ”;
  listObject.values[2] = ”;
  listObject.values[3] = ”;    
  listObject.values[4] = ”;   
  if (whichLevelChanged >= 1){
     listObject.values[0] = selectElement1.options[selectElement1.selectedIndex].innerHTML;  
  }
  if (whichLevelChanged >= 2){
     listObject.values[1] = selectElement2.options[selectElement2.selectedIndex].innerHTML;  
  }
  if (whichLevelChanged >= 3){
     listObject.values[2] = selectElement3.options[selectElement3.selectedIndex].innerHTML;  
  }
  if (whichLevelChanged >= 4){
     listObject.values[3] = selectElement4.options[selectElement4.selectedIndex].innerHTML;  
  }
  listObject.populate();   
}

The PopulateDropDowns.asp page. Notice that we’re creating a JSON object with the data for each level of the list, along with which element should be selected in each level. Then, we "stringifying" the JSON object, and then send this back to the client. As we saw earlier, the client-side code parses this string back into the same JavaScript object.

<%@ Language=JavaScript %>
<!–#include file="json.asp"–>

<%

function addLevelToJsonObject(whichLevel,recordset,value)
{

  var property = "level" + whichLevel + "Value";
  jsonObject[property] = value;
  property = "level" + whichLevel;
  jsonObject[property] = new Array(recordset.RecordCount);
  for (var i = 0; i< recordset.RecordCount;i++){
    jsonObject[property][ i ] = recordset(‘title’) + ”;
    recordset.MoveNext();
  }
}

try{

Response.Clear();
BuildQueryStringVariables();

var jsonObject = new Object();
jsonObject.listName=listName;

//Level 1:
  var level = FCApp.GetHgbstList(listName);
  var level1default = FCApp.GetHgbstElmDefault(listName) + ”;
  if (level1value==”){ level1value = level1default;}
  addLevelToJsonObject(1,level,level1value);

//Level 2: 
  var level = FCApp.GetHgbstList(listName, level1value);
  var level2default = FCApp.GetHgbstElmDefault(listName, level1value);
  if (level2value==”){ level2value = level2default;}
  addLevelToJsonObject(2,level,level2value);

//Level 3: 
  var level = FCApp.GetHgbstList(listName, level1value, level2value);
  var level3default = FCApp.GetHgbstElmDefault(listName, level1value, level2value);
  if (level3value==”){ level3value = level3default;}
  addLevelToJsonObject(3,level,level3value);

//Level 4: 
  var level = FCApp.GetHgbstList(listName, level1value, level2value, level3value);
  var level4default = FCApp.GetHgbstElmDefault(listName, level1value,level2value, level3value);
  if (level4value==”){ level4value = level4default;}
  addLevelToJsonObject(4,level,level4value);

//Level 5: 
  var level = FCApp.GetHgbstList(listName, level1value, level2value, level3value, level4value);
  var level5default = FCApp.GetHgbstElmDefault(listName, level1value,level2value, level3value, level4value);
  if (level5value==”){ level5value = level5default;}
  addLevelToJsonObject(5,level,level5value);

  jsonObject.selectElement1Id = selectElement1Id;  
  var jsonText = JSON.stringify(jsonObject);
  Response.Write(jsonText);
}
catch(e)
{
  Response.Write(e.description);
}

%>