lance2k
lance2k

Reputation: 367

How to dynamically set angular-google-maps api key using Angular Service?

I'm using @agm/[email protected] and Angular 14.2.10. The api key is provided by the server, so I created an EnvService which has the following method

  /**
   * get google map api key
   * @returns Observable<any>
   */
  getMapsKey(): Observable<EnvResponse> {
    return this.getRequest(this.urls.base + '/env/maps-key').pipe(takeUntil(this.ngUnsubscribe));
  }

Which returns the following json response

{
    "googleMapsApiKey": "apiKey"
}

I followed this guide https://gist.github.com/msaxena25/fa59c2130642de3ef6330c6cff2ce6c2, but I could not get it to work. When I set { provide: MapsAPILoader, useClass: CustomLazyAPIKeyLoader} on my AppModule providers, I get the following errors on my PdoProfileComponent:

ERROR NullInjectorError: R3InjectorError(PdoProfileModule)[MapsAPILoader -> MapsAPILoader -> undefined -> undefined -> undefined]: 
  NullInjectorError: No provider for undefined!
    at NullInjector.get (core.mjs:6359:27)
    at R3Injector.get (core.mjs:6786:33)
    at R3Injector.get (core.mjs:6786:33)
    at R3Injector.get (core.mjs:6786:33)
    at injectInjectorOnly (core.mjs:4782:33)
    at Module.ɵɵinject (core.mjs:4786:60)
    at Object.CustomLazyAPIKeyLoader_Factory [as factory] (custom-apikey-loader.service.ts:14:45)
    at R3Injector.hydrate (core.mjs:6887:35)
    at R3Injector.get (core.mjs:6775:33)
    at R3Injector.get (core.mjs:6786:33)

pdo-profile.component.ts:132 ERROR Error: NG0200: Circular dependency in DI detected for MapsAPILoader. Find more at https://angular.io/errors/NG0200
    at throwCyclicDependencyError (core.mjs:240:11)
    at R3Injector.hydrate (core.mjs:6883:13)
    at R3Injector.get (core.mjs:6775:33)
    at R3Injector.get (core.mjs:6786:33)
    at ChainedInjector.get (core.mjs:13769:36)
    at lookupTokenUsingModuleInjector (core.mjs:3293:39)
    at getOrCreateInjectable (core.mjs:3338:12)
    at ɵɵdirectiveInject (core.mjs:10871:12)
    at Module.ɵɵinject (core.mjs:4786:60)
    at NodeInjectorFactory.GoogleMapsAPIWrapper_Factory [as factory] (agm-core.js:222:126)

It works when I remove { provide: MapsAPILoader, useClass: CustomLazyAPIKeyLoader} from my AppModule providers, but I won't be able to set my google map api key dynamically. How can I achieve my goal?

AppModule

@NgModule({
  imports: [
    ...,
    AgmCoreModule.forRoot({
      apiKey: 'initialKey',
      libraries: ['places']
    }),    
  ],
  declarations: [AppComponent, AdminLayoutComponent, AuthLayoutComponent, PdoLayoutComponent],
  providers: [
    InterceptService,
    {
      provide: HTTP_INTERCEPTORS,
      useClass: InterceptService,
      multi: true,
    },
    { provide: LocationStrategy, useClass: HashLocationStrategy },
    { provide: MapsAPILoader, useClass: CustomLazyAPIKeyLoader} // I don't get error when I remove this
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

PdoProfileModule (Child Module)

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild(PdoProfileRoutes),
    FormsModule,
    SharedModule,
    MatTooltipModule,
    MatGoogleMapsAutocompleteModule,
    AgmCoreModule,
  ],
  declarations: [PdoProfileComponent], // this component uses agm-map
})
export class PdoProfileModule {}

custom-apikey-loader.service.ts (this will dynamically set the api per guide)

import { GoogleMapsScriptProtocol, LAZY_MAPS_API_CONFIG, LazyMapsAPILoaderConfigLiteral, MapsAPILoader } from '@agm/core';
import { DocumentRef, WindowRef } from '@agm/core/utils/browser-globals.d';
import { Inject, Injectable } from '@angular/core';
import { EnvService } from './env.service';

@Injectable()
export class CustomLazyAPIKeyLoader extends MapsAPILoader {
    private _scriptLoadingPromise: Promise<any>;
    private _config: LazyMapsAPILoaderConfigLiteral;
    private _windowRef: WindowRef;
    private _documentRef: DocumentRef;
    private _key: string;

    constructor(@Inject(LAZY_MAPS_API_CONFIG) config: any, w: WindowRef, d: DocumentRef, private envService: EnvService) {
        super();
        this._config = config || {};
        this._windowRef = w;
        this._documentRef = d;

        //set google map api key
        this.envService.getMapsKey().subscribe((res) => {
          if (res.googleMapsApiKey) {
            this._key = res.googleMapsApiKey;
          }
        })

    }

    load(): Promise<any> {
        if (this._scriptLoadingPromise) {
            return this._scriptLoadingPromise;
        }

        const script = this._documentRef.getNativeDocument().createElement('script');
        script.type = 'text/javascript';
        script.async = true;
        script.defer = true;
        const callbackName: string = `angular2GoogleMapsLazyMapsAPILoader`;
        
        console.log('map key', this._key);
        this._config.apiKey = this._key;
        script.src = this._getScriptSrc(callbackName);
        this._documentRef.getNativeDocument().body.appendChild(script);

        this._scriptLoadingPromise = new Promise((resolve: Function, reject: Function) => {
            (this._windowRef.getNativeWindow())[callbackName] = () => { console.log("loaded"); resolve(); };

            script.onerror = (error: Event) => { reject(error); };
        });


        return this._scriptLoadingPromise;
    }

   //rest of the code is the same from https://gist.github.com/msaxena25/fa59c2130642de3ef6330c6cff2ce6c2

Upvotes: 0

Views: 373

Answers (1)

Ahmet Emrebas
Ahmet Emrebas

Reputation: 945

Make your api-request before loading the app, and store the keys/data/user-profile somewhere synchronous. Then you will be able to access them synchronously.


import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';


async function preload(){ 
     // Write your code here
    // Get data from server and set it somewhere(cache/localstore).

}

preload().then(()=>{
      bootstrapApplication(AppComponent, appConfig)
})

Alternatively, a dedicated angular module redirects clients to the corresponding pages based on user role, profile information, last-visited page, etc would be a good solution to such problems that require dynamic data. We can call this module a preflight module.

Upvotes: 1

Related Questions