Tuesday, April 22, 2008

Universally Related Popup Menus AJAX Edition: Part 3

The XmlHttpObjectManager Object

The XmlHttpObjectManager is a private member variable of the callServer() function (line 141). It's 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 the object has finished bringing back all the data from the server (line 155). There are a total of five possible states but we're testing against a value of 4. Here's 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)

141var 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
179var 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's used to provide the state_change() function with the objects and variables 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 supply the list object for this purpose. All 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. While it shares some of the same properties such as length, it doesn't implement all the methods of a true Array object. There are two ways around this: to write our own slice() function, or, to steal the existing one from the Array object! Once we've copied the slice() function over to the arguments property (line 215), we can call it as if it always existed (line 217). The return type is a function because the onreadystatechange property expects one (line 219).


211Function.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 with various methods.

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's possible 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, 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:


250var 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 {
265 //Safari 3 bug workaround code
266
267
268 if ( window.BLANK_ENTRY ) options[0] = new Option(window.PROMPT_TEXT, "");
269
270 eval(xmlhttp.responseText);
271
272 //more Safari 3 bug workaround code
273
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 didn't 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
266if (window.IS_SAFARI) list.style.display='none';
267
268if ( window.BLANK_ENTRY ) options[0] = new Option(window.PROMPT_TEXT, "");
269
270eval(xmlhttp.responseText);
271
272//Safari 3 bug workaround
273if (window.IS_SAFARI) style.display='block';

view plain | print | ?

After populating the list, there's 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.
277window.BASE_LIST.setDefaultIndex( list.options.length );
278
279fireOnChangeEvent.call(list);
280}

view plain | print | ?

Any status other that 200 is an indication that the items couldn't 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}

No comments: