🤯 New: Coding Assessments Practice your skills on real-world programming challenges

XMLHttpRequest and Javascript Closures

    Harry Fuecks
    Share

    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.