'How to wait for google maps API script to return before calling custom directive on main page of website in Angular

Problem -Google maps autocomplete drop down isn't working on my website main page after parsing and loading the google maps API script. I'm experiencing what I believe to be a race condition on my websites main page, in an input field inside a navbar at the top, where I'm using the google maps autocomplete feature in a custom autocomplete directive. The problem, which is the autocomplete drop down isn't working is occurring in mobile only, desktop seems to be ok. I'm using '<script async defer...' to prevent waiting for the script to finish DL'ing to prase the page, so I believe the directive (containing the places autocomplete) on the input tag is parsed and rendered and being instantiated before the script has time to finish loading. Result, the dropdown doesn't work on the main page (mobile only), other pages are fine.

Solution 1 - Remove 'async defer' and load synchronously, which solves the problem and seems to load pretty fast, but not my ideal solution especially for my websites main page. Maybe someone can chime in here with their thoughts on sync loading maps API before parsing the page?

Solution 2 - Load the Google API script asynchronously and wait for it to finish before instantiating the places autocomplete object and listener inside the directive. I'd like to use the built in callback feature but unsure if this will work, but it seems like an avenue I should try...

Here is my attempt but I need help as I have errors.

Index.html - pseudo code

<html lang="en">
<head>
  // other tags left out for brevity
  <script src="./index.js"></script>
  <script src="https://maps.googleapis.com/maps/api/js?key=secret-key&libraries=places&callback=initMap" async defer></script>
</head>
<body>
  <app-root></app-root>
</body>
</html>

Mainpage.html - pseudo code created for brevity

<nav class="navbar navbar-light bg-light fixed-top">
  <form class="form-inline">
    <input appGooglePlaces class="form-control google-place-input" placeholder="Search" aria-label="Search" (onSelect)="fetchResults($event)">
  </form>
</nav>

Index.ts - I created this file to receive the callback 'initMap' from the script, but not being called and receiving an error saying.

localhost/:1 Uncaught (in promise) fe {message: 'initMap is not a function', name: 'InvalidValueError', stack: 'Error\n at new fe (https://maps.googleapis.com/m…GUFa3DM&libraries=places&callback=initMap:197:125'}

function initMap(): void {
  // not being hit
  console.log('callback hit');
  // instantiate the objects inside the 'appGooglePlaces' directive so we can use it
}
export {
  initMap
};

FYI - I can embed the callback function right inside the index.html file inside a script tag, but then I can't call other Angular components, services, directives, etc. Is this possible in Angular?

appGooglePlaces - directive

import {
  Directive,
  ElementRef,
  OnInit,
  Output,
  EventEmitter,
  Input
} from '@angular/core';
import {
  Address
} from '../shared/models/address';

@Directive({
  selector: '[appGooglePlaces]'
})
export class GooglePlacesDirective implements OnInit {
  private element: HTMLInputElement;
  private autocomplete: google.maps.places.Autocomplete;
  @Output() onSelect: EventEmitter < any > = new EventEmitter();
  @Input() countrySelected = '';

  constructor(private elRef: ElementRef) {
    this.element = elRef.nativeElement;
  }

  ngOnInit() {

    this.autocomplete = new google.maps.places.Autocomplete(this.element);

    google.maps.event.addListener(this.autocomplete, 'place_changed', () => {

      const place = this.autocomplete.getPlace();

      if (place.name === '') {
        return false;
      }

      const address = new Address();
      // address processing left out for brevity
      this.onSelect.emit(address);
     
    });

  }



}


Solution 1:[1]

I think I have something that might work for you along the lines of "Solution 2" in the question...

Here is a working example that works on the premise that there is a global callback located in the index.html that would be the callback hit from maps.

<script>
  console.log('calling globalMethod in 5 seconds...');
  window.setTimeout(() => {
    window.globalMethod('THIS IS A NEW VALUE FROM OUTSIDE ANGULAR');
  }, 5000);
</script>

Then from within app.component (or wherever component you want) there is a window method defined in the constructor.

constructor(public zone: NgZone) {
    (window as any).globalMethod = (someValue) => {
      console.log('globalMethod >> ' + someValue);
      this.zone.run(() => (this.name = someValue));
    };
  }

This is the line of code that will force Angular to run an update that occurred from outside of Angular this.zone.run(() => (this.name = someValue));

So my suggestion in your application is that you could set an *ngIf variable here that would then render your map.

this.zone.run(() => (this.showMap = true));

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Zze