Pixel-in-Gene

Exploring Creativity with Design / Graphics / Technology

Using jQuery.Deferred() and RequireJS to Lazy Load Google Maps API

In the world of jQuery or for that matter, any JavaScript library, callbacks are the norm for programming asynchronous tasks. When you have several operations dependent on the completion of some other operation, it is best to handle them as a callback. At a later point when your dependent task completes, all of the registered callbacks will be triggered.

This is a simple and effective model and works great for UI applications. With jQuery.Deferred(), this programming model has been codified with a set of utility methods.

$.Deferred() is the entry point for dealing with deferred operations. It creates a “promise” (a.k.a Deferred object) to trigger all the registered done() or then() callbacks once the Deferred object goes into the resolve() state. This is according to the CommonJS specification for Promises. I am not going to cover all the details of $.Deferred(), since the jQuery docs do a much better job. Instead, I’ll jump right into the main topic of this post.

The soup of AMD, $.Deferred and Google Maps

In one of my recent explorations with web apps, the AMD pattern turned out to be extremely useful. AMD, with the RequireJS library, forces a certain structure on your project and makes building large web apps more digestible. Abstractions like the require/define calls allows building apps that are more composable and extensible. It sure is a great way to think about composable JS apps in contrast to the crude <script> tags.

With these abstractions, it was easier to think of the app as a set of modules. Some modules provide base level services, while others depend on such service-modules. One particular module, which also happens to be the entry point into the app, was heavily dependent on the Google Maps API. Early on, it was decided to never keep the user waiting for the maps to load and allow interaction right from the get go.This meant that they could do some map-related tasks even before the maps API had loaded. Although this felt impossible at the onset, it turned out to be quite easy, all thanks to $.Deferred().

The first step was to wrap the Google Maps API in a GoogleMaps object. This hides away the details about loading the maps while allowing the user to carry on with the map related tasks.

Wrapping the google maps API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function GoogleMaps() {
  
}

GoogleMaps.prototype.init = function() {
  
};

GoogleMaps.prototype.createMap = function(container) {
};

GoogleMaps.prototype.search = function(searchText) {
};

GoogleMaps.prototype.placeMarker = function(options) {
};

The calls to createMap, search and placeMarker need to be queued up until the maps API has loaded. We start off with a single $.Deferred() object, _mapsLoaded

The deferred object
1
2
3
4
5
_mapsLoaded = $.Deferred()

function GoogleMaps() {
  // ...
}

Then in each of the methods mentioned earlier, we wrap the actual code inside a deferred.done(), like so:

Wrapping calls in deferred.done()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function GoogleMaps() {
    _mapsLoaded.done(_.bind(function() {
        this.init();
    }, this));
}

GoogleMaps.prototype.init = function() {
};

GoogleMaps.prototype.createMap = function(container) {
    _mapsLoaded.done(_.bind(function() {
      // create the maps object
    }, this));
};

GoogleMaps.prototype.search = function(searchText) {
    _mapsLoaded.done(_.bind(function() {
      // search address
    }, this));
};

GoogleMaps.prototype.placeMarker = function(options) {
    _mapsLoaded.done(_.bind(function() {
      // position marker
    }, this));
};
  

With this, we can continue making calls to each of these methods as if the maps API is already loaded. Each time we make a call, it will be pushed into the deferred queue. At some point, when the maps API is loaded, we need to call a resolve() on the deferred object. This will cause the queue of calls to be flushed and resulting in real work being done.

One aside on the code above is the use of _.bind(function(){}, this)_. This is required because the callback to done() changes the context of this. To keep it pointing to the GoogleMaps instance, we employ _.bind().

Resolving the deferred object
1
2
3
4
5
6
7
window.gmapsLoaded = function() {
    delete window.gmapsLoaded;
    _mapsLoaded.resolve();
};

require(['http://maps.googleapis.com/maps/api/js?sensor=true&callback=gmapsLoaded']);
  

The google maps API has an async loading option with a callback name specified in the query parameter for the api URL. When the api loads, it will call this function (in our case: gmapsLoaded). Note that this needs to be a global function, ie. on the window object. A require call (from RequireJS) makes it easy to load this script.

Once the callback is made, we finally call resolve() on our deferred object: _mapsLoaded. This will trigger the enqueued calls and the user starts seeing the results of his searches.

Summary

In short, what we have really done is:

  1. Abstract the google maps API with a wrapper object
  2. Create a single $.Deferred() object
  3. Queue up calls on the maps API by wrapping the code inside done()
  4. Use the async loading option of google maps api with a callback
  5. In the maps callback, call resolve() on the deferred object
  6. Make the user happy

Demo

In the following demo, you can start searching on an address even before the map loads. Go ahead and try it. I have deliberately put in a 5 second delay on the call to load the maps API, just for a flavor of 3G connectivity!

Don’t forget to browse the code in your Chrome Inspector. You do use Chrome, don’t you? ;–)

Comments