Delegated AJAX
About a year ago I needed a quick and easy AJAX library to just grab XML or JSON. At that time I was working heavily in Objective-C on the iPhone which uses a lot of delegation of functionality. I wanted to build something similar for my web application.
In most browsers the XMLHttpRequest object is a native JS object and can be extended with JS’s class prototyping. So I created a function…
XMLHttpRequest.prototype.Request = function(address, postParams, delegate);
The first param is the URL to request, the second is an associative array of values to POST, and the last is a delegate class.
The delegate class can contain any or none of the following functions in the following example.
function MyDelegate() { this.onstart = function() {} this.onprogress = function(completed, total) {} this.onsuccess = function() {} this.onerror = function(code, status) {} this.onfinish = function() {} }
The functions sort of speak for themselves in what they do. “onstart” happens first, “onfinish” last, “onprogress” happens while the progress of the request is ongoing, “onerror” when an error occurs, and “onsuccess” when the request succeeds.
Usually only an “onsuccess” is needed. “onerror” is often very useful to handle any errors.
The delegate class definition (“MyDelegate” in the above example) can be defined to take any number of parameters. Via the JS closure paradigm all of the defined event handlers will have direct access to them. This is most handy.
Furthermore, because in the creation of a JS object via function all the code in the function is ran, this gives us the best way to make an AJAX request.
function FetchCurrencyDataDelegate(countryCode) { this.onsuccess = function() { // handle the return of the data var obj = this.responseJSON; } XMLHttpRequest.Request( '/currency.php', { 'countryCode' : countryCode }, this ); }
Passing “this” into the Request object uses a closure to ensure that it never leaves scope while the request object exists. The Request function will then use that object reference to make the appropriate event handler function calls when needed.
You might think that XMLHttpRequest.Request
shouldn’t work because XMLHttpRequest is a type and not an instantiated object. You are correct. To make that shortcut work needs one more function, a helper.
XMLHttpRequest.Request = function(address, postParams, delegate) { var request = new XMLHttpRequest; request.Request(address, postParams, delegate); }
When the AJAX request returns the library code applies all of the XMLHttpRequest objects properties to the delegate and then calls “onsuccess.” In addition, the library creates a “responseJSON” value which is a decoded JS object if the server returns a JSON string.
So, starting the ajax call is now as simple as…
new FetchCurrencyDataDelegate('us');
We don’t care that our current scope doesn’t keep a reference to the new object because the AJAX library’s Request function has a copy of it which will keep it alive. We are now relying on the Delegate to properly handle all UI or storage changes resulting for the AJAX call.
The source code is below. But first there is one more important topic.
This AJAX library works in all major browsers as is except for Internet Explorer 7 or before (which are sadly still widely used and therefore “major”). The reason they don’t work is that they lack a native XMLHttpRequest Object. The fix for that is simple… create one. The second file is also supplied at the end. To use both files, in your html document’s head section include this.
<!--[if lte IE 7]> <script type="text/javascript" src="/js/XMLHttpRequest.IEFix.js"></script> <![endif]--> <script type="text/javascript" src="/js/XMLHttpRequest.Request.js"></script>
The conditional comment, which looks like just a comment to all browsers except IE, will include the IEFix JS file only if the browser version is less than or equal to 7. Note: the IEFix must come before the other file.
These browsers also have one big limitation. The XMLHttpRequest Object’s onreadystatechange doesn’t get called in the scope of the object (“this” does not equal the XMLHttpRequest object). This means a global instance of the object must be kept for the onreadystatechange event to use. This isn’t a huge problem except that it means only one AJAX call can be happening at any given time. Future work on my part may find a work around for this. But I don’t care much for Internet Explorer.
XMLHttpRequest.Request.js
// Verifies that the object is an array. function isArray(obj) { //null test (this is needed to keep the other check from blowing up) if(obj == null){ return false; } if (obj.constructor.toString().indexOf("Array") == -1){ return false; } else{ return true; } } //this function takes an associative array and turns it into a get (or post) string XMLHttpRequest.prototype.EncodePOST = function(vars) { //if null, return null if(!vars == null) return null; //if it isn't an array, then we don't want it (right?) if (isArray(vars) != false) throw "POST variables need to be submitted as an associative array, please."; var str = ""; //create "get" string for(x in vars) { str += encodeURIComponent(x) + "=" + encodeURIComponent(vars[x]) + "&"; } //hack off that last & return str.substring(0, str.length - 1); } XMLHttpRequest.Request = function(address, postParams, delegate) { var request = new XMLHttpRequest; request.Request(address, postParams, delegate); } XMLHttpRequest.prototype.Request = function(address, postParams, delegate) { this.onreadystatechange = function() { // check the ready state, ignore if we aren't done (for now) if (this.readyState != 4) return; switch (this.status) { case 200: // see if we have a function to call if (delegate) { if (!(this.responseXML && this.responseXML.documentElement)) { try { // If the return is not XML then see if it is JSON. delegate.responseJSON = eval("(" + this.responseText + ")"); } catch (ex) {} } else { // if webkit then hookup events } // Push property values to the delegate delegate.responseXML = this.responseXML; delegate.responseText = this.responseText; delegate.status = this.status; delegate.statusText = this.statusText; if (delegate.onsuccess) { // notify delegate of success delegate.onsuccess.apply(delegate, new Array()); } } break; // Could handle other error codes here (such as 401 Unauthorized) so that they will be happen for all delegates default: try { // Notify delegate of an error delegate.onerror.apply(delegate, new Array(this.status, this.statusText)); } catch (ex) { // default error handler... an alert alert("Error retrieving server data: " + this.statusText); } break; } if (delegate) { if (delegate.onfinish) { // Notify delegate to update UI of ajax finish delegate.onfinish.apply(delegate, null); } } return; }; this.onprogress = function (e) { if (delegate) { if (delegate.onprogress) { // Notify delegate of progress delegate.onprogress.apply(delegate, new Array(e.position, e.totalSize)); } } } var postString = this.EncodePOST(postParams); this.open("POST", address, true); this.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); if (delegate) { if (delegate.onstart) { // Call onstart to notify UI of ajax call delegate.onstart.apply(delegate, null); } } this.send(postString); }
XMLHttpRequest.IEFix.js
Request = null; function XMLHttpRequest() { if (!Request && window.ActiveXObject) { try { this.req = new ActiveXObject("Msxml2.XMLHTTP"); } catch(e) { try { this.req = new ActiveXObject("Microsoft.XMLHTTP"); } catch(e) { this.req = false; } } } if (!this.req) return false; Request = this; this.req.onreadystatechange = function() { var r = Request; Request.readyState = Request.req.readyState; if (Request.readyState == 4) { Request.responseXML = Request.req.responseXML; Request.responseText = Request.req.responseText; Request.status = Request.req.status; Request.statusText = Request.req.statusText; Request = null; } r.onreadystatechange(); } this.open = function(method, url, async) { this.req.open(method, url, async); } this.setRequestHeader = function(name, value) { this.req.setRequestHeader(name, value); } this.send = function(postString) { this.req.send(postString); } }