Programming - - By Harry Fuecks

XMLHttpRequest and Javascript Closures

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.