So great to see google maps being added as apart of this library. I have had an internal version for my own projects for some time(back since angular.js and now upgraded/rewrite for angular 2+) now which wrapped the google maps lib to provide an angular integration.
One of the major issues we had was you do not want to wait for or load the google maps api on every page when it isn't needed. For our use case we were only using it on a details page for 2 records + on some reports if they happened to have a map. It required some heavy changes to make all google map related calls actually start from a promise/observable which loads the sdk first if it isn't already present.
I get that there is the async
and other script tags to defer loading but still once you need this feature it is basically a must have and it also requires some hard API changes so I'd really hope it's could be applied sooner rather than later.
I currently have directives/components for the following:
@mbehrlich From release notes I think you are the primary person working on maps. Would be great to get your thoughts on this.
Most apps aren't loading maps on every page and the maps sdk is quite large if you aren't using it.
@epelc to clarify, are you talking about dynamically loading the _Angular component_ or dynamically loading the underlying Google Maps API? Currently the Angular component shouldn't have any opinions about how/when the underlying maps API is loaded.
I am saying to dynamically load the underlying maps api. Usually you could
manage yourself but I guess youโd need the library to either expose this
loading(I used this option to great effect) or have it be able to request
the maps lib be loaded when it is needed. Does that make sense?
The Angular component doesn't care when the Google Maps API is loaded, so long as it's before you try to _show_ the Angular component. Here's an example that dynamically loads the Google Maps API before showing the component: https://stackblitz.com/edit/angular-sym9gg
I agree with @epelc on loading the SDK only when you are actually showing an instance of a map. I required 2-way-binding with map markers and polygons so I developed a similar Google Maps Angular wrapper for internal use.
To achieve this I had a GoogleMapsApiLoaderService inject the maps api script and provide an observable to fire when the script is loaded. The map component would init this service and wait for the script before attempting to access anything under google.maps namespace. Components nested in the Map components gets a reference to the Map so they, in turn, wait for the map to initialize before attempting to use the google.maps namespace.
Regardless of how you are async loading the maps api script, it opens up a can of worms. Here are some of the problems I ran into:
Here is an example of a Polygon directive with 2-way binding: polygon.directive.ts
Would be nice to have an onReady()
output event similar to the one in the agm library here.
@Output() mapReady: EventEmitter<any> = new EventEmitter<any>();
I toggle the google-map with an *ngIf and I need to know when it appears again to add my Drawing Controls on it.
https://github.com/angular/components/pull/20492/files
https://github.com/angular/components/tree/master/src/google-maps#lazy-loading-the-api
This new doc lazyload example would have an issue with
You have included the Google Maps JavaScript API multiple times on this page. This may cause unexpected errors.
when the same Google map component is used multiple times within a single-page app.
any better solutions?
Hi @weilinzung looks unrelated to this issue but I took a quick peak.
Basically that example is loading the api every time you use that component even if it's only 1 page and you end up navigating via router to that page, another page, then back again you end up with the google maps api loaded multiple times. That example looks like it is broken outside of a single page single component demo.
You should be able to refactor the httpClient.jsonp('https://maps.googleapis.com/maps/api/js?key=YOUR_KEY_HERE', 'callback')
and surrounding api loading into a service so that it can act as a singleton and prevent multiple loads. Note you will want to add providedIn: 'root'
to your @Injectable
so that it does not create an instance per module and thus load the api for each instance.
@epelc thanks for that info. I am kind of new with Angular providedIn: 'root'
, do you know any simple examples for that? thank you!
@weilinzung should explain it here https://angular.io/tutorial/toh-pt4
Basically run ng generate service GMapLoader
then move the apiLoaded
field onto that and start using the service instead of the raw field on your components.
@epelc If use the service method and add to the shared map component, wouldn't still load httpClient.jsonp('https://maps.googleapis.com/maps/api/js?key=YOUR_KEY_HERE', 'callback')
multiple times?
I had to move everything to the top top level, so I think that way could work for me.
I can't fix duplicate loading even after creating a singleton service. Can you please explain what does "move everything to the top top level" mean?
A singleton service by itself isn't enough because the Observable created from HttpClient sends an HTTP request every time it is subscribed to. We solved this by creating an injection token that only issues the HTTP request the first time the observable is subscribed to.
export const MAP_LOADED = new InjectionToken<Observable<boolean>>('Google Maps API Loaded');
// in module
{provide: MAP_LOADED, useFactory: loadMapApi, deps: [HttpClient]}
function loadMapApi(httpClient: HttpClient) {
return httpsClient.jsonp('https://maps.googleapis.com/maps/api/js?key=YOUR_KEY_HERE', 'callback).pipe(
shareReplay({bufferSize: 1, refCount: true),
map(() => true),
catchError(e => {
console.log(e);
return of(false);
})
);
}
// in component
constructor(@Inject(MAP_LOADED) public mapLoaded$: Observable<boolean>) { }
<google-map *ngIf="mapLoaded$ | async"></google-map>
However, this approach does not work if you want to lazy load the map API in different modules with different libraries as the URL is not the same.
Hi guys! What about that?
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root',
})
export class GoogleMapsService {
private isApiLoaded: BehaviorSubject<boolean> = new BehaviorSubject(false);
isApiLoaded$ = this.isApiLoaded.asObservable();
constructor(httpClient: HttpClient) {
console.log('๐ ~ GoogleMapsService ~ loading google maps api...');
const key = environment.googleMapsApiKey;
httpClient
.jsonp(`https://maps.googleapis.com/maps/api/js?key=${key}`, 'callback')
.subscribe(
() => {
console.log('๐ ~ GoogleMapsService ~ google maps api loaded');
this.isApiLoaded.next(true);
},
(error) => {
console.log('๐ ~ GoogleMapsService ~ google maps api cannot be loaded', error);
}
);
}
}
Hi guys! What about that?
import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', }) export class GoogleMapsService { private isApiLoaded: BehaviorSubject<boolean> = new BehaviorSubject(false); isApiLoaded$ = this.isApiLoaded.asObservable(); constructor(httpClient: HttpClient) { console.log('๐ ~ GoogleMapsService ~ loading google maps api...'); const key = environment.googleMapsApiKey; httpClient .jsonp(`https://maps.googleapis.com/maps/api/js?key=${key}`, 'callback') .subscribe( () => { console.log('๐ ~ GoogleMapsService ~ google maps api loaded'); this.isApiLoaded.next(true); }, (error) => { console.log('๐ ~ GoogleMapsService ~ google maps api cannot be loaded', error); } ); } }
That seems like it would generally work but you'd have to be careful about providing that service separately at the component level as it would create a new instance and therefore load the script twice.
@Component({
provide: [GoogleMapsService] // will load a second time
})
I'd still suggest storing the replay of the observable.
>
That seems like it would generally work but you'd have to be careful about providing that service separately at the component level as it would create a new instance and therefore load the script twice.
@Component({ provide: [GoogleMapsService] // will load a second time })
I'd still suggest storing the replay of the observable.
first of all, thank you for your suggestions ๐
I'm using this service in 3 different ionic pages (โpageโ is just terminology to identify a component that is being used as a screen in an Ionic application, there is nothing special that differentiates a page to a normal Angular component.):
import { Component, OnInit } from '@angular/core';
import { GoogleMapsService } from 'src/app/core/services/internal/google-maps.service';
@Component({
selector: 'app-asset-form',
templateUrl: './asset-form.page.html',
styleUrls: ['./asset-form.page.scss'],
})
export class AssetFormPage implements OnInit {
isMapApiLoaded$: Observable<boolean>;
mapOptions: google.maps.MapOptions;
mapMarkerOptions: google.maps.MarkerOptions = {
draggable: false,
};
mapMarkerPositions: google.maps.LatLngLiteral[] = [];
constructor(private readonly googleMapsService: GoogleMapsService) {}
ngOnInit() {
this.initMap();
}
private initMap() {
this.isMapApiLoaded$ = this.googleMapsService.isApiLoaded$;
this.mapOptions = {
center: {
lat: 38.8303836,
lng: 0.1041089,
},
disableDefaultUI: true,
gestureHandling: 'cooperative',
zoom: 5,
};
}
}
I didn't see nor You have included the Google Maps JavaScript API multiple times on this page. This may cause unexpected errors
not๐ ~ GoogleMapsService ~ google maps api loaded
messages in console
Is this ok or I must to improve something?
Thanks in advance!
Most helpful comment
https://github.com/angular/components/pull/20492/files
https://github.com/angular/components/tree/master/src/google-maps#lazy-loading-the-api
This new doc lazyload example would have an issue with
You have included the Google Maps JavaScript API multiple times on this page. This may cause unexpected errors.
when the same Google map component is used multiple times within a single-page app.any better solutions?