Following from Simon’s excellent tips on Closures and executing JavaScript on page load got an email from Scott Raymond regarding how this can be applied to XMLHttpRequest so figured I’d share some experiences I’ve been having as a result of ScriptServer.
The Mozilla implementation of XMLHttpRequest provides the “onload” and “onerror” properties to which you can assign your own callbacks, the callback automatically being passed an Event object through which you can get back to your calling object – this is best seen by looking at the Mozblog nsXmlRpcClient.js – see what they do with their _onload function.
Unfortunately, Microsoft’s ActiveX implementation doesn’t, requiring you use a property called onreadystatechange and thankfully Mozilla also supports this (side note – is Mozilla’s XMLHttpRequest the first time someone’s pulled Microsoft’s favorite “Embrace and Extend” on Microsoft themselves? ). The problem is nothing get’s automatically passed to the callback function assigned to onreadystatechange, but that’s where the closure comes in handy.
The rest I’ll leave to a long piece of code
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<script type="text/javascript">
<!--
// Mozilla only implementation!
// Constructor for generic HTTP client
function HTTPClient() {};
// Add methods and properties as array
HTTPClient.prototype = {
url: null,
// Instance of XMLHttpRequest
xmlhttp: null,
// Used to make sure multiple calls are not placed
// with the same client object while another in progress
callinprogress: false,
// The user defined handler - see MyHandler below
userhandler: null,
init: function(url) {
this.url = url;
this.xmlhttp = new XMLHttpRequest();
},
// handler argument is a user defined object to be called
asyncGET: function (handler) {
// Prevent multiple calls
if (this.callinprogress) {
throw "Call in progress";
};
this.userhandler = handler;
// Open an async request - third argument makes it async
this.xmlhttp.open('GET',this.url,true);
// Have to assign "this" to a variable - not sure why can't use directly
var self = this;
// Assign a closure to the onreadystatechange callback
this.xmlhttp.onreadystatechange = function() {
self.stateChangeCallback(self);
}
// Send the request
this.xmlhttp.send(null);
},
stateChangeCallback: function(client) {
switch (client.xmlhttp.readyState) {
// Request not yet made
case 1:
try {
client.userhandler.onInit();
} catch (e) { /* Handler method not defined */ }
break;
// Contact established with server but nothing downloaded yet
case 2:
try {
// Check for HTTP status 200
if ( client.xmlhttp.status != 200 ) {
client.userhandler.onError(
client.xmlhttp.status,
client.xmlhttp.statusText
);
// Abort the request
client.xmlhttp.abort();
// Call no longer in progress
client.callinprogress = false;
}
} catch (e) {
/* Handler method not defined */
}
break;
// Called multiple while downloading in progress
case 3:
// Notify user handler of download progress
try {
// Get the total content length
// -useful to work out how much has been downloaded
try {
var contentLength =
client.xmlhttp.getResponseHeader("Content-Length");
} catch (e) {
var contentLength = NaN;
}
// Call the progress handler with what we've got
client.userhandler.onProgress(
client.xmlhttp.responseText,
contentLength
);
} catch (e) { /* Handler method not defined */ }
break;
// Download complete
case 4:
try {
client.userhandler.onLoad(client.xmlhttp.responseText);
} catch (e) {
/* Handler method not defined */
} finally {
// Call no longer in progress
client.xmlhttp.callinprogress = false;
}
break;
}
}
}
// A user defined handler to response to the XMLHTTPRequest
var MyHandler = {
onInit: function() {
echo("About to send request<br>");
},
onError: function(status,statusText) {
echo("Error: "+status+": "+statusText+"<br>");
},
onProgress: function(responseText,length) {
echo("Downloaded "+responseText.length+" of "+length+"<br>");
},
onLoad: function(result) {
echo("<pre>"+result+"</pre>");
},
}
// Just a function to help display results
function echo(string) {
document.getElementById("results").innerHTML += string;
}
// Invoke the client
function getPage() {
// Modify this to some page
var url = "http://localhost/test/test.txt";
var client = new HTTPClient();
client.init(url);
try {
client.asyncGET(MyHandler);
} catch (e) {
alert(e);
}
echo("Async request so still able to do stuff here<br>");
}
</script>
</head>
<body>
<a href="javascript:getPage();">getPage</a>
<div id="results">
</div>
</body>
</html>
Note I'm also using an asychronous request here - have heard people complain XMLHttpRequest is too slow, locking up the browser - an asychronous request helps you prevent that.
Note also this is also Mozilla specific right now. Since versions 3.x of the ActiveX XMLHttpRequest it seems, for some reason I don't understand, they've stopped populating properties like "status" until we hit readyState 4 - that means you can't respond to errors or get information about the request until it's completely finished. For a cross browser implementation of something similar, see here (have yet to implement the closure though, hence using lots of globals) - example use here.
Harry Fuecks is the Engineering Project Lead at Tamedia and formerly the Head of Engineering at Squirro. He is a data-driven facilitator, leader, coach and specializes in line management, hiring software engineers, analytics, mobile, and marketing. Harry also enjoys writing and you can read his articles on SitePoint and Medium.