menu

Questions & Answers

How to load Google Maps in service in Angular 2

I managed to lazy load Angular's built-in Google Maps component as stated in the official doc:

export class GoogleMapsDemoComponent {
    apiLoaded: Observable<boolean>;
    geoCoder: any;

    constructor(httpClient: HttpClient) {
        this.apiLoaded = httpClient.jsonp('https://maps.googleapis.com/maps/api/js?key=YOUR_KEY_HERE', 'callback')
        .pipe(
          map(() => true),
          catchError(() => of(false)),
        );
     }
    
    ngOnInit(): void {
        this.apiLoaded.subscribe(() => this.initGeoCoder());
    }
    
    initGeoCoder() {
        this.geoCoder = new google.maps.Geocoder();
    }
}

I'm running into errors now though if the component gets initialised multiple times.

You have included the Google Maps JavaScript API multiple times on this page. This may cause unexpected errors.

So I want to move the loading of the Google Script to a dedicated service and inject it into all components that need it.

There is a discussion in the GitHub repo confirming this approach:

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.

I don't managed to do that properly though, I constantly get the following error message that appears to be raised when the Google script is not loaded yet:

ERROR ReferenceError: google is not defined

Service:

export class GeoService {
    apiLoaded: Observable<boolean>;

    constructor(private http: HttpClient) {

        this.apiLoaded = this.http
            .jsonp(
                'https://maps.googleapis.com/maps/api/js?key=API_KEY',
                'callback'
            )
            .pipe(
              map(() => true),
              catchError((error) => of(error))
            );
    }
}

Component:

export class GoogleMapsDemoComponent {
    apiLoaded: Observable<boolean>;
    geoCoder: any;

    constructor(geo: GeoService) {}
    
    ngOnInit(): void {
        this.geo.apiLoaded.subscribe(() => this.initGeoCoder());
    }
    
    initGeoCoder() {
        this.geoCoder = new google.maps.Geocoder();
    }
}

My service is in a shared module that is being imported into the module of the component. I also tried to use a service inside the same module just to make sure I'm not messing up import, but I received the same error message.

Can anyone tell me what I'm missing here?

Comments:
2023-01-18 05:42:03
If someone else thinks this question should be downvoted I'd really appreciate a small hint why. I can't see how this question could be improved.
2023-01-18 05:42:03
You'll definitely need to npm install the correct types (@types/googlemaps) but that's not all that will get in your way. For folks who are not seeing anything happen (and are not logging the result, success or failure of the jsonp call), make sure you've imported HttpClientModule and HttpClientJsonpModule in your module. Way to fail silently, Angular!
Answers(2) :

Here's my solution without creating elements on the DOM manually.

import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, of } from "rxjs";
import { catchError, map, share, tap } from "rxjs/operators";

@Injectable({ providedIn: 'root' })
export class GoogleMapsApiService {

    private _loaded: boolean;
    private observable: Observable<boolean>;

    constructor(
        private http: HttpClient
    ) {
    }

    load(): Observable<boolean> {
        // if the api has already been loaded, return true to indicate it has been completed
        if (this._loaded) return of(this._loaded);

        // if a request to load (observable) is NOT currently outstanding, create the request (observable)
        if (!this.observable) {
            // append: your api key to this url here: ?key=XXXXX
            this.observable = this.http.jsonp('https://maps.googleapis.com/maps/api/js?key=xxxx', 'callback')
                .pipe(
                    map(() => true),
                    share(),
                    tap(() => {
                        this._loaded = true;
                        // clear the outstanding request
                        this.observable = undefined;
                    }),
                    catchError(err => {
                        console.error(err);
                        return of(false);
                    })
                );
        }

        // return the observable
        return this.observable;
    }
}

I ended up with a combination of What's the best way to load the google maps api into angular2? and https://github.com/angular/components/issues/21665:

geo.service.ts:

geocode(
    request: google.maps.GeocoderRequest
 ): Observable<google.maps.GeocoderResult[]> {
    return new Observable<google.maps.GeocoderResult[]>((observer) => {
      if (!this.geocoder) {
        this.geocoder = new google.maps.Geocoder();
      }
      this.geocoder.geocode(request, (results, status) => {
        // need to manually trigger ngZone because "geocode" callback is not picked up properly by Angular
        this.ngZone.run(() => {
          if (status === google.maps.GeocoderStatus.OK) {
            // if status is "OK", the response contains a valid GeocoderResponse.
            observer.next(results);
            observer.complete();
          } else {
            observer.error(status);
          }
        });
      });
    });
  }

  loadGoogleMaps(url: string, id: string, callback): void {
    if (!document.getElementById(id)) {
      const script = document.createElement('script');
      script.type = 'text/javascript';
      script.src = url;
      script.id = id;
      if (callback) {
        script.addEventListener(
          'load',
          (e) => {
            callback(null, e);
          },
          false
        );
      }
      document.head.appendChild(script);
    }
  }

component:

ngAfterViewInit() {
  this.geo.loadGoogleMaps(environment.googleMapsURL, 'google-map', () => {
    this.initGeoCoder();
});

geoDataFromAddress(
    address: google.maps.GeocoderRequest['address']
  ): Observable<google.maps.GeocoderResult> {
    return this.geo
      .geocode({ address: address.toLocaleLowerCase() })
      .pipe(map((results) => results[0]));
  }