0

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
2
  • why are you exposing your API key in http request? anyone can get it from network tab. you should add your Keys into environment.ts file. Commented Mar 1, 2024 at 12:57
  • to update APIs without rebuilding our FE app every-time we change our APIs. Also the end-point is protected, only authenticated users can use the endpoint and we plan to encrypt the http response. But if this get's too complex, when plan to ditch google api and just set it on angular's environment.ts file. Commented Mar 1, 2024 at 16:31

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.