Brian Love
Angular + TypeScript Developer in Denver, CO

Angular 2 + Google Maps Places Autocomplete

Reading time ~5 minutes

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 angular2-google-map 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 angular2-google-maps module as well as the googlemaps TypeScript declaration file using Node Package Manager (npm):

$ npm install angular2-google-maps --save
$ npm install @types/googlemaps --save-dev

Next, modify your @NgModule decorator:

import { AgmCoreModule } from "angular2-google-maps/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:

  • First, we import the AgmCoreModule class from the angular2-google-maps/core module.
  • In our @NgModule imports property we use the AgmCoreModule.forRoot() method and specify the places library.
  • You will notice that I am also using ReactiveFormsModule for Angular’s new model-driven forms.

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: [`
    .sebm-google-map-container {
      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>
      <sebm-google-map [latitude]="latitude" [longitude]="longitude" [scrollwheel]="false" [zoom]="zoom">
        <sebm-google-map-marker [latitude]="latitude" [longitude]="longitude"></sebm-google-map-marker>
      </sebm-google-map>
    </div>
  `
})
export class App implements OnInit {}

A couple of things to note:

  • Don’t forget that you must define a height for the map element. In this case, I am setting it to 300px.
  • For the template, I am using some minimal Bootstrap CSS styling to make things look nice.
  • My input element uses a local template variable named search. This is declared using the hash symbol ( # ), or I could have used the ref- prefix such as ref-search.
  • My input element has one-way data binding for the FormControl directive to the searchControl publicly available variable in my controller. We’ll look at this in a moment.
  • Next, I am implementing the sebm-google-map directive as per the documentation for the angular2-google-maps module. I am binding the latitude to a public variable in my controller named latitude, and I am doing the same thing for the longitude. I am also setting the scrollwheel option to false. Then I bind the zoom value to my zoom property in my controller.
  • I have also included a marker that will use the same longitude and latitude as the center point for the map.

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 { MapsAPILoader } from 'angular2-google-maps/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.

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:

Brian Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Denver and I ski (a lot).