1

I am trying to write a re-usable drop-in Module to load Google Maps asynchronously and return a promise.

Here is the code I came up with, using AngularJS.

However, there is a re-usability drawback of creating a global callback function "behind the scene". Which is a side effect that may result in bugs, if the same namespace happens to be used by any other library.

My questions - is there any way to achieve this effect without creating global variables.

Below is the code that creates that "evil" global callback:

// Google async initializer needs global function, so we use $window
angular.module('GoogleMapsInitializer')
    .factory('Initializer', function($window, $q){

        // maps loader deferred object
        var mapsDefer = $q.defer();

        // Google's url for async maps initialization accepting callback function
        var asyncUrl = 'https://maps.googleapis.com/maps/api/js?callback=';

        // async loader
        var asyncLoad = function(asyncUrl, callbackName) {
          var script = document.createElement('script');
          //script.type = 'text/javascript';
          script.src = asyncUrl + callbackName;
          document.body.appendChild(script);
        };

// Here is the bad guy:

        // callback function - resolving promise after maps successfully loaded
        $window.googleMapsInitialized = function () {
            mapsDefer.resolve();
        };

        // loading google maps
        asyncLoad(asyncUrl, 'googleMapsInitialized');

        return {

            // usage: Initializer.mapsInitialized.then(callback)
            mapsInitialized : mapsDefer.promise
        };
    })
Community
  • 1
  • 1
Dmitri Zaitsev
  • 13,548
  • 11
  • 76
  • 110

1 Answers1

3

The script loading technique for getting data from a server on a cross-origin domain that you are referring to is JSONP. You can read more about it here. Somewhat by definition, JSONP can only work by calling a globally scoped function.

So, in direct answer to your question: no you can't use JSONP cross-origin techniques without the global function - that's just how the mechanism works. A script is executed in the global namespace and it has to call a function it can reach from the global namespace. Even jQuery and YUI do something like this for implementing JSONP.

And, since you are using Angular, it already has JSONP functionality built into it. See the doc here so you don't have to create your own mechanism for doing this.


But, that said, if you were making your own, you can make it much less likely that your global function will collide with anyone else's code or even with another instance of your library by taking some precautions to make the global names you create more random.


Here's an example of how you can make the odds of any sort of naming collision very small. This uses three techniques:

  1. Use some leading underscores on your prefix.
  2. Add a random sequence of digits to the function name.
  3. Add a time stamp to the function name.
  4. Remove the global after it is used.

Here is your code with those aspects implemented.

// Google async initializer needs global function, so we use $window
angular.module('GoogleMapsInitializer')
    .factory('Initializer', function($window, $q){

        // maps loader deferred object
        var mapsDefer = $q.defer();

        // Google's url for async maps initialization accepting callback function
        var asyncUrl = 'https://maps.googleapis.com/maps/api/js?callback=';

        // async loader
        var asyncLoad = function(asyncUrl, callbackName) {
          var script = document.createElement('script');
          //script.type = 'text/javascript';
          script.src = asyncUrl + callbackName;
          document.body.appendChild(script);
        };

        // generate a unique function name
        // includes prefix, current time and random number
        var fname = "__googleMapsInitialized_" + 
            (new Date().getTime()) + "_" + 
            (Math.random() + "").replace(".", "");

        // callback function - resolving promise after maps successfully loaded
        $window[fname] = function () {
            mapsDefer.resolve();
            // remove the global now that we're done with it
            delete $window[fname];
        };
        // loading google maps
        asyncLoad(asyncUrl, fname);

        return {

            // usage: Initializer.mapsInitialized.then(callback)
            mapsInitialized : mapsDefer.promise
        };
    })

Demo of the unique function name generator: http://jsfiddle.net/jfriend00/oms7vc6o/

P.S. I don't know Angular myself, but it appears that Angular already knows how to make JSONP calls all by itself so you don't have to make your own solution here. See this Angular doc page and this other question and this article for details.

Community
  • 1
  • 1
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Added reference to another article that shows you how to use built-in Angular functionality for this JSONP call rather than making your own JSONP mechanism. – jfriend00 Aug 11 '14 at 08:56
  • +1 Great answer! Totally forgot about `JSONP`, wish folks from Google mention it when giving the code sample. – Dmitri Zaitsev Aug 11 '14 at 09:52
  • It turned out that Google Maps do not support `JSONP`(http://www.quora.com/Why-doesnt-the-Google-Maps-API-support-JSONP) and that call is actually not `JSONP`, and hence cannot be used with Angular's `$http.jsonp`, see http://jsbin.com/melob/2/edit. So the only way is to use your awesome solution, which works! – Dmitri Zaitsev Aug 11 '14 at 17:34
  • @DmitriZaitsev - most people just use Google's JS interface to Google Maps: https://developers.google.com/maps/documentation/javascript/services?csw=1#Geocoding and let them worry about the details of the request. – jfriend00 Aug 11 '14 at 17:50
  • @DmitriZaitsev - it appears that Google maps uses the whole JSONP mechanism, but produces the result as an already-formed javascript object, not a JSON piece of text that can be parsed into an object. So, thus it doesn't work with standard JSONP handling code. I'd still suggest you just use the Google JS API for maps and not have to worry about these details. – jfriend00 Aug 11 '14 at 17:57
  • I don't quite understand - I am already using their API but I need the async initializer. Is there any other way? – Dmitri Zaitsev Aug 11 '14 at 18:49
  • @DmitriZaitsev - OK, I see. I don't know of any other way to do the async initialize. It looks like you can just go with what you have now. – jfriend00 Aug 11 '14 at 19:31