googlemaps / js-api-loader

Load the Google Maps JavaScript API script dynamically.
Apache License 2.0
337 stars 64 forks source link

Dynamically load map libraries #5

Open indrimuska opened 4 years ago

indrimuska commented 4 years ago

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:

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

jpoehnelt commented 4 years ago

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 commented 4 years ago

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.

JustFly1984 commented 3 years ago

@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.

jpoehnelt commented 3 years ago

@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.

JustFly1984 commented 3 years ago

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

JustFly1984 commented 3 years ago

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

jpoehnelt commented 3 years ago

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

wlbksy commented 1 year ago

any updates?

dinomastrianni commented 1 year ago

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.

danieldbird commented 7 months ago

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"),
]);