URPM: AJAX EDITION

by Rob Gravelle (rgconsulting(AT)robgravelle(DOT)com)

 

Welcome to the fourth installment of my Universally Related Popup Menus (URPM) series! You might think that I'd feel like an actor after a successful trilogy who revives a tired character one more time, but quite honestly, I couldn't be more excited about where things stand! I think that you'll agree that AJAX has really ushered in a whole new paradigm of browser and server interaction and end user satisfaction. If you haven't yet heard of it, prepare to be amazed!

 

Legend:

blue text:          JavaScript Code
green text:        HTML markup
red text:            ASP code
purple text:       XML markup
maroon text:     SQL

Side Bar


Italic text:          Script elements     

 

AJAX in a Nutshell

AJAX stands for Asynchronous JavaScript And XML. Unlike Perl or Java, AJAX is not a distinct language, but rather an extension of JavaScript which is used in conjunction with XML and DHTML. AJAX was created to solve a nagging problem that developers of dynamic web content were faced with: how to refresh web data without reloading the entire page. Before the advent of AJAX, developers had to resort to browser dependent hacks such as IFrames or more esoteric solutions such as Java applets. In JavaScript, the XMLHttpRequest object is what allows the browser to make asynchronous server calls behind the scenes. Because the execution of AJAX processes can proceed independently, other processes may be started before the asynchronous process has finished. In other words, the user can continue to interact with the page. Otherwise, the browser would "freeze up" while it waits for the response from the server. We'll be looking at the XMLHttpRequest object in a lot more detail a little later on.

 

AJAX vs. XML

In the URPMs Version III article, I used XML to logically represent the contents of several related dropdown lists. To make it even easier on the developer, an ASP script generated the XML code from a database. Here is an example of the resulting XML code:

 

XML:

<?xml version="1.0" encoding="windows-1252" ?>

<URPMs>

  <m1 value="1" desc="ACURA">

    <m2 value="2" desc="CL">

      <m3 value="7">2.2 PREMIUM</m3>

      <m3 value="4">2.3</m3>

      <m3 value="5">3.0</m3>

    </m2>

    <m2 value="8" desc="EL">

      <m3 value="10">PREMIUM</m3>

      <m3 value="1">SE</m3>

      <m3 value="9">SPORT</m3>

    </m2>

    <m2 value="1" desc="INTEGRA">

      <m3 value="3">GS</m3>

      <m3 value="6">GS-R</m3>

      <m3 value="2">LS</m3>

      <m3 value="18">RS</m3>

      <m3 value="8">SE</m3>

      <m3 value="17">TYPE-R</m3>

    </m2>

  </m1>

  <m1 value="2" desc="ALFA ROMEO">

    <m2 value="12" desc="SEDAN LS">

      <m3 value="23">LEVEL N/A</m3>

    </m2>

    <m2 value="13" desc="SPIDER">

      <m3 value="24">LEVEL N/A</m3>

      <m3 value="25">VELOCE</m3>

    </m2>

  </m1>   

...

 

This structure makes it quite easy to see how each child list relates to its parent node. After reading a lot of responses from people and playing with the sample page myself, there is no question that it was a versatile and useful implementation of the URPM concept. However, there was still one issue that it did not resolve and that is the initial page size. The problem is that all possible selections must be download along with the page. Depending on the size of your lists, that can amount to a lot of unnecessary text! ( On the plus side, changes in the list contents are lightning fast. :-) ) AJAX circumvents this issue by loading the child lists when a selection is made instead of when the page first loads. A server-side script retrieves items from a database or flat file and then returns them to the page. Small amounts of data like this will usually load fairly quickly so that any delay in loading is minimal and hardly noticeable at all. As a side benefit, retrieving only the relevant list items resulted in far simpler server-side code than generating complex nested XML nodes.

 

JavaScript Gets a Makeover

The JavaScript has been updated from version 1.2 to 1.3 in order to take advantage of the new call() and apply() Function object methods. Both methods allows you to share the same object between any number of functions. This allows us to pass it to a function so that it runs within the scope of the object. In other words, the supplied object will be accessible to the function via the this pointer. The call() function accepts any number of parameters, while apply() takes only two, as the second parameter is an array. Later on, we'll see how to recycle a function's arguments by passing the Function.arguments property to the apply() function. Here is some sample code for the call() and apply() functions:


function car(make, model, year)
{this.make = make, this.model = model, this.year = year}




function hireCar(carNo, make, model, year)
{this.carNo = carNo, car.call(this, make, model, year)}


                      OR


function RentalCar(carNo, make, model, year)
{this.carNo = carNo, car.apply(this, new Array(make, model, year))}


Part 1: Running the Sample Script

I made modifications to the ASP scripts from the last article on the same IIS 5 server as before. Therefore, you will also need to have a web server which supports ASP at your disposal unless you plan on using a different server-side language.

 

There are 5 files included with this article.  Here's an explanation of each:

1. Vehicle lists 2000.mdb:
An Access database.  It should reside in a folder called "URPMs db" located directly parallel to the "WWWRoot" folder.

2. URPMs.dsn
A DSN file containing database connection information.

 

All the following files should be placed in a folder called "URPMs", directly under the "WWWRoot" folder.

3.  testAJAX.htm:
The main html page - used for testing the script.

4. URPM_AJAX.js:

The JavaScript file that contains all of the client-side AJAX and URPM functionality.

5. getListItems.asp

The server-side script that retrieves the list items from the database.

 

Unzip the files to the parent directory of your server’s WWWRoot.  This will probably be Inetpub folder. Here is a diagram of the resulting directory structure, once the files have been extracted:

<c:\inetpub>
   |
   |
WWWRoot------URPMs dB>vehicle lists 2000.mdb
   |                  URPMs.dsn
   |
   |
 URPMs------>testAJAX.htm

             URPM_AJAX.js

             getListItems.asp


            

Try it out!

To see the script in action, open up your favorite browser and type something like the following into the address bar, where <servername> is the name of your server. If it's running on your desktop, it may very well be "localhost". Otherwise, you'll have to know the IP address or server name:

 

http://<servername>(:port)/UPRMs/testAJAX.htm

 

This is what I had to type to bring up the sample page:

http://localhost/URPMs/testAJAX.htm

 

 

Part 2: Using the URPMs in Your Own Web Page

To use the script with your own asp page, you will have to make some changes to your database as well as add some simple HTML to your web page.  Here's what you have to do:

 

1: Create the File DSN 

This is the exact same File DSN as in part III.  To create the DSN:

<input type=hidden name=FileDSN id=FileDSN value="C:\MyDB\myDSN.dsn">

That's all you really need to do, but if you don't want to allow users to be able to write to the database, you can hit the "Options>>" button and check the "Read Only" checkbox.  You could also enter a user ID and Password in the Advanced dialog if your database requires it. 

 

2: Design the Query (or Queries) 

Every time the getListItems.asp script is called it runs a stored query (also known as a stored procedure) in the database. I like keeping the queries in the database because they're a lot easier to design and test from within the database. If you open up the Vehicle lists 2000.mdb database and look at the Queries Objects you'll see that there is a query for each URPM. In a larger scale database such as MySQL or Oracle, you could develop one stored procedure to handle the retrieval for all the lists' items! Unfortunately, you can't pass table or column names as parameters in Access, so you have to have one for each. To keep some consistency between the queries and the HTML, I named the queries "get" + the Select list's ID. For example, the query name for the Manufacturers Select element is called getManufacturers. Use whatever names fit your selects' contents, but make sure to name your queries "get" plus the select's ID. See line 32 in the getListItems.asp file to see how the list's ID is used to derive the query name.

 

Designing the query for the base list is easy since there are no parameters to worry about. All that's required is that you select the ID and Description fields from the table, in that order. The query below weeds out the null rows, but that may not be an issue for you. Finally, use an ORDER BY clause to sort if you wish. This in no way affects the URPMs' behavior. The following SQL retrieves the IDs and descriptions from the Manufacturer table and sorts the results by the description:

 

SELECT Manufacturer.ID, Manufacturer.Description
FROM Manufacturer
WHERE (((Manufacturer.ID Is Not Null)
AND ((Manufacturer.Description) Is Not Null))
ORDER BY Manufacturer.Description;

 

The queries for the child lists require slightly more SQL code because of table joins and the ID parameter, but they're still quite simple as far as SQL statements go. Since we want all the items that are related to the parent ID, there has to be a foreign key in the table linking back to the parent ID field. Here is the SQL for the getLevels query:

 

SELECT Level.[Level ID] AS ID, Level.Level AS Descr FROM Model
INNER JOIN [Level] ON Model.[Model ID] = Level.[Model ID]
WHERE (((Level.[Level ID]) Is Not Null)
AND ((Level.Level) Is Not Null) AND ((Model.[Model ID])=[ID]))
ORDER BY Level.Level;

 

These queries bring back a much smaller recordset than the version III URPMs query because that version had to not only get the contents for all the lists, but it had to maintain their relationships as well. This had a tendency to result in a lot of repetition. Just have a look at this small sample of the deprecated getLists query:

 

Sample Output from the URPMs III getLists Query
Sample Output from the URPMs III getLists Query

 

Compare that with the typical output of one of the new queries. Here's the list of model levels that relate to an Audi model:

 

Sample Output from the getLevels Query
Sample Output from the getLevels Query

 

Yep, that's the whole thing. Notice the difference in number of rows: 5 vs. 919! That's the difference that retrieving list options when a selection is made as opposed to when the page first loads makes.

 

3: Modify Your HTML Page:

Add the following code to the HEAD of your document:

<STYLE type="text/css">
  	.saveHistory {behavior:url(#default#savehistory);}
</STYLE>
<META NAME="save" CONTENT="history">

This code tells Internet Explorer to save the dynamically generated list options and selections between pages.

 

You must also add "class=saveHistory" to the select tags of each URPM so that I.E. knows which elements will keep their contents. For example:

 

<select name="lstManufacturers" ID=lstManufacturers class=saveHistory>

 

Also add the following code to link the JavaScript to the Html page:

 

<script language="Javascript1.3" src="URPM_AJAX.js">

 

Add the onload event hander to the <BODY> tag to initialize the lists, passing the URPMs' IDs to the initialize() function.:

 

<BODY onload="initialize('URPM1', 'URPM2', 'URPM3')">

Convert the "reset" button, if applicable, to a simple "button" type and add the following code to the onClick() event.  This is done so that the form resets before the lists.:

 

<input name="myButton" type=button value=reset onClick="this.form.reset(); resetLists();">

 

OPTIONAL STEP: Parameters are passed to the URPM_AJAX.js script via hidden fields. This saves you from having to muck around in the script, which makes a lot of people nervous. In fact, unless you want to do something special, you shouldn't have to modify the getListItems.asp or URPM_AJAX.js scripts at all! There are five parameters that can be set. The first is the showBlank field. It tells the script to include a blank item at the top of the list which would be used in the case of non-mandatory fields. It will accept any non-whitespace and non-zero value as true. Hence, "True", "true", "False", "dog", "whatever" would all evaluate to true. Conversely, a value of "0" or "false" would be interpreted as false. Moreover, the absence of a value or the hidden field itself will also default to false.

 

<Input type=hidden name=showBlank id=showBlank value="true">

 

The next hidden field is promptText. If you'd rather have some text instead of a blank field, you can set the prompt text such as "Please Make a Selection" or "Levels" here. If you include a value in this field, showBlank is redundant and therefore, not required.

 

<Input type=hidden name=promptText id=promptText value="make a selection will ya">

 

The defaultIndex field sets the listIndex on the base list when the page first loads. It is zero-based and does not include the optional blank entry. The following HTML code will select the eleventh item as the default value if showBlank is false and the twelfth if it is true:

 

<Input type=hidden name=defaultIndex id=defaultIndex value="10">

 

The final two fields relate to the location of your server-side script. The url field can be used to supply your own script instead of getListItems.asp.

 

<Input type=hidden name=url id=url value="getWidgetsForLists.asp">

 

The FileDSN field is used to provide your own file DSN for the database. The default path, stored in the getListItems.asp file is "c:\inetpub\URPMs db\URPMs.dsn". If you want to store the DSN in a different folder, or wish to rename it, you'll have to use this field.

 

<Input type=hidden name=FileDSN id=FileDSN value="D:\myWebFolder\Related Lists\myServerSideScript.php">

 

4: Copy the database, DSN, HTML, JavaScript, and ASP/server-side script to your server.

And that's it. You're good to go.

 

 

Part 3: Customizing the Script

 

For those of you who like to tinker with things or have additional functionality that you'd like to add to the scripts, here is a line by line explanation of the JS and ASP scripts to help get you better acquainted with the code.

 

At the very top of the URPM_AJAX.js file there is a couple of global "constants". I could have used real constants by going with JavaScript 1.5, but I felt that 1.3 had more browser support at this time. Hence, these are actually just regular variables. Constants can be distinguished from regular variables by their uppercase names. The DEFAULT_URL stores the name of the default server-side script. If you wish to substitute your own script, remember to use a hidden field in your page, as outlined in part 2. You should never have to change this value. We need to detect Safari browsers because there is a bug in version 3 (at least on Windows 2000).

 

1	var DEFAULT_URL = "getListItems.asp";
2	var IS_SAFARI = ( navigator.userAgent.toLowerCase().indexOf('safari') != -1  );


The initialize() function is the first to be called.  It is called from the BODY tag's onLoad() event with the IDs of each URPM, starting with the base list:

 

<BODY onload="initialize('baseList', 'firstChild', 'secondChild', 'thirdChild')"/>

 

The first call within the initialize() function (see line 5) is to setOptionalProperties() (line 7). It sets some global constants from hidden fields in the html document. However, it is within the initLists() function that most of the work takes place (line 15). Some of its duties include binding the onchange event to our script and setting up the AJAX XmlHttpRequest object. The initLists() function returns true if it's safe to proceed and populate the lists and false if any errors are encountered. Although initLists() runs every time the page loads, the lists are only populated on the first page load because the list options and selections should be retained between page refreshes. If we repopulate the lists every time the page loads, we would wipe out the previous selections! The test to decide whether or not to populate the lists is the presence of options (line 15). In other words, once they're there, we don't load them again. The call to callServer() (line 15) loads the base list and causes a cascade effect to all the child lists. Here is the code for the initialize() function:


5	function initialize()
6	{
7		setOptionalProperties();
8
9		//This is only done the first time loading so that the lists will retain
10		//their values when using the back and forward buttons on the browser.
11		//The only side effect is that using the refresh button will not reset the
12		//lists in IE.  Instead, you have to use the "Reset" button on the form
13		//or highlight the URL in the address field and hit enter to force a page reload.
14
15		if(initLists(arguments) && !window.BASE_LIST.options.length) window.BASE_LIST.callServer();
16	}

Lets take a closer look at the setOptionalProperties() function (line 18) . The optional properties, like all variables, have to be initialized each and every time as the values are lost between page reloads. Global variables have been prefaced with the window namespace to explicitly show that all global variables are appended as a property to the window Object when created. It's also a useful way to keep track of them. Here is the code for the setOptionalProperties() function:


18	function setOptionalProperties()
19	{
20		var getHiddenFieldValue = function( fieldId )
21		{
22			var field = document.getElementById(fieldId);
23
24			return field ? field.value : false;
25		}
26
27		var convertToBoolean = function( str )
28		{
29			var booleanValue = false;
30
31			try
32			{
33				//this works on true and false and numbers such as 0 and 1, and blank strings
34				booleanValue = eval(str);
35
36				booleanValue = Boolean(booleanValue);
37			}
38			catch (e)
39			{
40				booleanValue = Boolean(str);
41			}
42
43			return booleanValue;
44		}
45
46		window.BLANK_ENTRY = convertToBoolean( getHiddenFieldValue("showBlank") );
47
48		window.PROMPT_TEXT = getHiddenFieldValue("promptText");
49		if ( window.PROMPT_TEXT ) window.BLANK_ENTRY = true;
50
51		window.URL = getHiddenFieldValue("url");
52		if ( ! window.URL ) { window.URL = window.DEFAULT_URL; }
53
54		window.FILE_DSN = getHiddenFieldValue("FileDSN");
55	}

The getHiddenFieldValue() (line 20) function above is used to retrieve the optional properties from the hidden form fields. There are a few scenarios that it has to deal with, including:

  1. No field present.
  2. No value assigned.
  3. Data type conversion required.

Dealing with all these possibilities is a lot easier than might first appear. Since all form control values are read in as strings, simply testing for the field is enough. There's no need to check for a blank value since that could be valid. A value of false is returned if the field isn't there because it's the most unambiguous value to test for in an if statement. The function is stored in a local variable to show that it's private:

20	var getHiddenFieldValue = function( fieldId )
21	{
22		var field = document.getElementById(fieldId);
23
24		return field ? field.value : false;
25	}

In the case of the BLANK_ENTRY variable (line 46), a bit of conversion is necessary to get the proper value in order to convert "true" and "false" strings to their respective boolean equivalents. Sounds easy, but it's not as trivial a matter as it first appears. The challenge is that the Boolean() function (lines 36 and 40) converts any non-empty string to true! One way to get around this inconvenience is to use the eval() function (line 34). It will attempt to interpret any string as JavaScript code. In doing so, it will also throw errors for any bad syntax that it encounters. Once the string has been evaluated, the Boolean() function must still be used because numbers such as 0 or 1 will be stored as integers. If eval() does not recognize the string as JavaScript code, we call the Boolean() function directly so that any non-empty string besides "false" will evaluate to true.

The Boolean function is used to convert a non-Boolean value to a Boolean value. Creating a new Boolean object will also work, but instanciating a new object for a one time use is somewhat wasteful.

x = Boolean(expression) //preferred
x = new Boolean(expression).valueOf() //don't use

Note: If the Boolean object has no initial value or if it is 0, -0, null, "", false, undefined, or NaN, the variable is set to false. Otherwise it is true (even with the string "false")!

Here's the code for the convertToBoolean() function:

27	var convertToBoolean = function( str )
28	{
29		var booleanValue = false;
30
31		try
32		{
33			//this works on true and false and numbers such as 0 and 1, and blank strings
34			booleanValue = eval(str);
35
36			booleanValue = Boolean(booleanValue);
37		}
38		catch (e)
39		{
40			booleanValue = Boolean(str);
41		}
42
43		return booleanValue;
44	}

The next function, initiLists() (line 58), is where all of the URPMs' setup is done. It loops through each ID passed to the arguments array, and attempts to get a reference to each element using the Document Object Model's (DOM) getElementById() function. This is the preferred way to reference form elements nowadays. Previously, you had to use document.formname.elementname notation. This was inconvenient because you had to keep track of the form object as well as the element! The getElementById() (line 22) function deals with this issue by ignoring the form altogether.

The last variable (line 324) stores a reference to the list form element that we initialized on the previous run through the loop. Hence, the base list won't have anything in the last variable. The next property (line 332) is added to the last variable and set to the current list so that each list points to the next one, similar to linked list:

A Doubly Linked List

The first time through the loop, we call the initBaseList() function (line 335) using the JavaScript 1.3 call() method (line 335), passing the list as the object, with no arguments. It returns the browser-dependent function code to fire the onchange event programmatically, which is then stored in the fireOnChangeEvent (lines 324, 335) variable.

The bindOnChangeHandlerToList() function (line 337) is called for all except the last list. As the name suggests, it binds our changeHandler function to the list's onchange event by adding it as a listener. Once the function is bound, the fireOnChangeEvent() function will indirectly cause the setSublist() function (line 337) to be called within the scope of the list via the onchange event. We don't bind the setSublist() function (line 337) to the last child list, because it has no sublist to set.

An alert message (line 341) is displayed and the function aborted if any of the lists are not found. This message is intended for people who are using the script rather than web clients, since the only thing that can cause the error is probably invalid syntax. Here is the code for the initializeLists() function in its entirety, excluding local functions:

58	function initLists( args )
59	{
		//...lots and lots of code

322		//initLists() function body starts here**********************************************
323
324		var list, last, fireOnChangeEvent;
325
326		for (var i=0; i<args.length; i++)
327		{
328			list = document.getElementById(args[i]);
329
330			if ( list )
331			{
332				if (last) last.next = list;
333				last = list;
334
335				if ( i == 0 ) fireOnChangeEvent = initBaseList.call(list);
336
337				if (i < args.length - 1 && !bindOnChangeHandlerToList.call(list, setSublist)) return;
338			}
339			else
340			{
341				alert("'" + args[i] + "' is not a valid ID. Unable to link the lists.");
342
343				return;
344			}
345		}
346
347		return true;
348	}

The base list has its own initialization function because it has more responsibility than the child lists. These include holding a pointer to the callServer() function and determining the browser-dependent code to fire the onchange event. Its initialization function is called initBaseList() (lines 87, 335).

The first variable assignment in initBaseList() creates the global BASE_LIST reference and stores it using the this pointer (line 131). In fact, most of the functions are called within the scope of the list object so that we are always able to easily reference it. The next line creates a reference to the local callServer() function by creating a property of the same name (line 133). This has to be done because by the time callServer() is called, we no longer have a reference to it as it is private to the initLists() function.

87	var initBaseList = function()
88	{
		//...private functions


131		window.BASE_LIST = this;
132
133		window.BASE_LIST.callServer = callServer;
134
135		return getFireEventFunction.call(window.BASE_LIST);
136	}

The base list again takes on the this scope for the getFireEventFunction() function (line 89). Here, it plays a key role because the function code is determined based on the syntax that the browser recognizes rather than by using traditional browser sniffing techniques. It's a whole lot easier to test for this.fireEvent than for the browser type, version, and platform - especially since many modern browsers have been known to spoof Internet Explorer!

The getFireEventFunction() function returns one of two functions based the evaluation of the this.fireEvent statement (line 93) If it is recognized by the browser's JavaScript interpreter, then it is assumed that the browser is Internet Explorer, or at least behaves like it! In this case, the function calls the proprietary IE fireEvent() function with "onchange" as the parameter (line 94). For Level 2 DOM Mozilla type browsers, a bit more code is required. First we have to create an event of the type HTMLEvents (line 97). Other types include UIEvents, MouseEvents, and MutationEvents. Next, we have to specify exactly which kind of event we want to call, using the initEvent() function (line 98). For HTMLEvents, this covers 'abort', 'blur', 'change', 'error', 'focus', 'load', 'reset', 'resize', 'scroll', 'select', 'submit', and 'unload'. It takes two additional arguments: whether or not the event bubbles, and whether or not it's cancelable. Finally, we call dispatchEvent() to fire it (line 100).

DOM Level 2 Events

Event Module Event type to pass to createEvent Method to be used to initialize the event
User Interface event module "UIEvents" event.initUIEvent
Mouse event module "MouseEvents" event.initMouseEvent
Mutation event module "MutationEvents" event.initMutationEvent
HTML event module "HTMLEvents" event.initEvent
The Event Object Methods
HTMLEvents and generic Events initEvent( 'type', bubbles, cancelable )
UIEvents initUIEvent( 'type', bubbles, cancelable, windowObject, detail )
MouseEvents initMouseEvent( 'type', bubbles, cancelable, windowObject, detail, screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget )
MutationEvents initMutationEvent( 'type', bubbles, cancelable, relatedNode, prevValue, newValue, attrName, attrChange )

 

89	var getFireEventFunction = function()
90	{
91		var BUBBLES = true, CANCELABLE = true;
92
93		return 	( this.fireEvent	//IE
94			  ? function() { this.fireEvent("onchange"); }
95			  : function() 		//Non-IE
96			  {
97				var	evt = document.createEvent("HTMLEvents");
98				evt.initEvent("change", BUBBLES, CANCELABLE);
99
100				this.dispatchEvent(evt);
101			  }
102			);
103	}

In the initializeLists() function above (line 58), we call the bindOnChangeFunctionsToListElement() function to bind the setSublists() function to the list's onchange event by adding it as a listener (line 337). We could just assigned it directly by writing "list.onchange = function { setSublist(this.id) };" but this would overwrite any existing functions in the SELECT's tag, such as "<SELECT size=1 onchange='validate(this)'>".

Adding an event listener to an event is not hard to do, but as always, cross-browser differences adds a bit of unavoidable complexity. Much like we saw in the getFireEventFunction() function (line 89), there are two means to the same end. In Internet Explorer, the attachEvent() function (line 75) is used, while in DOM 2 compliant browsers, you add a listener to an event by calling addEventListener() (line 71). The World Wide Web Consortium had their hands full when deciding on an event model because the two main standards were diametrically opposed, with Netscape using something called the capturing model, and Internet Explorer endorsing bubbling. Basically, the two camps emerged as a result of answering the questing of what should happen if an element within another element both have handlers for the same event: Should the parent (outer) element fire first, or should the child (inner element)? Netscape concluded that the parent should fire first. That's Event Capturing. Internet Explorer took the opposite stance and decided that the child should be the one to fire first! That's event bubbling. The W3C took the middle road in creating their own W3C Event Model whereby the event is first captured until it reaches the innermost element and then bubbles up again. This allows the developer to select either style of event firing by supplying the addEventListener() function with a Boolean argument. I used "constants" to show that true means capture and false means bubble (line 62). In practice, unless you have a specific reason to use capturing, stick with bubbling, as I did here.

Another browser difference is the way that the event is referenced. In Mozilla style browsers, the event is passed to the function as an argument, whereas in IE, it is stored in the window.event property. One way to combine these two very disparate models is to pass the firing element as an argument, since the source element is the only event property that we will be using anyway. This is typically done by testing for e.target. If that's a no go, the event's srcElement will reference the element instead (line 66).

The last line of the bindOnChangeHandlerToList() function returns true to the initLists() function so that it can proceed (lines 79, 337). However, should the browser fail to recognize either event registration models, an error message is displayed (line 79) and the function exits without returning a value to the initLists() function (line 81). This has the effect of halting execution of the script as the (non-)value of "undefined" propagates back to the initialize() function and evaluates to false (line 15).

60	var bindOnChangeHandlerToList = function( changeHandler )
61	{
62		var CAPTURING_PHASE = true, BUBBLING_PHASE = false;
63
64		var getEventHandler = function()
65		{
66			return function(e) { changeHandler.call(e.target ? e.target : event.srcElement); };
67		}
68
69		if( this.addEventListener )
70		{
71			this.addEventListener( 'change', getEventHandler(), BUBBLING_PHASE );
72		}
73		else if( this.attachEvent )
74		{
75			this.attachEvent( 'onchange', getEventHandler() );
76		}
77		else
78		{
79			alert( "Could not bind onchange event handlers to list.  Aborting." );
80
81			return;
82		}
83
84		return true;
85	}

Here is a more in-depth look at what happens when the callServer() function is called:

The keystone of AJAX in JavaScript are the XMLHttpRequest and XMLHTTP (Microsoft's ActiveX implementation) objects, which provide programming interfaces or API's that enables the performing of HTTP requests. We create the AJAX object using the static getXmlHttpInstance() method of the XmlHttpObjectManager class (line 225). The XmlHttpObjectManager is a static singleton that acts as XmlHttpRequest object factory. The XmlHttpRequest object that is returned is also a singleton because we reuse the same one for all server calls. If we are successful in creating it, we proceed to construct the URL which will be used to call the server-side ASP component (line 227). The list ID is appended as the first argument in the URL's hash (line 229, see sidebar below). The other two arguments are optional (lines 231, 233) as the code value does not apply to the base list and the file dsn does not have to be supplied unless it differs from the default one.

Portions of the URL

Knowing how to manipulate the different portions of the URL is a valuable asset in working with AJAX, so here's a quick overview of the URL's syntax:

URLs generally take the following form:
 
<protocol>//<host>[:<port>]/<pathname>[<hash>][<search>]

protocol Property
The protocol property is the string at the beginning of a URL, up to and including the first colon (:), which specifies the method of access to the URL.

host Property
The host property is a string comprising of the hostname and port strings.

hostname Property
The hostname property specifies the server name, subdomain and domain name (or IP address) of a URL.

port Property
The port property is a string specifying the communications port that the server uses.

pathname Property
The pathname property is a string portion of a URL specifying how a particular resource can be accessed.

hash Property
The hash property is a string beginning with a hash (#), that specifies an anchor name in an HTTP URL.
 
search Property
The search property is a string beginning with a question mark that specifies any query information in an HTTP URL.

 

Here's the code for the first part of the callServer() function:

138	var callServer = function( selection )
139	{
.		//Private classes and functions

.
225		var xmlhttp = XmlHttpObjectManager.getXmlHttpInstance();
226
227		if ( xmlhttp )
228		{
229			var url	= window.URL + "?list=" + this.id;
230
231			if (arguments.length) 	url += ( "&code=" + selection );
232
233			if (window.FILE_DSN) 	url += ( "&FileDSN=" + window.FILE_DSN );

I used a with statement (line 235) around the XmlHttpObjectManager because there are several calls to it in the next couple of lines. Namely, RUN._ASYNCHRONOUSLY, state_change, READY_STATE, and STATUS are all members of the XmlHttpObjectManager. The open() method initiates a connection to the server (line 237). It accepts three parameters. The first is "GET" or "POST", which tells the server that we will be downloading or uploading. The second argument is the URL of the resource that we want. The URL is encoded using the built-in JavaScript function encodeURI() in case the URL contains invalid characters such as spaces or ampersands. The third argument is a Boolean that tells the HmlHttpRequest object to either execute synchronously or asynchronously. The latter tends to be the superior choice because synchronous server calls can cause the browser to freeze up while waiting for a response. The next line sets the onreadystatechange event property to our state_change() function (line 239). The onreadystatechange event requires a function to call when the onreadystate property changes. This line is strategically placed between the open() and send() methods because this is the only way to use the same XmlHttpRequest object for multiple calls. If you set it before the open() method is called, the object will reset the onreadystatechange property, effectively losing the assigned function.

 

More on the encodeURI Function

The encodeURI() function encodes special characters, with the exception of: , / ? : @ & = + $ #. Use the encodeURIComponent() function to encode these characters.

 

The bind() function explicitly passes the variables that we will need to the state_change() callback function (line 239). While proactively passing the variables to the callback function is not as common as assigning an anonymous function to the onreadystatechange event property, it is rapidly gaining popularity as the preferred approach because it is the most surefire way to guarantee that we will still have a reference to them by the time the callback function executes. The problem with the anonymous function lies in the time lag between when the function is assigned to the onreadystatechange event property and when it is called by the XmlHttpRequest object. This is especially true of the this pointer because it could be referring to a different object by the time the function is called. This issue is known as the "loss of scope" problem.

 

The Anonymous Function

An anonymous function is a function without a name.
Here's an example::

xmlhttp.onreadystatechange = function( ){ alert("State Changed!"); };

 

Here is an overview of the variables and properties that we need to provide to the state_change() function:

As always, the this pointer refers to our list object (line 239). The xmlhttp variable has to be passed as well (line 240) because we will need to access its readystate and status properties. The fillListCallback argument is our callback function (line 241). The READY_STATE constant contains the state that the change_state() function will be looking for (line 242). Finally, the STATUS constant will be referenced by fillListCallBack() (line 243).

The XmlHttpRequest send() function is where the request is actually sent to the server (line 246). We supply a null because we aren't uploading anything.

235	  with ( XmlHttpObjectManager )
236	  {
237		xmlhttp.open("GET", encodeURI(url), RUN._ASYNCHRONOUSLY);
238
239		xmlhttp.onreadystatechange = state_change.bind(	this,
240								xmlhttp,
241								fillListCallBack,
242								READY_STATE,
243								STATUS );
244		}
245
246		xmlhttp.send(null);
247	  }
248	}

The XmlHttpObjectManager Object

The XmlHttpObjectManager is a private member variable of the callServer() function (line 141). It is a static class, so we never create an instance of it. Rather, it just houses some variables and methods that relate to the XmlHttpRequest object. Its main purpose is to act as a wrapper to the XmlHttpRequest object because we only create one that we reuse every time we want to call the server. The real object is stored in the private XmlHttpObject variable (line 143). To get a reference to it, we call getXmlHttpInstance(), which is one of two public functions exposed via the interface object (line 144). The interface object uses JavaScript Object Notation (JSON) which is a standardized way of denoting an object literal, much the same way that XML is used to describe data. Hence, its syntax is quite a bit different from regular JavaScript The entire object is located between two curly braces {}. Each public property is notated by its name, followed by a colon (:) and the value. The properties are then separated by commas.

Now let's take a look at our interface's methods and properties.

The getXmlHttpInstance() method (line 146) is what I like to call a "mutating function". Here's why. The first time it's called, it returns a new XmlHttpObject (line 150) but it also sets itself to a new function that returns the private xmlHttpObject variable (line 148). Oddly, this has no effect on the execution of the function because it's already on the stack, so the changes don't take effect until after the current one finishes executing. Every successive call afterwards receives the same xmlHttpObject variable.

The state_change() method is the second one that is accessible via the XmlHttpObjectManager's public interface (line 153). Its job is to check the xmlhttp.readyState property every time the xmlhttp.onreadystate property changes. We have to check the readyState property to make sure that the object has finished bringing back all the data from the server (line 155). There are a total of five possible states but you usually only want to do anything once you've got all your data, which is why we are testing against a value of 4. Here is the list of ready state values and their meaning:

  1. The request is uninitialized (before you've called open()).
  2. The request is set up, but not sent (before you've called send()).
  3. The request was sent and is in process (you can usually get content headers from the response at this point).
  4. The request is in process; often some partial data is available from the response, but the server isn't finished with its response.
  5. The response is complete; you can get the server's response and use it.

Once we hit the last loading state, we can run the call back function (line 159). Any errors that occur in the callback function will be trapped by the try/catch block so that a message can be displayed (line 163). The list is again passed along in the this pointer.

The following properties are also accessible via the interface object: RUN (line 171), READY_STATE (line 173), and STATUS (line 175). They are JSON objects containing named constants. Here's the first part of the XmlHttpObjectManager object, including its private interface variable (we'll see how it's made public in a moment):

141	var XmlHttpObjectManager = (function()
142	{
143		var xmlHttpObject 	 = null;
144		var interface		 = 
145		{
146			getXmlHttpInstance:function()
147			{
148				interface.getXmlHttpInstance = function() { return xmlHttpObject; };
149
150				return new XmlHttpObject();
151			}
152			,
153			state_change:function(xmlhttp, callBackFunction, READY_STATE, STATUS)
154			{
155				if (xmlhttp.readyState == READY_STATE._DONE_LOADING)
156				{
157					try
158					{
159						callBackFunction.call(this, xmlhttp, STATUS);
160					}
161					catch (e)
162					{
163						alert( "An error occurred in the AJAX call back method:"
164							 + "\nNumber:\t" + e.number
165							 + "\nName:\t" + e.name
166							 + "\nMessage:\t" + e.message );
167					}
168				}
169			} 	//public constants
170			,
171			RUN: { _ASYNCHRONOUSLY:true }
172			,
173			READY_STATE: { _DONE_LOADING:4 }
174			,
175			STATUS: { _OK:200 }
176		};
.		...

Our next stop is the XmlHttpObject() constructor class (line 179). The XmlHttpObject is a little different than your typical JavaScript class in that it sets the XmlHttpObjectManager's private xmlHttpObject variable (notice the lowercase "x") and it also returns it. In other words, the constructor returns a different object than the class (line 208). The constructor is used to create the XmlHttpRequest in a browser-independent way. The first couple of try/catch blocks are for Internet Explorer (lines 181, 187). The "new XMLHttpRequest()" constructor is used for all other DOM Level 2 compliant browsers (line 199). If it can't create the object, it displays an error message and returns false so that the script can abort (line 201).

178		//constructor
179		var XmlHttpObject = function()
180		{
181			try
182			{
183				xmlHttpObject = new ActiveXObject('Msxml2.XMLHTTP');
184			}
185			catch (e)
186			{
187				try
188				{
189					xmlHttpObject = new ActiveXObject('Microsoft.XMLHTTP');
190				}
191				catch (E)
192				{
193					xmlHttpObject = false;
194				}
195			}
196
197			if (! xmlHttpObject && typeof(XMLHttpRequest) != 'undefined')
198			{
199				xmlHttpObject = new XMLHttpRequest();
200
201				if ( ! xmlHttpObject )
202				{
203					alert("Your browser does not support the XMLHttpRequest object.");
204					return false;
205				}
206			}
207
208			return (xmlHttpObject);
209		}

The bind() function (line 211) is a private member function to the XmlHttpObjectManager. It is used to provide the state_change() function with the objects and variables that it needs when the XMLHttpRequest fires the onreadystatechange event. It does so by creating local variables within the function on which it's applied. The function that bind() is being applied to - state_change() in this case - is saved in the method variable (line 213). The first argument is the object which will define the scope of the callback function (line 211). We will be supplying the list object for this purpose. All the other arguments will be passed along to the calling function, but in order to do that, we have to separate them from the first argument. The Array object has the perfect method called slice(). It splits the array from the element number that we provide and returns the remaining elements as a new Array. Unfortunately, it turns out that the arguments property is not a true Array. Although it shares some of the same properties such as length, it does not implement all the methods of a true Array object. There are two ways around this: we write our own slice() function, or, we steal the existing one from the Array object! You know what they say about reinventing the wheel. Reuse! Once we've copied the slice() function over to the arguments property (line 215), we can call it just as if it always existed (line 217). The return type is a function because the onreadystatechange property expects one (line 219).

211		Function.prototype.bind = function(object)
212		{
213			var method = this;
214
215			arguments.slice = Array.prototype.slice;
216
217			var oldArguments = arguments.slice(1);
218
219			return function() { return method.apply(object, oldArguments); };
220		}

The last line in the XmlHttpObjectManager returns our interface object containing all the public methods and properties (line 222). Those parentheses immediately proceeding the function curly brace (line 223) (along with the opening parenthesis before the function) are used to create an inline function (line 141). This causes the XmlHttpObjectManager to be instantiated on the same line as it's declared. Thus, the only object that we can interact with is the interface which is what's actually stored in the XmlHttpObjectManager variable. The result is a static object that we can call various methods on.

141		var XmlHttpObjectManager = (function()
.			//code
.			//code
.			/more code
.

222			return interface;
223		})();

When the XmlHttpRequest object's readyState contains a value of 4 ( done loading ), the state_change() function calls fillListCallBack() (lines 159, 250). This is where the string from the server gets converted into list options. To act as a reminder that the function is executing within the list's scope, a variable by that name stores the this pointer (line 252). The HTTP status code is returned in the XmlHttpRequest's status property. If its value is 200 (for OK) (line 254), we proceed to check the responseText property (line 257). This is necessary because it is entirely possible that there are no linked items for that entry. The XmlHttpRequest object also has a responseXml property, but we don't use it because we treat the response as a straight string, and not an XML formatted document. We could have formatted the response as an Xml document on the server-side, but that would require parsing to be done at the JavaScript end. By formatting the response as JavaScript code, we can use the mighty eval() function to set all the options in one fell swoop (line 270)! Here is one of the shorter responses of Model options:

options[options.length] = new Option("METRO", "133");
options[options.length] = new Option("STORM", "134");

Again a with statement is used (line 263). It serves a specific purpose here as the server script does not include the list identifier. This was done in order to save bandwidth. Remember that each and every call to the server creates network traffic, so those two little words can amount to over 10% reduction! There is some code in the fillListCallBack() function for a Safari bug in Windows, which we'll get to after the following code snippet:

250	var fillListCallBack = function( xmlhttp, STATUS )
251	{
252		var list = this;
253
254		if ( xmlhttp.status == STATUS._OK )
255		{
256			//confirm that there are linked items
257			if ( !xmlhttp.responseText || xmlhttp.responseText == "N/A" )
258			{
259				list.options[0] = new Option("N/A", "");
260			}
261			else
262			{
263				with ( list )
264				{
.					//Safari 3 bug workaround code
.
268					if ( window.BLANK_ENTRY ) options[0] = new Option(window.PROMPT_TEXT, "");
269
270					eval(xmlhttp.responseText);
271
.					//more Safari 3 bug workaround code
274				}
.				...

Now it's time to take closer look at the code for handling a pesky Safari bug that I came across on Windows 2000. Once you've opened a listbox, any additional options than weren't there originally show up as solid black! They are present, but you can't see them. To get around this, I had to set the style before and after populating the list (lines 266,273). I got the idea because I noticed that it did not occur with multi-selects. Besides this problem, there were also issues with the onchange event being fired instead of onblur!

 

Here is the code inside the with statement for the Safari bug fix:


265		//Safari 3 bug workaround
266		if (window.IS_SAFARI) list.style.display='none';
267
268		if ( window.BLANK_ENTRY ) options[0] = new Option(window.PROMPT_TEXT, "");
269
270		eval(xmlhttp.responseText);
271
272		//Safari 3 bug workaround
273		if (window.IS_SAFARI) style.display='block';

After populating the list, there is a call to the setDefaultIndex() in the base list (line 277). This is the only global constant that can't be set with the others because setDefaultIndex() (lines 277,112) checks that value against the number of options in the list. Hence we have to wait until the base list has been populated. The fireOnChangeEvent() function is called so that the next list's options will be set accordingly (line 279). If you recall, we called the bindOnChangeFunctionsToListElement() function in initLists(). That binded the setSublist() function to the onchange event, which saves us from having to call setSubList() directly.

275			//this initialization code has to run here
276			//because ajax is running asynchronously.
277			window.BASE_LIST.setDefaultIndex( list.options.length );
278
279			fireOnChangeEvent.call(list);
280		}
.		...

Any status other that 200 is an indication that the items could not be retrieved due to an error. To keep things simple, the list is populated with one entry of "Ajax Error" (line 284) and an error message is displayed (line 284). The list variable is set to null at the end of the function to avoid memory leaks (line 289).

281		}
282		else
283		{
284			list.options[0] = new Option("Ajax Error", "");
285
286			alert( "The XMLHttpRequest object returned a status of '" + xmlhttp.status
287				 + ": " + xmlhttp.statusText + "'." );
288		}
289		list = null;
290	}

 

A complete list of HTTP response codes can be found on Wikipedia.

 

This brings us to our special setter method for the base list's DEFAULT_INDEX property (line 112). We need to call this function every time that the page loads, but only after the list has been populated, so that we can validate the default index against the options length. Getting the value from the hidden field is nearly the same than for other optional properties except for the conversion to an integer (line 118). To do the conversion, we can use the native parseInt() JavaScript function. It returns the first number that it locates in a String. Spaces are allowed, but if it encounters any characters that cannot be converted into some kind of numeric value, it returns NaN, which stands for "Not a Number". Once we have validated the number, we can safely set the DEFAULT_INDEX. We have to add the BLANK_ENTRY to it as well because it is not included in the index (line 124). Thus, if BLANK_ENTRY is set to true, then we want to add one to the number. This is easily done, because JavaScript converts a false Boolean to 0 and a true one to 1. Unlike other setters, the setDefaultIndex() function also sets the base list's selectedIndex without the accompanying fireOnChangeEvent() function (line 124). We don't need to since the fillListCallBack() function already calls it (line 279).

Like the getXmlHttpInstance() method of the XmlHttpObjectManager, this function is also of the mutating variety. Instead of checking for a condition to determine whether or not it's the first time through the fillListCallBack() function, it's far easier to simply call the function every time and let it take care of what it needs to. In this case, it simply resets itself to a bare-bones function that does absolutely nothing (line 128)!

112		this.setDefaultIndex = function( optionsLength )
113		{
114			var defaultIndexField = document.getElementById("defaultIndex");
115
116			if (defaultIndexField)
117			{
118				var defaultIndex = parseInt(defaultIndexField.value);
119
120				if (! isNaN(defaultIndex)
121				   	&& defaultIndex >= 0
122					&& defaultIndex < optionsLength - window.BLANK_ENTRY )
123				{
124					this.selectedIndex = this.DEFAULT_INDEX = (defaultIndex + window.BLANK_ENTRY);
125				}
126			}
127
128			this.setDefaultIndex = function() {};
129		}

The onchange event will cause setSubList() to execute (line 292), since we bound it in the bindOnChangeFunctionsToListElement() function (line 60). It populates the child lists. To do this, it uses the next property reference to its linked URPM (line 294). There is more browser specific code here to deal with a Safari bug where it has a nasty habit of calling the onchange event when it shouldn't! Since we can't be certain that an onchange event has actually occurred, we have to store the last selectedIndex and manually verify that it has changed (line 297). In the words of Borat: "Very nice.....NOT!". Before we go about retrieving the new items to put in the sublist, we clear the child list so that we don't have any remaining garbage in there (line 310).

The next step depends on whether or not a database item has been selected from the parent list. For example, the blank entry would not necessitate a server call. We can test for an index which is equal to or greater than the BLANK_ENTRY variable because JavaScript automagically promotes a Boolean to an integer when comparing to an integer (line 312). Since the selectedIndex property is zero-based, a value of one would be larger than the BLANK_ENTRY's index. In that case, the callServer() function would be executed within the sublist's scope, with the selected value as the parameter (line 314). If the blank entry was selected, we would still fire the onchange event so that the next list will be cleared (line 318).

292	var setSublist = function setSublist()
293	{
294		var sublist = this.next;
295
296		//safari 3 bug workaround
297		if (window.IS_SAFARI)
298		{
299			if (! this.lastIndex
300			   || this.lastIndex != this.selectedIndex)
301			{
302				this.lastIndex = this.selectedIndex;
303			}
304			else
305			{
306				return;
307			}
308		}
309		//clear the list's contents
310		with (sublist) { if( options.length > 0 ) while(options.length) options[0] = null; }
311
312		if ( this.selectedIndex >= window.BLANK_ENTRY )
313		{
314			callServer.call( sublist, this.options[this.selectedIndex].value );
315		}
316		else
317		{
318			fireOnChangeEvent.call(sublist);  //clears the next sublist
319		}
320	}

 

That is what it takes to set up the lists when the page first loads. Now we'll look at what happens when you select an item from a list.

The Onchange Event

Way back in the bindOnChangeFunctionsToListElement() function, the setSubList() function was bound to the onchange event. Getting the sublist is a snap because every URPM that has one contains a pointer to it in the next property. Any time the user selects an item from a list, it sets off a chain of events that updates all the URPMs passed to the initLists() function during the document onload(). Here is the chain of function calls during normal execution of the onchange event:

 

onchange event
|
list.setSubList()
|
Is a valid entry selected?
/ \
YES NO
| |
sublist.callServer() sublist.fireOnChangeEvent()
| |
change_handler.bind() list.setSubList()
. |
. etc
.  
onreadystatechange=4  
|  
change_handler()  
|  
list.fillListCallBack()  
|  
list.fireOnChangeEvent()  
|  
etc  

 

The Reset Event

When the Reset button is clicked, it calls the public resetLists() function (line 351), which in turn calls the base list's reset() method (line 105). It sets the default index (line 107) and calls the fireOnChangeEvent() function so that the child lists are set accordingly (line 109).

350	//call the list method
351	function resetLists() { if (window.BASE_LIST) window.BASE_LIST.reset() }



105	this.reset = function()
106	{
107		this.selectedIndex = this.DEFAULT_INDEX;
108
109		fireOnChangeEvent.call(this);
110	}

 

The Server-side ASP Code

The getListItems.asp script is responsible for generating JavaScript code to load the new items into each list. As mentioned at the start of the article, it turns out that this is a far easier task that generating an XML-formatted representation of all the URPMs relationships.

The first several lines are the same as the ASP script that supplied the Version III URPM list items. The first line sets the language to VBScript (line 1). The other options are PerlScript and JavaScript. The Option Explicit directive forces variable declaration using the Dim statement (line 3). There are three constants in the script. Unlike JavaScript, these really are constants! They have to be set when you declare them using the Const keyword. The first one sets the default file dsn (line 5). If you don't supply one in the HTML page, it will look for one called "URPMs.dsn" in a folder called "URPMs dB" one level above the "WWWRoot". The next two store the column indexes for the ID (line 6) and description fields (line 7). The ID should always be the first field returned by your queries, followed by the description.

The next line declares our global variables: oConn holds the database connection, cmd stores the proc command, and rs contains the recordset. responseString is the script output to be returned to the browser. FileDSN is the variable which is used by the Connection object (line 9). It may or may not contain the FILE_DSN constant, depending on whether or not one is supplied via an input parameter.

The On Error Resume Next line tells the script that we wish to continue with the script execution even when an error is encountered (line 11). Otherwise, it would exit and nothing would be returned to the AJAX XmlHttpReponse Object.

Set oConn = Server.CreateObject("ADODB.Connection") creates the database connection object (line 13).

The FileDSN string is set to the input parameter if there is one. Otherwise, we use our default (line 15).

Finally, we establish a database connection using the FileDSN as the argument (line 22).

1	<%@ Language=VBScript %>
2	<%
3		Option Explicit
4
5		Const FILE_DSN 		= "c:\inetpub\URPMs dB\URPMs.dsn"
6		Const ID_FIELD 		= 0
7		Const DESCRIPTION_FIELD	= 1
8
9		Dim oConn, cmd, RS, responseString, FileDSN
10
11		On Error Resume Next
12
13		Set oConn = Server.CreateObject("ADODB.Connection")
14
15		FileDSN = "FILEDSN="
16		if Request("FileDSN").Count then
17			FileDSN = FileDSN & Request("FileDSN")
18		else
19			FileDSN = FileDSN & FILE_DSN
20		end if
21
22		oConn.Open FileDSN
.	   	...

The next section of code deals with the Command object. First, we set the cmd variable to a new Command object (line 24) and set its ActiveConnection to the current one (line 25).

The next three constants are used by the cmd object (lines 27, 28, and 29). I chose to create my own rather than use the Microsoft ones for two reasons:

  1. VBScript does not recognize them as Visual Basic does.

  2. Although there is a workaround by including the "Adovbs.inc" in your script, the problem with that approach is that it's overkill for a mere three constants.

The next line sets the command type using one of our three constants. There are four types of command types:

Constant

Description

adCmdText

Evaluates CommandText as a textual definition of a command.

adCmdTable

Evaluates CommandText as a table name.

adCmdStoredProc

Evaluates CommandText as a stored procedure.

adCmdUnknown

(Default) The type of command in the CommandText property is not known.

The name of the proc is set according to the list's name. The format is:

"get" + UpperCase First Letter + lowercase remaining letters (line 32). This system is especially well suited to single word names such as "Manufacturers" and "Models" like I used in the sample page. For multiple words with spaces, the names don't look as good!

If the list ID is supplied in the code parameter, a new parameter is created and appended to the Command's Parameter Collection (line 34). All the lists except the base one should supply this value in the onchange event. The CreateParameter() function (line 36) takes five arguments. The first one is the name of the parameter. This has to match the name in your query! The next two arguments accept enumerated values, so I stored the values in the adVarChar and adParamInput constants for readability. The second argument sets the parameter data type. adVarChar is database parlance for a string. The third argument tells the function whether the parameter is of the input or output type. adParamInput denotes that the parameter is for input. The fourth argument is the length of the parameter. Ten is a more than safe length since the numeric ID will be passed to the server as a string. Hence, the length would be the number of digits in the ID. Finally, the last argument is the value of the parameter.

We then execute the proc, storing the results (line 39).

24	Set cmd = Server.CreateObject("ADODB.Command")
25	Set cmd.ActiveConnection = oConn
26
27	Const adCmdStoredProc 	= &H0004
28	Const adParamInput 	= &H0001
29	Const adVarChar 	= 200
30
31	cmd.CommandType = adCmdStoredProc
32	cmd.CommandText = "get" & UCase(Left(Request("list"), 1)) & LCase(Mid(Request("list"),2))
33
34	if request("code").Count > 0 then
35		' Assign a value to the parameter.
36		cmd.Parameters.append cmd.CreateParameter("ID", adVarChar, adParamInput, 10, request("code"))
37	end if
38
39	set RS = cmd.Execute

Now that we've got the list items, we have to convert the recordset because the AJAX XmlHttpResponse object is expecting a string to be returned. To do that, we first have to set the Charset (line 41). In IIS 5, the default charset is "utf-8". This does not accommodate French Characters. Why would we care about French characters? We have to be prepared because they do make an appearance in English from time to time. In the context of the automobile test lists, the "Mazda Protogé" requires the correct charset to display the last character. The one we want is "ISO-8859-1".

We have to initialize responseString to an empty string because we will be appending to it to create the response (line 43). Looping through a recordset is quite easy to do. We just test for the End Of File (EOF) cursor marker (line 44). As long as we haven't encountered it, we concatenate the JavaScript code that will be executed on the client-side (line 45). We don't have to know the list index because we are always adding the new Option at the end of the options array. That position is denoted by the options.length property (line 46). We have to escape double quotes in the description field because it will cause a syntax error in the JavaScript engine (line 47). The ID field doesn't need any doctoring because it only contains digits (line 48). movenext sets the cursor on the next record (line 50).

41	Response.Charset = "ISO-8859-1"
42
43	responseString = ""
44	While Not rs.EOF
45		responseString = responseString & _
46				"options[options.length] = " & _
47				"new Option(""" & Replace(RS(DESCRIPTION_FIELD),"""","\""") & _
48				""", """ & RS(ID_FIELD) & """);" & vbCrLf
49
50		rs.movenext
51	Wend

Before sending back the response string, we check for an error since we told the script to go ahead no matter what (line 53). If the Err object has been populated, the error message is passed along to the AJAX XmlHttpRequest object to display (line 54). Alternatively, if there are no list items for that parent ID, a value of "N/A" is returned to display in the list (line 56).

Finally, the responseString is returned to the client's browser (line 60), and the objects are cleaned up (line 62).

53		If Err Then
54			responseString = Err.Description
55		ElseIf Len(responseString) = 0 Then
56			responseString = "N/A"
57		End If
58
59		'this will return something even if there are no associated items.
60		Response.Write responseString
61
62		Set oConn = Nothing
63		Set cmd = Nothing
64		Set RS = Nothing
65	%>

Future Enhancements

Nothing in life is ever perfect, and despite having improved the URPMs several times over, there are still some things that can be improved upon.

For instance, the code that clears the list items is overzealous in its obliteration of all the list items. It would be more efficient to remove items only if there are leftovers remaining after the new ones have been created. The options' text and values could be changed for existing ones instead of creating new options.

Why not have default indexes for each list or even each linked URPM's list. That would allow each item of a list to result in a different default listIndex in the child list. Sounds like a good idea, but a lot of work. I'll let someone else run with it!

The ASP code to call the proc could be better developed for multiple word names. As it stands, and ID of "kitchen sinks" would call a proc called "getKitchen sinks". Not bad, but it looks weird and the space could be a problem.

That's just some small stuff. For your homework, implement a caching system on the client-side to store previously-selected list items and their related lists. That would potentially eliminate some server calls if the user decided to go back to a previous selection.

 

Conclusion

I hope you enjoyed this installment of the URPMs. I think that you'll agree that AJAX was tailor made for this sort of application. Although I wrote the script pretty much from scratch, with the advent of frameworks like Struts and Spring, this type of thing is bound to get a lot easier in the years to come. Until then, AJAX remains a giant leap forward in the quest for a rich web user experience.

 

References:

JavaScript 1.3 Overview - Part 1

JavaScript 1.3 Overview - Part 2

JavaScript Browser Support

DOM Events

Linked Lists on Wikipedia

Advanced Event Registration Models

How to Create DOM Events

Event Firing Order

JavaScript Closures

JavaScript Object Notation

Stored Procedures In Access

Synchronous vs. Asynchronous Processing Explained

The Adovbs.inc Constants File

The HTTP Charset Parameter

The Response.Charset Property

Charsets for Every Occasion

Struts Framework

Spring Framework

 

Robert Gravelle is a Senior Programmer/Analyst for the Canadian Border Services Agency as well as a freelance IT consultant. Rob specializes in Java, Perl, ASP, Microsoft application automation, JavaScript, VBScript, Web Development, and multi-tier systems. Feel free to contact Rob at rgconsulting(AT)robgravelle(DOT)com should you like more information, but note that, although Rob will do his very best to respond in a timely manner, due to the volume of emails received, Rob cannot guarantee a response to every email.