Posted on Jan 07, 2014 By Philip Whitt

Google's Closure tools offer some great ways to build a maintainable JavaScript web app. But some of the most popular Google JS libraries (maps, visualization ect...) aren't included in the Google Closure library. To complicate things further some of the more popular JS libraries are only available through the Google Loader a.k.a. Google JS API. Most have externs files available for the Closure Compiler, but if you need to dynamically load the actual libraries like Google Maps or Google Visualization into your Closure Compiled JS app, you might think you're out of luck...

But alas! It is possible to dynamically load Google JS libraries from Google Loader in a Closure Compiled app. I'll show you how below.

Google Loader Overview
First, if you aren't already familiar with Google's JS Loader here's a quick overview. The Google Loader offers a nice way to dynamically load a variety of different JS libraries through a simple interface. Just include the following JS file in the <head> of your HTML
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
You now have access to dynamically load different libraries like earth, maps v2, visualization ect... 
// Load the Visualization API and the piechart package
google.load('visualization', '1', {'packages' : ["corechart"]});

// Set a callback to run when the Google Visualization API is loaded.
google.setOnLoadCallback(function() {
    // Execute google viz dependent code 
});
Unfortunately, this method is the only way to load certain libraries like Google Visualization. This is where the problem lies for Closure Compiled apps - if your code relies on Visualization, you cant execute your code until the library is loaded. 

The solution could be as simple as loading all the libraries you need and then in the call back executing your Closure App. But in the interest of optimization, I like to only load libraries when they are needed. 

For me, I often find the parts of my JS apps that need external Google libraries are rare and not as highly visited so it wouldn't make sense to propagate higher load times out to all users just for those less visited pages.

Loading a Google JS API
For this example you're going to need to have JQuery available to your app. Just add the JQuery Closure extern to your compiler and add JQuery to your page.

First, using jquery we need to load the jsapi script using $.getScript(string, function) if it hasn't already been loaded:
if (!util.ExternalApiLoader.isGoogleApiLoaded_) {
    $.getScript('http://www.google.com/jsapi', function() {
        util.ExternalApiLoader.isGoogleApiLoaded_ = true;
    });
    return; }
The call back function for $.getScript() will get called once the script has finished loading which gives you access to goog.load() API;
google.load('visualization', '1', {
    'callback' : 'ExternalApiLoaderGoogleVizCallBack();',
    'packages' : ["corechart", "geochart"]
});
As seen above, there's an option for 'callback' which is a string. This is the name of the function that google.load() will call once the script has been loaded. If you're using ADVANCED_OPTIMIZATIONS you're going to have to export this function to preserve its name.

Putting it all together
goog.provide('util.ExternalApiLoader');

/**
* @param {Function} callback
* @public
* @return {void}
*/
util.ExternalApiLoader.googleViz = function(callback) {
    util.ExternalApiLoader.googVizCallBack_ = callback;

    // if google js api isn't loaded, load it
    if (!util.ExternalApiLoader.isGoogleApiLoaded_) {
        $.getScript(util.ExternalApiLoader.Api.GOOGLE_JSAPI, function() {
            util.ExternalApiLoader.isGoogleApiLoaded_ = true;

            google.load('visualization', '1', {
                'callback' : 'ExternalApiLoaderGoogleVizCallBack();',
                'packages' : ["corechart", "geochart"]
            });
        });
        return;
    }
    util.ExternalApiLoader.googleVizCallBack();
};

/**
* @private
*/
util.ExternalApiLoader.googleVizCallBack = function() {
    util.ExternalApiLoader.googVizCallBack_.call();
};

/**
* @type {boolean}
* @private
*/
util.ExternalApiLoader.isGoogleApiLoaded_ = false;

/**
* @type {Function}
* @private
*/
util.ExternalApiLoader.googVizCallBack_ = function(){};

/**
* @type {Function}
*/
var ExternalApiLoaderGoogleVizCallBack = function() {
    util.ExternalApiLoader.googleVizCallBack();
};

goog.exportSymbol('ExternalApiLoaderGoogleVizCallBack', ExternalApiLoaderGoogleVizCallBack);
ExternalApiLoaderGoogleVizCallBack = /** @type {Function} */goog.getObjectByName('ExternalApiLoaderGoogleVizCallBack');

Easy Usage
Once the call backs have been sorted, you can dynamically load the library when needed from your controller code like such:
util.ExternalApiLoader.googleViz(function() {
    // Google Visualization Charts API is now available, render ect...
});

Loading non Google Loader JS APIs, like Google Maps v3
So long as an external JS API supports function call backs on script load, it's fairly easy (so long as you export your function name). The most common example of this is using the Google Maps v3 JS API - which specifies a function call back in a GET param:
goog.provide('util.ExternalApiLoader');

/**
* @param {Function} callback
* @public
* @return {void}
*/
util.ExternalApiLoader.googleMaps = function(callback) {
    util.ExternalApiLoader.googMapsCallBack_ = callback;

    // if google maps isn't loaded, load it and let it perform callback
    if (!util.ExternalApiLoader.isGoogleMapsLoaded_) {
        $.getScript(             'http://maps.google.com/maps/api/js?sensor=false&async=2&callback=ExternalApiLoaderGoogleMapsCallBack',             function() {
                // Nothing needs to go here since goog maps has callback function in the uri
                // param which is the preserved name ExternalApiLoaderGoogleMapsCallBack
            }         );
        return;
    }
    util.ExternalApiLoader.googleMapsCallBack();
};

/**
* @private
*/
util.ExternalApiLoader.googleMapsCallBack = function() {
    util.ExternalApiLoader.googMapsCallBack_.call();
};

/**
* @type {boolean}
* @private
*/
util.ExternalApiLoader.isGoogleMapsLoaded_ = false;

/**
* @type {Function}
* @private
*/
util.ExternalApiLoader.googMapsCallBack_ = function(){};

/**
* @type {Function}
*/
var ExternalApiLoaderGoogleMapsCallBack = function() {
    util.ExternalApiLoader.googleMapsCallBack();
};

goog.exportSymbol('ExternalApiLoaderGoogleMapsCallBack', ExternalApiLoaderGoogleMapsCallBack);
ExternalApiLoaderGoogleMapsCallBack = /** @type {Function} */goog.getObjectByName('ExternalApiLoaderGoogleMapsCallBack');

Useful Resources

Back to Blog