7

I'm using Gulp and Browserify to bundle my JavaScripts. I need to expose a callback function that should be executed after the Google Maps API loads.

How can this be done without using something like window.initMap? The problem with this is that I need to fire a large number of other methods inside initMap, so there has to be a better way of doing it besides using window.functionName and polluting the global namespace.

On the other hand, is it alright to just exclude the callback parameter and do something like this instead?

$.getScript('https://maps.googleapis.com/maps/api/js').done(function() {
  initMap();
});

Any help would be greatly appreciated. I have spent more time that I would ever admit in getting this to work.

gulpfile.js:

gulp.task('browserify', ['eslint'], function() {
  return browserify('/src/js/main.js')
    .bundle()
    .pipe(source('main.js'))
    .pipe(buffer())
    .pipe(gulp.dest('/dist/js'))
    .pipe(reload({ stream: true }));
});

main.js:

require('jquery');
require('./map');

map.js:

var map = (function() {
  'use strict';

  var mapElement = $('#map');

  function googleMapsAPI() {
    $.getScript('https://maps.googleapis.com/maps/api/js?callback=initMap');
  }

  function initMap() {
    var theMap = new google.maps.Map(mapElement);
    // functions...
  }

  function init() {
    googleMapsAPI();
  }
});

map.init();
Cofey
  • 11,144
  • 16
  • 52
  • 74

4 Answers4

3

No, it's not okay to not include the callback parameter.

The google maps API library calls a bunch of other scripts to be loaded on the page and then, when they have all been loaded, the callback parameter is called on the window object.

Just declare it on the window object:

var MyApp = {
    init: function() {
         //all your stuff
    }
}

window.initMap = function() {
   window.initMap = null; //set this to null this so that it can't get called anymore....if you want
   MyApp.init();
};

and then just include the script tag on your page:

<script src="https://maps.googleapis.com/maps/api/js?callback=initMap"></script>
Adam Jenkins
  • 51,445
  • 11
  • 72
  • 100
  • The problem is that I need to call all sorts of other custom functions after `initMap()` is fired. If I call them with `map.functionName()`, I get a function not defined error. The only way around it is to use `window.functionName()`, but that pollutes the global namespace. – Cofey Aug 14 '17 at 16:39
  • 1
    @Cofey - you don't have any choice but to *pollute the global namespace* because that is the only way google maps can use `callback`. A lot of people think `don't ever put anything in the global scope because doing so is bad` but that's wrong. What you should be thinking is `don't put anything in the global scope that you don't absolutely need to be there`. You **need** the function defined by `callback=initMap` (which is `initMap`) to be in the global namespace. Don't sweat it. – Adam Jenkins Aug 14 '17 at 16:42
  • Further, when I'm developing client side, I typically define my own namepsace that I can *hang stuff* on. I often have `window.myNameSpace = window.myNameSpace || {}` in my project and then hang various things off of `window.myNameSpace` like `window.myNameSpace.constants = { CONSTANT_ONE = 123456 }` – Adam Jenkins Aug 14 '17 at 16:45
  • I tried out your suggestion, but Browserify is wrapping all of the code, so I keep getting the error message "initMap is not a function". What needs to be changed to get `initMap()` to be seen outside of Browserify's code? – Cofey Aug 14 '17 at 17:37
  • Show your code, by explicitly setting `window.initMap = function() {...}` browserify can't touch it. It can wrap it in as many IIFE's as it wants, but `window` is a global object. Just to be safe, make sure you include your browserify bundle after your google maps script. Without you showing the contents of your modules, though, we can't tell what's wrong. – Adam Jenkins Aug 14 '17 at 17:39
  • For whatever reason, it _didn't_ work when the Google Maps script was _above_ `main.js`. Once I moved the Google Maps script _after_ `main.js` it started working. Any ideas why? Should `async` or `defer` be used? – Cofey Aug 14 '17 at 17:53
  • Like I said above, the main URL for the google maps API loads a script from another URL and calls the specified callback when **that** script has loaded. The thing is, that script might be in your browsers cache (and hence it will load immediately, forcing your callback to be called virtually immediately) or it might not be and have to be requested forcing your callback to appear more asynchronous. For this reason, you must always define the method used in your `callback=initMap` parameter **before** your google maps script tag, whether you have the script tag specified as `async` or not. – Adam Jenkins Aug 14 '17 at 17:57
  • Hey @Adam -Why do you say the `callback` is required? I understand it's Google's documented method, but have you tried not using it and have had failed results? What's you /reasoning/understanding behind that, as I've used the API without a callback and had no issues. Am I missing something on this? – lscmaro Aug 21 '17 at 03:15
  • 1
    I didn't say it was required, I said it's not okay. You're alternative is to load maps synchronously (bad idea) or use something like setTinwout to hope that the maps API has loaded in time. Just use the callback parameter – Adam Jenkins Aug 21 '17 at 09:58
1

If you want to load the script and then do something when the script has been loaded, you can set the attributes async and onload when injecting the script. By wrapping all the code into an IIFE we will keep private all objects defined inside the IIFE, avoiding populate the global namespace window. See the following example:

// IIFE (Immediately-Invoked Function Expression)
// Keeps all private
!function() {
/**
 * Injects the script asynchronously.
 *
 * @param {String} url: the URL from where the script will be loaded
 * @param {Function} callback: function executed after the script is loaded
 */
function inject(url, callback) {
  var tag = 'script',
    script = document.createElement(tag),
    first = document.getElementsByTagName(tag)[0];
  script.defer = script.async = 1; // true
  script.type = 'text/javascript';
  script.src = url;
  script.onload = callback;
  first.parentNode.insertBefore(script, first);
}

/**
 * Injects and initializes the google maps api script.
 */
function injectMapsApi() {
  var key = 'your-api-key';
  var query = '?key=' + key;
  var url = 'https://maps.googleapis.com/maps/api/js' + query;
  inject(url, initMapsApi);
}

/**
 * Callback that initializes the google maps api script.
 */
function initMapsApi() {
  var maps = window.google.maps;
  // ... code initializations
  console.log(maps);
}

injectMapsApi();

}(); // end IIFE

You need to register and claim you API key in order to use the google maps API. More information here:

jherax
  • 5,238
  • 5
  • 38
  • 50
  • I really appreciate that you brought in the IIFE option, as my inaugural project template uses an IIFE and I was trying to figure out how to do this. Lots I don't fully understand. In your `inject` function, can you point me towards a resource I can read that explains the design and what you accomplish with the function? – tim.rohrer Jan 08 '18 at 12:58
  • @tim.rohrer the function `inject()` creates a HTML element ` – jherax Jan 08 '18 at 15:37
  • Thanks @jherax. So, this is for a `script` object? https://www.w3schools.com/jsref/dom_obj_script.asp – tim.rohrer Jan 18 '18 at 02:39
  • 1
    @tim.rohrer Yes Tim, it is for **`DOM Script Object`** – jherax Jan 18 '18 at 16:28
0

I honestly think here it is a better solution to simply define a global initMap function to keep things simple while taking advantage of Google Maps asynchronous initialization. It might sound like a hack, but you can define a random name for the function and then simply remove it from the global scope once Google Maps SDK has called it. This mechanism is similar to the one used in JSONP.

var functionName = getRandomName();
window[functionName] = function() {
    window[functionName] = undefined;
    // call to your initialization functions
};

In this answer you can check out that the way to prevent polluting the global scope is to make the google maps script load synchronously, what could harm user experience, specially on smartphones.

jorgonor
  • 1,679
  • 10
  • 16
0

I've had issues with this approach Google has taken also. I don't like it very much myself.

My way to deal with this as of late has been creating the global function, with a twist of firing an event to trigger my actual application javascript. This way I have my application JS clean of dealing the maps API handling, and it's one small global function call outside of my main object.

function initMap(){
  $(document).ready(function(){
    $(window).on('GoogleMapsLoaded', myObj.init());
    $(window).trigger('GoogleMapsLoaded');
  });
};

With that I just include the callback=initMap in the script url.

UPDATE: Another option is to just include your callback as a function inside your object. Ex: your object could be something like

var app = app || {};

(function($){

   $(function(){
      $.extend(app, {
        initMap:function(yourMainWrapDiv){
           //Do whatever you need to do after the map has loaded
        },
        mapLoadFunction(){
           //Map API has loaded, run the init for my whole object
           this.initMap($('#mainWrapper'))
        },
        mainInit: function(){
           ///do all your JS that can or needs  
           // to be done before the map API loads
           this.maybeSetSomeBindings();
        },
        maybeSetSomeBindings: function(){
             //do things
        }
      });
      //On document ready trigger your mainInit
      //To do other things while maps API loads
      app.mainInit()
   });
})(jQuery);

Then you can just use the callback to jump inside your one global object and run what you need to run just for the map handling. Your API url could be with callback=app.initMap

That could keep it cleaner also

UPDATE 2: Yet another option (I minimally tested) would be to NOT use the callback parameter in the Google API url, and link it with whatever else, library wise, you needed. (places, search, etc). https://maps.googleapis.com/maps/api/js?key=YOUR-KEY-HERE&libraries=places for example.

Then in your object init function just set a timer with to see if the google object is available! Maybe something like this:

var app = app || {};

(function($){

   $(function(){
      $.extend(app, {
        init:function(){
          var self = this;
          var timer = setInterval(function(){
              if ($('.ex').length){ //but really check for google object
              console.log('things exist google, elements, etc..');
                  self.next();
                  clearInterval(timer);
              }
          });
        },
        next:function(){
          console.log('google object exists')
        }
      });
      app.init()
   });
})(jQuery);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div class='ex'>as an example for something to trigger a flag (true/false) to clear the interval</div>

in any case where you try to access the global object, in this case app, as a callback in the URL you would set callback=app.yourFunctionToCall NOT callback=app.funtionToCall() you script tag should also have the async and defer attributes attributed to it to promote further html parsing (your app's js should be directly after the maps script)

lscmaro
  • 995
  • 8
  • 22