js-api-loader icon indicating copy to clipboard operation
js-api-loader copied to clipboard

Dynamically load map libraries

Open indrimuska opened this issue 5 years ago • 10 comments

Hi all, This version of the map loader, like the previous one, does not provide the ability to incrementally and dynamically load libraries at different instance of time.

In my use case I have a single page app with forms which show an Autocomplete, a Map or a Map with drawing tools, depending on the selected route. I have modeled these control in reusable building blocks (e.g. React components, Angular directives, Web Elements, ...), but unfortunately each control requires a different subset of map library:

  • autocomplete: ["places"]
  • map: ["places"],
  • map with drawing tools: ["places", "drawing"]

If I am not wrong, this loader should be able to guarantee 1-time-loading of libraries, so if I require "places", then I cannot load "drawing" later anymore. Basically the loader dies as soon as you use it (actually it may work if I use it multiple times, but I am not sure if the previously provided callback will be fired again).

image

The loader should take the ownership of handling multiple loading requests, also avoiding to load libraries which are already in place.

SBE:

const loader1 = new Loader({ apiKey, libraries: ["places"] });
await loader1.load(); // <-- loads "places" lib

const loader2 = new Loader({ apiKey, libraries: ["places", "drawing"] });
await loader2.load(); // <-- loads only "drawing" lib

The implementations is also quite easy, broadly like the following:

libraries: Libraries;
constructor(libraries: Libraries) {
   // filter out already loaded services
   const librariesToLoad = libraries.filter(library => this.libraries.indexOf(library) < 0);
   // everything is loaded already?
   if (librariesToLoad.length === 0) return;
   // save reference to new libs
   this.libraries.push(...librariesToLoad);
   // create tag and load `librariesToLoad` libs
   // ...
}

For what concern the callback to be provided in the URL, this can be dynamically provided with an incremental name:

window[`__googleMapsCallback_${++this.cbNo}`] = this.callback.bind(this);

url += `?callback=__googleMapsCallback_${this.cbNo}`;

Thanks, Indri

indrimuska avatar Apr 13 '20 09:04 indrimuska

Thanks for reporting this use case. I'm not sure we will be able to support it though. If I make a request to load without any libraries followed by another with a library, I get the error about the API being loaded multiple times. It may work, but is probably fragile and could be broken in future releases. I'll dig a bit deeper internally to see what that error is actually about.

Here is a simple jsfiddle with two script loads demonstrating error: https://jsfiddle.net/jwpoehnelt/3ft4m92d/3/.

jpoehnelt avatar Apr 13 '20 16:04 jpoehnelt

This issue(multiple loads) has been around for years. I would be concerned about any state tied to existing objects and billing implications. I'm think I am going to take a different route here albeit a longer term one and advocate for a public method to load additional libraries. Something like the following:

google.maps.load('places').then(...)

// or

google.maps.loadLibrary('places').then(...)

// or

google.maps.loadLibraries(['places']).then(...)

From a first glance at the internals, there is nothing fundamentally complex about this but will require some time for it to be released.

jpoehnelt avatar Apr 13 '20 16:04 jpoehnelt

@jpoehnelt I would like to add one more use-case for dynamic load map - language switch.

I'm a maintainer of @react-google-maps/api https://www.npmjs.com/package/@react-google-maps/api I've released new version 1.12.0 with React hook based on your library.

I have my own script for loading googlemapsapi, and I'm seeing that with your script it is not possible to build url using googleMapsClientId and channel params.

One more request: could you please export type Libraries for usage? I had to copy it from your project to mine.

JustFly1984 avatar Oct 11 '20 07:10 JustFly1984

@JustFly1984

For the language request, can you file a feature request at https://issuetracker.google.com/issues/new?component=188853&template=787814.

Premium plans are now defunct and most of these will expire or be expiring shortly. No other users require client id and/or channel. I opened #69 for this.

jpoehnelt avatar Oct 12 '20 15:10 jpoehnelt

@jpoehnelt I have created an issue, please follow up. https://issuetracker.google.com/issues/170657543

JustFly1984 avatar Oct 13 '20 07:10 JustFly1984

@jpoehnelt it turns out, there is already an issue exists https://issuetracker.google.com/issues/35819089

JustFly1984 avatar Oct 13 '20 09:10 JustFly1984

channel has been added to the latest release in #72.

jpoehnelt avatar Oct 13 '20 13:10 jpoehnelt

any updates?

wlbksy avatar Jan 18 '23 07:01 wlbksy

To answer OP's question, I ran in to the .load() method being deprecated, and the same issue with Autocomplete and Geocoding libraries only being needed on specific routes.

The function below can be imported and awaited anywhere, to check that window.google.maps exists in your global scope. You want to specify all the libraries you'll need to use throughout your application, so they all get appended to the maps URL.

const LoadGoogleMaps = async () => {
  // Check if Google Maps has already been loaded
  if (!window?.google?.maps) {
    return await new Loader({
      apiKey: process.env.GOOGLE_MAPS_API_KEY,
      version: 'weekly',
      libraries: ['core', 'maps', 'places', 'geometry', 'geocoding']
    // Use .importLibrary('core'); in place of .load() to make google.maps available in the global scope.
    }).importLibrary('core');
  }
};

Then throughout your application at runtime, you can use google.maps.importLibrary() to make any library available in your local scope. You can directly await and use the constructors or methods rather than accessing them from google.maps.

For example:

try {
  // Verify Google Maps JS API is loaded
  await LoadGoogleMaps();
  // Instantiate Autocomplete Service
  const { AutocompleteService } = await google.maps.importLibrary('places');
  const $autocomplete = new AutocompleteService();
  // Instantiate Geocoding Service
  const { Geocoder } = await google.maps.importLibrary('geocoding');
  const $geocoder = new Geocoder();
} catch (error) {
  console.error(error);
}

This documentation was very helpful: https://developers.google.com/maps/documentation/javascript/load-maps-js-api#js-api-loader, but the key for me was using .importLibrary('core') in place of .load().

Hope this helps anyone else who comes across the "You have included Google Maps API multiple times" warning in the console.

dinomastrianni avatar Aug 09 '23 19:08 dinomastrianni

In case it helps somebody, the multiple import issue for me was resolved using Promise.all

const [mapsLibrary, markerLibrary] = await Promise.all([
  loader.importLibrary("maps"),
  loader.importLibrary("marker"),
]);

danieldbird avatar Jan 09 '24 03:01 danieldbird