Universal Related Popup Menus / Single Form Version | WebReference

Universal Related Popup Menus / Single Form Version

Single Form Version

Universal Related Popup Menus

by Robert Gravelle (blackjacques@musician.org)

A few months ago, I worked on a system for an on-line automobile broker, which included a Web form where customers could enter information about the type of vehicle they were looking for. As I began to design the page, I quickly realized that this was the perfect candidate to try the universal related popup menus that I had read about at WebReference. The eternal optimist that I am, I hoped to just download the script form the site and insert it directly into my HTML code. If only Web development was that easy! As the project unfolded, I soon realized that some of our requirements called for some customization of the script. In the process, I also discovered some areas where the script could be made easier to setup and maintain for our purposes. All of this customization produced a script which differed dramatically from the original in several respects. This article explains all the changes I made to come up with the finished product in case you ever have to come up with a similar script.

The main stumbling block to using the existing script was the use of one form per control. This design was perfectly suitable for linking to other Web pages, but it would be less than ideal for submitting a large form's contents to the server. I thought about copying the values of every element to another form and submitting it, but that would be a lot of overhead. The most reasonable solution was to make the script work with a single form. At first, I wondered how possible it would be to do, but after some head scratching and a lot of dead ends, I got it working. Here is a demo of my single form version:

Automobile Request Form Example

Choose a manufacturer:

-> Choose a model:


-> Choose a model level:

Although it has exactly the same look and feel as the original script, there are a number of differences between the two, the main one being that it is now a lot easier to submit the form data with a single button click. I also wound up creating a new function to make it easier to setup the lists and another to sort them. But more on that later. Right now, let's look at the changes that were needed to make the script work with a single form:

Legend:
blue text: JavaScript
green text: modified code

The get function:

function get(elt)
{
   // loop thru document.forms property and exit w/ current element index
   var num = -1;
   for (var i = 0; i < elt.form.elements.length; i++) {
     if (elt.form.elements[i].name == elt.name) {
       num = i;  // save element index
       break;
     } 
   }
   return num;  // returns current element index
}

The sindex function:

function sindex(num, offset, elt)
{  //***modified next line to get the right element index***
   // sel finds selected index or value of num + offset's form element
   var sel = elt.form.elements[ num + offset ].selectedIndex;
   if (sel < 0) sel = 0;
   return sel;
}

The update function:

function update(num, elt, m)
{ 
   // updates submenus - element(num)'s menu options, and all submenus
   // if refd element exists
   if (num != -1) {
      num++; // reference next element, assume it follows in HTML
      //***modified next line to refer to elements within the same form***
      with (elt.form.elements[num]) { 

	 // null out options in reverse order (bug workaround)
         for (var i = options.length - 1; 0 < i; i--) options[i] = null; 
         
         // fill up next menu's items
         for (var i = 0; i < m.length; i++) 
           options[i] = m[i].value != null ? 
			new Option(m[i].text, m[i]Value) : 
			new Option(m[i]Text); // this avoids setting the menu value to null
  
         options[0].selected = true; // default to 1st menu item, windows bug kludge
      }
      if (m[0].length != 0) {
         update(num, elt, m[0]); // update subsequent form if any grandchild menu exists
      }
    }
}

The relate function:

function relate(elt, tree, depth) //don't need the form argument anymore
{ // relate submenus based on sel of form - calls update to redef submenus
   if (v) {
      var num = get(elt); // fetch the current elt index
      var a = tree;        // set a to be the tree array
 
      while (a != null && --depth != -1) {
        // traverse menu tree until we reach the corresponding option record
        a = a[sindex(num, -depth, elt)];
      }       
 
      // at depth 3, should end up w/ something like a[i][j][k]
      // where each index holds the value of s(elected )index in related form select elts
      if (a != null && a.length) {
         // if a array exists and it has elements,
         // feed update() w/ this record reference
         update(num, elt, a); 
         return;
      }
   }
   // if a hasn't any array elements or new Option unsupported then end up here
   //jmp(form, elt); // we don't need to jump to a new page 
}

As you can see by the code excerpts above, it really didn't take many changes to make the script work with a single form. It was mostly a matter of changing form indexes to element indexes. With the modifications made, I was ready to concentrate on inserting the actual data into the lists, and testing the Web page. That's where I ran into some trouble with the Object array which was needed to populate the lists. Although it was a brilliant construct from a functional point of view, it was just a little too complicated for me to follow. Have a look at this sample taken from the original article and see what you think:

if (v) {
	m = new Array(
		new Array(
			new O("3-D Animation","/3d/", new Array(
//				new O("Glossary","/3d/glossary/", null),
				new O("Lesson56","/3d/lesson56/", new Array(
					new O("56.1","/3d/lesson56/", null),
					new O("56.2","/3d/lesson56/part2.html",null),
					new O("56.3","/3d/lesson56/part3.html",null))
				),
				new O("Lesson57","/3d/lesson57/", new Array(
					new O("57.1","/3d/lesson57/", null),
					new O("57.2","/3d/lesson57/part2.html",null),
					new O("57.3","/3d/lesson57/part3.html",null))
				),
				new O("Lesson58","/3d/lesson58/", new Array(
					new O("58.1","/3d/lesson58/", null),
					new O("58.2","/3d/lesson58/part2.html",null),
					new O("58.3","/3d/lesson58/part3.html",null))
				),
				new O("Lesson59","/3d/lesson59/", new Array(
					new O("59.1","/3d/lesson59/", null),
					new O("59.2","/3d/lesson59/part2.html",null),
					new O("59.3","/3d/lesson59/part3.html",null))
				))
			)
		)
	);
}

Now, this is a fairly small list. In my case, it quickly became downright unwieldy. And more unwieldy it got, the more errors cropped up. After trying to get it to display for several hours, I decided that it might just be easier to use a simpler data type. So what I did was create separate arrays for each list, and nested in such a way that would make the linking obvious at a glance:

var m            = new Array(); //an empty array that will hold the final Objects.
var manufacturer = new Array(); //list 1
var model        = new Array(); //list 2
var level        = new Array(); //list 3
 manufacturer[0]="ACURA";
  model[0]="CL";
   level[0]="2.2 PREMIUM";
   level[1]="2.3";
   level[2]="3.0";
   level[3]="";
  model[1]="EL";
   level[4]="PREMIUM";
   level[5]="SE";
   level[6]="SPORT";
   level[7]="";
  model[2]="INTEGRA";
   level[8]="GS";
   level[9]="GS-R";
   level[10]="LS";
   level[11]="RS";
   level[12]="SE";
   level[13]="TYPE-R";
   level[14]="";
 manufacturer[1]="ALFA ROMEO";
  model[10]="SEDAN LS";
   level[31]="LEVEL N/A";
   level[32]="";
  model[11]="SPIDER";
   level[33]="LEVEL N/A";
   level[34]="VELOCE";
   level[35]="";
  model[12]="";
  ...

I used a simple array for each menu. I didn't need to go into any multi-dimentional arrays because my client didn't want to use any codes, meaning that I only had to worry about the text property. Like any array, each one starts at zero, and increments by one. Each subsection was punctuated by a blank entry (""). In the example above, the first manufacturer, "Acura," links to the "CL," "EL," and "Integra" models. You can also see that the "CL" model links to three levels, the "EL" model also links to 3 levels, and the "Integra" links to 6 levels.

To make these arrays into the final Object array, I made the following function:

function initLists() { 
  if (!v) return 0; 
  var args = initLists.arguments; 
  var argLen = args.length - 1; 
  var temp = new Array(); 
  //this loop iterates through each 2nd argument (arrays) 
  for(var arg=argLen; arg>0; arg-=2) {
    var list = args[arg-1];     
    var a = args[arg];
    var newSubI = 0, oldSubI = 0;
    for(var i=0; i < a.length ;i++) {    
      if (a[i] != "") {
        //create a new Option with the appropriate submenu
        temp[temp.length] = new O(a[i], null, (arg==argLen ? null : m[oldSubI++]));
      }
      else {
        temp = temp.sort(compare);
        m[newSubI++] = temp; 
        temp = new Array(); 
      }
    }
    //reset the length of array
    m.length = newSubI;
  }
  //set up the listboxes 
  //get the root list
  var elt = args[0];
  
  if (typeof (elt=elt.form.elements[get(elt)-1]) != "undefined")
    relate(elt,m,1); 
  
  return 1;
}

The function can handle any number of arguments in an alternating menuName, menuContentsArray... format.

Here is a new function I wrote to sort the list contents, based on the text property:

function compare(a,b) {
   return (a.text > b.text)? 1 : -1; 
}

The init function is called from the onLoad event in the body tag:

initLists(document.oneform.ModelName,model,document.oneform.ModelLevel,level)

My storage of the list items is different from the original script in that I also have an array to store the base menu's contents. The objects are only used to populate the child menus and do not store any info about the base menu. This can lead to synchronization problems between the base list and the linked child menus. For example, lets say my client forgot a couple of Manufacturers. No problem, just add them to the list. But guess what? Now the manufacturers want link to the right models unless we also alter the arrays. It seemed clear that since the arrays contain all of the info, we might as well just use this as our source data. I decided to write a separate function to initialize the base menu ( Manufacturers ), called fillBaseList():

function fillBaseList(index, elt, a) {
  if (v) { 
    //create a new Option for each array element 
    for(var i=0; i < a.length; i++) a[i] = new O(a[i],null);
    //fill the list
    update(index,elt,a);
  }
}

Getting back to the onLoad event, we would insert this function before the initLists():

onLoad="fillBaseList(0,document.forms[0].Manufacturer,manufacturer);initLists( ...

Future Enhancements

A major setback with my version of the related menus is that it is not easy to change the contents of the list boxes because the arrays are incremented from zero. That means that if you were to change list item 12 in a 200 item list, you would have to change all the array indexes from 12 to 200! From a maintenance point of view, this would not be a good thing. Especially when you consider how often vehicle information is likely to change. Luckily, all is not lost. I found a couple of solutions to this problem. The first thing I did was write a small program to create the arrays. By passing it a couple of parameters, it would allow the user to extract all of the info from a database and create the required arrays in a separate JavaScript file. In fact, by implementing the FTPing into the app, my client could change the lists anytime, literally with the push of a button. This does require quite a bit of extra setup work on the part of the developer, but it's worth it, because you never have to do it again.

Another solution would be to use a server-side script such as ASP or Perl to read the data from an on-line database and populate the lists with fresh data each time a visitor requests the page. The advantage of using this approach is that you could dynamically fill the lists with all the items, just in case that your visitor's browser doesn't support the Option object. In my case, this solution was not as preferable as the first, mainly because my client's main database is located off-line, on his own PC, but in many cases this would definitely be the way to go.

By using simple data types instead of Objects, I had to dispense with the list values. This worked for us, because my client wanted the text to be displayed in the form contents table, but, had he wanted to use codes, I would have had to use multi-dimentional arrays, and reintroduce some extra complexity to the script in the process.

Download the full script (zipped file)

# # #

About the author: Rob Gravelle is a Java Developer by day, and the guitarist for the melodic metal band Ivory Knight, by night. Torn between pursuing a music career and paying the bills, Rob chose to walk the fine line between recklessness and responsibility when he enrolled in the Computer Programmer course at Algonquin College, in 1996. A short time later, his love of Web page design developed into a full-fledged career when he was hired by for Citizenship & Immigration Canada. Now he designs large-scale systems for national organizations. Robert's band's Web site can be found at http://www.IvoryKnight.com. He can be reached at blackjacques@musician.org.

Comments are welcome


Created: Jan. 30, 2001
Revised: Jan. 30, 2001

URL: http://webreference.com/dev/menus/oneform.html