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.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://www.sample.com Widow Maker

    Thanks :)

    You approached this subject a wee while back in the Advanced PHP Forums, and keeping the topic in focus is great :)

  • http://www.megameta.net/ sobri

    Just thought I’d mention that XMLHttpRequest is implemented in Safari also.

    http://developer.apple.com/internet/webcontent/xmlhttpreq.html

    I’m not sure of its status in Opera, but I’m hoping for the best :)

  • Carl Smotricz

    Thanks for an excellent and very useful writeup!

    // Have to assign “this” to a variable – not sure why can’t use directly
    var self = this;

    I’ve been struggling with this for days! The answer is that “this” is the context of the function/object definition (i.e. HTTPClient) at the time the constructor runs (i.e. at “new” time) but the context of the call when a callback occurs; i.e. when StateChangeCallBack() is called, this points inside some completely different object. Venkman (many Kudos to the Moz team for Venkman!) shows this quite nicely as you stumble through errors and exceptions.

    This is where JavaScript closure comes in: A function “remembers” the context it was defined in, including all variables (except “this”, of course!) So when your callback runs, the “self” it refers to is the very “self” that was pointed at “this” at the time of creation of HTTPClient.

  • http://www.phppatterns.com HarryF

    Nice explaination. Many thanks for clearing that one up (and note to self, I should start using Venkman regularily).

  • Bryon Bean

    – snip –
    // Have to assign “this” to a variable – not sure why can’t use directly
    var self = this;
    — snip –

    After reading you article I just happend to stumble on this information, thought you might enjoy…

    (From http://www.crockford.com/javascript/private.html):

    function Container(param) {

    function dec() {
    if (secret > 0) {
    secret -= 1;
    return true;
    } else {
    return false;
    }
    }

    this.member = param;
    var secret = 3;
    var self = this;
    }

    By convention, we make a private self parameter. This is used to make the object available to the private methods. This is a workaround for an error in the ECMAScript Language Specification which causes this to be set incorrectly for inner functions.

  • Eric C.

    Great page, but the popup really pisses me off. Embracing Mozilla but then using javascript code to cirucumvent its pop-up blocking is a bit hypocritical. For other readers that find this page I suggest adding *sitepoint.com/popup* to your adblock list.

  • Brad Fults

    This example, although useful in many ways with regard to learning, is hardly plausible for use in real-world applications. The code is bloated and many unnecessary structures are created. For a barebones XMLHTTP library, I suggest XHConn: [ http://xkr.us/code/javascript/XHConn/ ].

  • Volre

    I tried the XHConn, however, right out of the box was not functional, had any experience with this? Solutions?

  • mitch

    Nice write up. Thanks.

    What changes need to be made to make this a cross-browser implementation (i.e., so it works in IE, too)?

    I tried changing init as follows:

    init: function(url) {
    this.url = url;

    if( window.XMLHttpRequest ) {
    this.xmlhttp = new XMLHttpRequest();
    } else if (window.ActiveXObject) {
    this.xmlhttp = new ActiveXObject(“Microsoft.XMLHTTP”);
    } else {
    // undefined
    }
    },

  • soldierdog

    I’m having a lot of trouble understanding how to get my targeting working in IE. Let’s say I have a

    with some form information inside. I submit using the xmlhttpreq but I want the information after processing to appear inside the test div. I have it working perfectly with firefox but for the life of me I just can’t get it to work with IE. I can only ouput to another DOM object.innerHTML and not he original.

    Any help Please

  • robin

    Where the hell has this code gone??!?

  • Morris Johns

    I wrote an article explaining closures if you are interested:
    http://blog.morrisjohns.com/javascript_closures_for_dummies

  • Anonymous

    The head of this is strong

    my problem is related to the following code

  • Anonymous

  • World Wide Weird

    Use the with keyword with code blocks that reference the object instead of passing this or self. This even works when, for example, calling the object’s methods from setTimeout. :D