After starting to look at Ajax as a possible way to do brower-based crypto last week, I began working on an improved coding pattern for Ajax yesterday. Although yesterday's code version succeeded in getting rid of the dangerous global references to a single xmlHttpRequest object from within the asynchronous code, it still required the calling code to declare separate global xmlHttpRequest objects for each type of request that it might be doing. Today's version, which you can test here, takes full advantage of the dynamic nature of JavaScript to get rid of that requirement.
Before I show the code, I should do something that I forget to do yesterday: show how it is invoked. You could look at the HTML source to figure it out, but I'lll save you that step. Yesterday's test invoked the code through a simple HTML button like this:
<input type='button' value='Get Both Feeds'
onclick='getFeed1();getFeed2()'>
Today's is a bit more elaborate:
<input type='button' value='Get Both Feeds'
onclick='getXMLText(window.document.ajaxForm.url1.value,"window.document.ajaxForm.feed1.value");
getXMLText(window.document.ajaxForm.url2.value,"setURL2()")'>
The implementation of this pattern relies on some of the neatest features of JavaScript. You can certainly use my code without fully understanding it, but if you're never played with the equivalence between JavaScript objects and associative arrays, or if you've never worked with the Function constructor to create code on-the-fly, I do recommend that you take the time go through this code. I'll try to explain it below, but I'm sure I won't do as good a job as JavaScript: The Definitive Guide or Danny Goodman's JavaScript Bible
Update: Some of the Javascript code show on this page has been updated (read "bug-fixed") here.
Here's the code.
<script language='javascript'> <!-- function initXMLHttpRequest() { // branch for native XMLHttpRequest object if (window.XMLHttpRequest) { req = new XMLHttpRequest(); } else if (window.ActiveXObject) { // branch for IE/Windows ActiveX version req = new ActiveXObject("Microsoft.XMLHTTP"); } return(req); } function makeCustomHandlerForObject(reqString, targetObjectString, returnProperty) { out = "var xmlOutput = processReadyStateChange(" + reqString + ",\"" + returnProperty + "\");" + targetObjectString + "= xmlOutput; window[" + reqString + "] = null;"; return out; } function makeCustomHandlerForFunction(reqString, targetObjectString, returnProperty) { out = "var xmlOutput = processReadyStateChange(" + reqString + ",\"" + returnProperty + "\");" + targetObjectString.substr(0,targetObjectString.length - 1) + "xmlOutput); window[" + reqString + "] = null;"; return out; } function loadXMLDoc(url, reqIndex, target,property) { req = window[reqIndex]; if (target.substr(target.length-2,2) == "()") { req.onreadystatechange = new Function( makeCustomHandlerForFunction(reqIndex,target,property) ); } else { req.onreadystatechange = new Function( makeCustomHandlerForObject(reqIndex,target,property) ); } req.open("GET", url, true); if (window.XMLHttpRequest) { req.send(null); } else { req.send(); } window.document.all.status.style.color = "green"; window.document.all.status.innerHTML = "Reading Feed"; } function processReadyStateChange(req, property) { // only if req shows "complete" if (req.readyState == 4) { // only if "OK" if (req.status == 200) { // ...processing statements go here return req[property]; } else { window.document.all.status.style.color = "red"; window.document.all.status.innerHTML = "There was a problem retrieving the RSS feed\n" + req.statusText; } } } var reqNum = 0; function getXMLText(source, target) { var reqIndex = "req" + (++reqNum); window[reqIndex] = initXMLHttpRequest(); loadXMLDoc(source, reqIndex, target, "responseText") } function getXMLDoc(source, target) { var reqIndex = "req" + (++reqNum); window[reqIndex] = initXMLHttpRequest(); loadXMLDoc(source, reqIndex, target, "responseXML") }
Let's walk through it. It starts in getXMLText(), which the HTML imput button invokes twice -- once for each of the two RSS feeds that it is pulling in. There's also a getXMLDoc() function in the Javascript source, but I'm not using it in this particular demo. It's untested as of now, as a matter of fact. The only difference between the two functions, though, is that the former passes the string "responseText" into loadXMLDoc(), while the latter passes in the string "responseXML". Since those are both valid property names for the xmlHttpRequest object, and the former is working, I'm 99% confident that that latter will, too. The importance of this difference is that the responseText property simply returns a string full of XML, and the responseXML property returns an object representation of the XML that is fully ready for processing with lots of handy XML tools. To make this demo less complex and yet prove that the Ajax code is successfully retrieving XML, I simply chose to retrieve it as text and display it.
The getXMLText() function takes two arguments. The first is a URL. The second is a string representing the name of the target for the XML. It's a string with the name because we're actually going to build new source code for a function that we'll define on the fly. If you'll look back at the HTML input code that I showed above, you'll see that I used two different target names -- in fact, it is two different types of target names!
The first call to getXMLText() has "window.document.ajaxForm.feed1.value" for the target argument. In this case, the string represents the name of an input element's value property. We'll simply be dropping the XML string into that input element in order to display it. I could just as easily have put a div into my HTML, like this:
<div id='feed1'></div>
in which case, I would have used "window.document.all.feed1.innerText" as the target argument instead. (OK... I was over-simplifying when I said I could have done this "just as easily". Dropping raw XML into the innerText of a div is likely to drive a browser a little bit batty, isn't it!?)
The second call to getXMLText() has "setURL2()" for the target argument. The string represents the name of a function that I want to call with the XML text as an argument. It's a callback, much as we see in the classic Ajax pattern. In today's code, I'm not passing any other arguments to the callback function, but I'm going to fix this in my ultimate Ajax pattern, which I'll work out and publish in a few days. Meanwhile, here's the setURL2() function:
function setURL2( xml ) {
window.document.ajaxForm.feed2.value = xml;
}
All this function does is assign the XML to an input element. Obviously, though, I could have done more with it.
So, how does getXMLText() actually work? The first thing it does is calculate a unique name that will be used for a new xmlHttpRequest object. There's a global variable reqNum, which was initialized to zero. The code pre-increments it and concatenates it after the string "req", storing the result in reqIndex. In other words, each call generates names "req1", "req2", "req3", etc., in sequence. Next, the code takes advantage of the equivalenct between JavaScript objects and associative arrays. It adds a new element with index reqIndex to the global array window. I.e., it adds a new property named by the value of reqIndex to the window object. Since the window object is global, this effectively defines a new global variable on-the-fly. This new property of the window object is an object itsef: an xmlHttpRequest object, returned by the initXmlHttpRequest() function (which is unchanged from the version yesterday). This method lets the code use each xmlHttpRequest object once and only once, eliminating the possibility of weird timing problems if a user clicks rapidly on different UI elements that each trigger an Ajax request. (The object will get cleaned up later, by the way.) All that's left for getXMLText() to do is to call loadXMLDoc(), passing in the url, the name of the newly created xmlHttpRequest object, the target.(which, as pointed out above, is also a name), and also "responseText", which is the name of the property of the xmlHttpReqest object that we want to work with.
Now, let's look at the loadXMLDoc() function. The code changed from yesterday is right at the beginning.
req = window[reqIndex];
if (target.substr(target.length-2,2) == "()") {
req.onreadystatechange = new Function( makeCustomHandlerForFunction(reqIndex,target,property) );
}
else {
req.onreadystatechange = new Function( makeCustomHandlerForObject(reqIndex,target,property) );
}
The first line just gets a reference to the xmlHttpRequest object that we created in a property of thewindow object. Then we look at the last two characters of the target argument to see if they are "()", in which case we call makeCustomHandlerForFunction. Otherwise, we call makeCustomHandlerForObject. This is what gives us the ability to either assign the XML that we retrieve to a an object property (i.e., the value property of an input element) or to pass it to a callback function. In either case, what we're really doing is using the Function constructor to write a new function on the fly and assign the code to the onreadystatechange property of our xmlHttpRequest object, so our newly-define handler function will be invoked when the XML is retrieved.
Now, let's look at the two functions that actually create the custom handler functions. All they're doing is building a string source code text. We've passed in everything that we need. The arguments for both functions are the name of the window property containing our xmlHttpRequest object, the name of the target property or function (depending on which of the functions you've called), and the name of the property of the xmlHttpRequest object that we're going to return. In makeCustomhandlerForObject(), we build a source code string as follows:
"var xmlOutput = processReadyStateChange(" + reqString + ",\"" + returnProperty + "\")";
targetObjectString = xmlOutput;
window[ "+ reqString + "] = null;"
To make that a little more clear, let's show an example of what the return value of makeCustomHandlerForFunction("window[req1]","setURL2()","responseText") that we return to the Function constructor might actually look like when properly formatted:
function () {
var xmlOutput = processReadyStateChange( window[req1], "responseText");
setURL2(xmlOutput);
window[req1] = null;
}
The resulting function has no name, but it can still be called because we're passing it as an object to the xmlHttpRequest object's onreadystatechange property. The generated code calls processReadyStateChange(), calls the callback function with the returned value, and then the assignment window[req1] = null; cleans up the xmlHttpRequest object that was created earlier.
The makeCustomHandlerForObject() function works similarly, but just generates code that does an assignment to the object or property whose name was passed in (instead of calling a function the way the code generated by makeCustomHandlerForFunction() does. An example of what the return value of makeCustomHandlerForObject("window[req1]","window.forms.ajaxForm.feed1.value","responseText") that we return to the Function constructor would look like this when properly formatted:
function () {
var xmlOutput = processReadyStateChange( window[req1], "responseText");
window.forms.ajaxForm.feed1.value = xmlOutput;
window[req1] = null;
}
There's just one more coding detail to explain. In order to generalize the processReadyStateChange() routine, to handle either returning the XML text (i.e., responseText) or XML document (i.e., responseXML), we're passing in the name of the property that we want, and I changed the return statement as follows:
return req[property];
The code is now almost as completely generalized as I want it to be. All that remains is to be able to pass additional arguments through makeCustomHandlerForFunction() to the callback function.
1. Alan Bell05/16/2005 05:59:25 AM
Homepage: Http://www.dominux.co.uk
it has occurred to me that one of the things which could be inserted in the page on the fly is more Javascript. This would allow a page to provision itself with more capabilities. Hmm, I think IBM needs to take Workplace back into the shop for an overhaul.
2. Richard Schwartz05/16/2005 06:15:38 PM
Homepage: http://smokey.rhs.com/web/blog/PowerOfTheSchwartz.nsf
I'd be a bit worried about whether one can in fact destroy JavaScript on the fly. If not, a beast such as Workplace might bring the user
s browser to it's knees if it just keeps on creating and loading code without doing any page transitions.
-rich
3. Alex Russell05/18/2005 03:56:34 AM
Homepage: http://dojotoolkit.org
Yes, you can pull in JS at runtime. It's how we do module loading in Dojo (http://dojotoolkit.org).
You can also clobber things out of the JS namespace and the DOM tree, but the relationship between doing something like a document.body.removeChild(document.getElementById("scriptBlock")) and "delete topLevelVar" isn't well defined in most browsers.
I do know, however, that removing script element blocks does reduce long-term memory usage in IE (it's how we keep the mod_pubsub client stable over the long-haul).
4. Richard Schwartz05/18/2005 07:06:22 AM
Homepage: http://smokey.rhs.com/web/blog/PowerOfTheSchwartz.nsf
@Alex: Thanks for that information.
-rich
5. Christianne Gierloff10/20/2005 10:43:05 AM
Thank you for the examples & documentation that you provided here and in Lotus Advisor. I'm in the process of learning javascript and have found this information invaluable in understanding how to use the xmlhttprequest. While I'm guilty of copying javascript code, copying it helps me learn how it works while practicing on my own. Until I read this post, I didn't understand what was so "asynchronous" about all this.
6. David Gilmore12/15/2005 11:13:47 AM
Homepage: http://www.htsi-global.com
- VERY nice work!!
- Sorry, got stuck looking at the live preview. (snicker)
7. Charlie07/05/2006 01:06:26 AM
run " test here" in IE popup some error at line 35 and 40
8. thelay12/10/2007 05:14:37 AM
Hi, this can be used for many requests of HTTP. I want to use AJax for Dbcolumn and DBlookup. I have many fields. But it only work for the last one. The request above are blank. How should I do that. thanks,
9. Richard Schwartz12/10/2007 07:55:01 AM
Homepage: http://www.rhs.com/poweroftheschwartz
Use Ajax to issue an ?OpenAgent URL to the Domino server and have the agent find the data, format it as XML, and return it back the browser; then use the XML parsing routines in Javascript in the browser to interpret the data and update the DOM.






the hitchhiker's guide to anti-spam for Lotus Domino
- 











