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
|
![]() |
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
|
![]() |
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:
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 |
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).
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.
|
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.
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.
An anonymous function is a function without a name. 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:
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:
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.
JavaScript 1.3 Overview - Part 1
JavaScript 1.3 Overview - Part 2
Advanced Event Registration Models
Synchronous vs. Asynchronous Processing Explained
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.