A Better Loading Message

In Chapter 6 of the book, we showed you one way of creating a loading message for your users. The method demonstrated followed this basic pattern:

  • Serve the user an HTML page with the loading message in it,
  • Have an onLoad event call the Maps API’s initialization, and add all the markers to the map, and then
  • Remove the loading message using a class swap on the body tag

This seems to make perfect sense, except that browsers complete all onload functionality before displaying anything to the user. Though there’s a sizable delay, by the time the map pops into view, the wait is over! So even if there was a classy loading message, the user likely missed out on it.

I’ve remedied this inconsistency now, and here’s a new demo of a superior splash-screen mechanism.

An Alternative Flow

The revised version of this demo changes an important element in the flow sequence outlined above. Rather than calling map_data.php right in the header of the markup file, we use an Ajax call to fetch it once the page is otherwise done loading. This means that the page’s load time is trivial, and it appears immediately… and then, a few moments later, the map initializes and the data shows up.

This is akin to the difference between a flash movie that’s a single big SWF you have to download, and the better kind that use a “loader”—a small, sacrificial movie that downloads quickly, and then provides feedback to the user, telling them that the rest of it is on the way.

That Ajax Thing

What was originally one big init() function has been broken into two halves. The first is still init(), but the second is initData(). The data initialization is where all the Maps stuff ended up, and then original one now looks like this:

function init() {
    document.getElementById('button-sidebar-hide').onclick = function() { return changeBodyClass('sidebar-right', 'nosidebar'); };
    document.getElementById('button-sidebar-show').onclick = function() { return changeBodyClass('nosidebar', 'sidebar-right'); };
    handleResize();

    xmlhttp = GXmlHttp.create();
    xmlhttp.open('GET', 'map_data.php', true);
    xmlhttp.onreadystatechange = function() {
        if (xmlhttp.readyState == 4) {
            if (xmlhttp.status != 200) {
                setAlertText('Could not access map data.');
            } else {
                var responseText = xmlhttp.responseText;
                markers = eval(responseText);
                if (!markers) {
                    setAlertText('Map data error.');
                } else {
                    initData();
                }
            }
        }
    }
    xmlhttp.send(null);
}

How does it work? Just like any Ajax call, it sets up the XMLHTTPRequest object, sets up the success handler, and ships off the request. I put in two error checks: one for if the data file goes missing, and another for if it’s erroneous. Either way, this should help keep the site from getting caught with its pants down.

In the non-error case, the request’s response is returned as a text string and eval’d. The eval’d result is saved to the markers variable, which is now a global that’s been declared at the top of the script.

Changes in the Data

Under the old model, the map_data.php script looked like this:

var markers = [
    { 'latitude': 19.7808333333, 'longitude': -155.0875, ... } ,
    { 'latitude': 19.7291666667, 'longitude': -155.022777777778, ... } ,
    ...
]

When you’re including the code as a javascript, that’s exactly what you want: everything assigned to some variable that can be read out later. But when the code is being eval’d, this is more awkward. If the old version of the data file was eval’d inside the request’s handler, then the markers variable would be local to that function. It would go out of scope a few lines later, and vanish.

This is why, when doing the eval() method, it’s often preferable to have the data simply as a lone expression in the datafile. It gets evaluated and returned by the eval() function, and from there it can be assigned or manipulated as necessary.

Now the file just looks like this:

[
    { 'latitude': 19.7808333333, 'longitude': -155.0875, ... } ,
    { 'latitude': 19.7291666667, 'longitude': -155.022777777778, ... } ,
    ...
]

Considerations

All in all, this is a great way to help make your mashup more usable—especially if some long data crunching might have viewers thinking you crashed their browser. It’s a little trickier to put together than the way we showed you in the book, but it’s also a better effect. Here are some extra things to keep in mind:

  • You could put the the map initialization stuff in the original init(), and only move the marker-creating loop to initData(). Both ways have slight advantages, which you can probably figure out with a little bit of experimentation.
  • If the data download is really really long, then there’s likely a problem with your app’s design. But if you’ve got a legitimate reason to be downloading a whole ton of stuff to the user’s browser, you could chunk it into little 100kb packets, and then update a progress meter as each one comes in over the wire.
  • Some purists will always hate eval(), and mostly for philosophical reasons. There’s some discussion on this by uber-JavaScripter Simon Willison, and (surprise!) he ends up using this exact situation as an example of legitimate eval() usage. (The one time that it’s definitely not cool to eval() is when the data is coming in unchecked from some external source; allowing your site to run someone else’s JavaScript is just plain bad news.)
  • A splash is not always the right option. Web surfers have limited patience; generally, it’s best to aim for an approach where you show something now, and grab any off-screen bits as required. (or maybe even stream them silently in the background…)

The Files Involved


6 Responses to “A Better Loading Message”  

  1. 1 Caspar Gorvin

    Hello Mike,

    I got your book this monday and I’m very pleased with it. I’ve been working with the Maps API for a while, but I still learn a lot from the book, in particular from the “Advanced” topics and from your hints on external sources for geocoding data.

    For the loading dilemma, I use an approach that is in my opignion easier and more flexible:

    1) put nothing but html in your html-file, don’t load any scripts by meta-tags except for the Google API.

    2) show a “loading…” message in your html-file

    3) in your onload() function, load your application’s scripts via XmlHttpRequest

    4) when onreadystate == 4 for the first script loaded, eval() the script and then go on and load the next script etc., to make sure, the scripts get loaded and initialized in the correct order

    5) load your data files the same way

    6) Due to the way the browsers implement variable scopes, any global objects contained in the scripts must be defined implicit, i. e. without the ‘var’ keyword. E. g.:
    Define: ‘myClass = function () {…}; ‘
    Not: ‘var myClass = function…’ and not ‘function myClass()…’

    An implementation of the above (error checking stripped off):

    // The CRequestObject class implements the XHTML-Request-Object class

    // Constructor
    function CRequestObject ()
    {
    // Create an XMLHttpRequest-object (browser implementation dependend)
    if (window.XMLHttpRequest) { // is Mozilla Firefox, Opera, Internet Explorer 7
    this.XHRObject = new XMLHttpRequest();
    }
    else {
    if (window.ActiveXObject) { // is Internet Explorer 6 or earlier
    this.XHRObject = new ActiveXObject(”Microsoft.XMLHTTP”);
    }
    }
    }

    // RequestFile()
    // Requests a file from the server and calls the specified function on completion

    CRequestObject.prototype.RequestFile = function (File, onLoadFunction)
    {
    var _this = this; // a way to pass the this-pointer of the CRequestObject to the event handler (which overwrites the this pointer)

    this.XHRObject.onreadystatechange = function ()
    {
    if(_this.XHRObject.readyState == 4) // complete
    {
    // Call the specified function
    if (_this.XHRObject.status

  2. 2 Caspar Gorvin

    Here’s the rest of the code example (the reply form cut it off):

    // Call the specified function
    if (_this.XHRObject.status

  3. 3 Caspar Gorvin

    onLoadFunction(_this.XHRObject);
    }
    }
    };
    }

    // Open and send the request for the file
    this.XHRObject.open(’GET’,
    File, // requested file
    true); // asynchronous call

    this.XHRObject.send(null);
    };

    // RequestAndRunScript()
    // Requests a script file from the server and runs the script

    CRequestObject.prototype.RequestAndRunScript = function (ScriptFile)
    {
    var _this = this;

    // Define an onload function that will be called after loading the script
    var onLoadFunction = function(XHRObject)
    {
    eval( XHRObject.responseText );
    if (_this.onCompletion) {
    _this.onCompletion();
    }
    };
    // Load the script file and run it
    this.RequestFile(ScriptFile, onLoadFunction);
    };

    // An example of an html-file’s onload function:

    window.onload = function()
    {
    // Load and initialize the scripts
    var RequestObject1 = new CRequestObject();
    var RequestObject2 = new CRequestObject();
    var RequestObject3 = new CRequestObject();
    var RequestObject4 = new CRequestObject();
    var RequestObject5 = new CRequestObject();
    RequestObject1.onCompletion = function() { RequestObject2.RequestAndRunScript(’Script2.js’); };
    RequestObject2.onCompletion = function() { RequestObject3.RequestAndRunScript(’Script3.js’); };
    RequestObject3.onCompletion = function() { RequestObject4.RequestAndRunScript(’Script4.js’); };
    RequestObject4.onCompletion = function() { RequestObject5.RequestAndRunScript(’Script5.js’); };
    RequestObject5.onCompletion = function() { alert(’All scripts loaded’); };
    RequestObject1.RequestAndRunScript(’Script1.js’);

    // Initialize the application, do something useful…
    var cp = new CCommandProcessor();

    };

  4. 4 Caspar Gorvin

    Why does this show the load message right away even though we loaded the scripts in the onload() function and the browser doesn’t show anything before finishing the onload() function? Because the window.onload() function starts the loading of the scripts but doesn’t wait until all the scripts are loaded. It terminates after setting up the XmlHttp-requests and - the browser displays the html-file’s ‘Loading…’ message. The only tricky thing to remember is to not do anything in the application that depends on one of the scripts before all the scripts are loaded.

  5. 5 Caspar Gorvin

    In the example code above, the alert() message box makes sure, no application code is executed before all scripts are loaded. Replace the alert()-call with a call to your application’s program entry function, and you’ll get the effect as described. The last line in the onload()-message (’var cp = …’) may only call functions that don’t rely on the scripts loaded. It’s there to demonstrate that calling a function of a script that’s beeing loaded will fail, because the onload() function continues (and terminates) without the loading to complete.

    PS. sorry for the split replies, but the form cut off the text in the middle of the code expample.

  6. 6 tony

    This new code does not work properly. I have copied the files verbatim and its simply not working. I am not sure if there is a problem in the code or a problem on the site b/c none of the database examples are working in chapter 6. Can you please fix?

Leave a Reply