Picture of Brian Love wearing black against a dark wall in Portland, OR.

Brian Love

Angular 2 + Google Maps Places Autocomplete

In this brief tutorial I’ll show you to to quickly build an Angular 2 + Google Maps Places Autocomplete application.

The Places Autocomplete by Google Maps is very helpful as it allows a user to search for an address or specific location. I’ll combine the Places Autocomplete API with a Google Map using the Angular Google Maps (agm) module.

tl;dr

Check out the Plunker: https://plnkr.co/edit/LdKdSj?p=preview

Getting Started

The first thing to do is to install the Angulage Google Maps module as well as the Google Maps TypeScript declarations file using Node Package Manager (npm):

$ npm install @agm/core --save
$ npm install @types/googlemaps --save-dev

Next, modify your @NgModule decorator:

import { AgmCoreModule } from '@agm/core';

@NgModule({
  imports: [
    AgmCoreModule.forRoot({
      apiKey: "YOUR KEY GOES HERE",
      libraries: ["places"]
    }),
    BrowserModule,
    FormsModule,
    ReactiveFormsModule
  ],
  declarations: [ App ],
  bootstrap: [ App ]
})
export class AppModule() {}

To review the code above:

Create Template

For this tutorial I am going to set the styles and template properties in my @Component decorator configuration. I would suggest that you have an external Sass file and an external HTML template instead. Let’s take a look at the @Component configuration:

@Component({
  selector: 'my-app',
  styles: [`
    agm-map {
      height: 300px;
    }
  `],
  template: `
    <div class="container">
      <h1>Angular 2 + Google Maps Places Autocomplete</h1>
      <div class="form-group">
        <input placeholder="search for location" autocorrect="off" autocapitalize="off" spellcheck="off" type="text" class="form-control" #search [formControl]="searchControl">
      </div>
      <agm-map [latitude]="latitude" [longitude]="longitude" [scrollwheel]="false" [zoom]="zoom">
        <agm-marker [latitude]="latitude" [longitude]="longitude"></agm-marker>
      </agm-map>
    </div>
  `
})
export class App implements OnInit {}

A couple of things to note:

Implement Controller

Next, we need to implement our controller. Let’s take a look at it:

import { ElementRef, NgZone, OnInit, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { } from 'googlemaps';
import { MapsAPILoader } from '@agm/core';

export class App implements OnInit {

  public latitude: number;
  public longitude: number;
  public searchControl: FormControl;
  public zoom: number;

  @ViewChild("search")
  public searchElementRef: ElementRef;

  constructor(
    private mapsAPILoader: MapsAPILoader,
    private ngZone: NgZone
  ) {}

  ngOnInit() {
    //set google maps defaults
    this.zoom = 4;
    this.latitude = 39.8282;
    this.longitude = -98.5795;

    //create search FormControl
    this.searchControl = new FormControl();

    //set current position
    this.setCurrentPosition();

    //load Places Autocomplete
    this.mapsAPILoader.load().then(() => {
      let autocomplete = new google.maps.places.Autocomplete(this.searchElementRef.nativeElement, {
        types: ["address"]
      });
      autocomplete.addListener("place_changed", () => {
        this.ngZone.run(() => {
          //get the place result
          let place: google.maps.places.PlaceResult = autocomplete.getPlace();

          //verify result
          if (place.geometry === undefined || place.geometry === null) {
            return;
          }

          //set latitude, longitude and zoom
          this.latitude = place.geometry.location.lat();
          this.longitude = place.geometry.location.lng();
          this.zoom = 12;
        });
      });
    });
  }

  private setCurrentPosition() {
    if ("geolocation" in navigator) {
      navigator.geolocation.getCurrentPosition((position) => {
        this.latitude = position.coords.latitude;
        this.longitude = position.coords.longitude;
        this.zoom = 12;
      });
    }
  }
}

First, I import the necessary classes: NgZone, OnInit, ViewChild, FormControl and MapsAPILoader.

Inside the class we first define our public variables to store the latitude, longitude, zoom and our searchControl. Next, I am using the @ViewChild decorator to get access to the DOM input element. The @ViewChild decorator accepts a single string that is the selector to the element or directive. In this case I am referencing the local template variable #search. It decorates the variable searchElementRef, which is an ElementRef to the search input.

Next, I use dependency injection (DI) to inject the MapsAPILoader and NgZone dependencies. We’ll use the MapsAPILoader later on in the ngOnInit() method to load the Google Places API. The NgZone service enables us to perform asynchronous operations outside of the Angular zone, or in our case, to explicitly run a function within the Angular zone. Angular’s zones patch most of the asynchronous APIs in the browser, invoking change detection when an asynchronous code is completed. As you might expect Angular zones are not patching the asynchronous behavior of Google Place autocomplete.

Now, in the ngOnInit() method I set some initial values for the latitude, longitude and zoom. Then, I create a new FormControl() instance for the searchControl. Next I invoke the setCurrentPosition() method, which will simply attempt to use the geolocation API in the browser to set the map to the user’s current location. You can just ignore this if you want.

I then use the load() method in the MapsAPILoader to load the Google Places API. This returns a promise object, so when this has been resolved we can then fire up our Google Places Autocomplete. The code here should look familiar to the code sample available from Google.

Note: I am limiting the results for Google Places Autocomplete to addresses using the types option. If you want to show all results, simply remove this, or specify an alternative type.

Finally, we use the fat arrow function in TypeScript to attach an event handler to the place_changed event for our autocomplete. We then wrap our asynchronous code within the NgZone.run() method.

The documentation for NgZone indicates that:

Running functions via run allows you to reenter Angular zone from a task that was executed outside of the Angular zone

Within the run() method we will store the data returns from the Google Maps Places API, including updating our map’s latitude and longitude. Without the use of the NgZone.run() method the changes to our latitude, longitude and zoom will not be triggered until change detection is triggered by another event or asynchronous operation.

Demo

Here is a demo on Plunker: