3

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);
     
    });

  }



}

11
  • Not sure what you mean, I'm just following this google created stackblitz here - stackblitz.com/github/googlemaps/js-samples/tree/… Commented Feb 27, 2022 at 23:01
  • Sorry, I forgot to include that in the code above. I do have it and still getting the error above. I'll update my code above. Commented Feb 27, 2022 at 23:14
  • One thing that's a little confusing is why the tag is .js (in the link), when the file type is .ts? Maybe I don't understand Angular file naming conventions well enough. Commented Feb 27, 2022 at 23:16
  • I understand that much, it's list a little confusing because I'm not using .JS anywhere else in the project to reference a .ts file function. Of course I'm not really doing anything else like this in my project and this is my first Ng project. Commented Feb 27, 2022 at 23:20
  • Maybe a better question for me to ask you is, is there a more Angular way to use the callback function from the script? Commented Feb 27, 2022 at 23:21

1 Answer 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));
Sign up to request clarification or add additional context in comments.

6 Comments

Hi Zze. Thanks for the help. I have a couple of questions. 1. Why didn't you use the Google maps built in callback function? 2. Why did you use a timeout? FYI - I want some method to run when the script finishes loading, does your example provide that functionality?
@user1186050 The setTimeout was just an example of waiting for the googleApi script to load that would then call your defined callback method (I think you used initMap). Is initMap what you are referring to by "Google maps built in callback function".? Note that I haven't used maps before, but this is the way I have done it with other js methods running outside of Angular and calling into Angular.
Yes, initMap, is what I'm referring to as the callback. I'm looking into window.globalmethod to see if I can use it in Angular. FYI - Once Google maps gives me the callback I have to enable places autocomplete on a HTML input tag, not a map. Furthermore, this autocomplete is in a custom directive I've created. So I'll need to do a little testing here.
@user1186050 ok, so I assume that you would need to rename window.globalmethod to window.initMap instead for it to work. globalmethod was just an example name I used. As I said i'm not really that familiar with maps to provide a 100% complete example right now. If you don't get it working, then I will try later in the week to get a better example (no promises).
no, I don't believe that is how it works, but I could be wrong. InitMap(), from other examples I see, has to be located within a <script> tag in index.html. Once the callback to initmap is made, inside that method I can then call window.globalmethod() to instantiate some other Angular service, component, directive, etc.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.