Location Select in Ionic

Location Select Page with Google Maps and Ionic



·

If you want to accept a user supplied location in your application, and you want it to be in any kind of remotely useful format, it could be quite hard to do. You could of course just have a simple text field and allow the user to type the address, but that’s a lot of work for the user and would lead to very inconsistent data – you could type the same address in many different ways.

If you wanted to display that address on a map, it would be difficult to do by going off of a user supplied address. It would be much easier if you had the latitude and longitude of that address, but I suspect there are very few people on the planet that know that Uluru is at -25.344428, 131.036882 off the top of their head.

The Google Places API can do a great deal to help us solve this problem. It gives us the best of both worlds in that we can allow users to input an address easily, but we can also store the location in a more developer friendly format.

In this tutorial, we are going to walk through how to build a location selection page in Ionic using the Google Places API. The user will be able to start typing an address and as they do the Google Places API will suggest a list of possible places. Upon selecting a particular place, we will set that place on a Google Map, and we will also be given the latitude, longitude, and name for the place that was selected.

Once we are finished, it will look something like this:

Location Select in Ionic

Before We Get Started

Last updated for Ionic 3.1.0

Before you go through this tutorial, you should have at least a basic understanding of Ionic concepts. You must also already have Ionic set up on your machine.

If you’re not familiar with Ionic already, I’d recommend reading my Ionic Beginners Guide or watching my beginners series first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic, then take a look at Building Mobile Apps with Ionic.

1. Generate a New Ionic Application

To begin, we are going to generate a new Ionic application with the following command:

ionic start ionic-location-select blank

Once that has finished generating, you should make it your working directory by running the following command:

cd ionic-location-select

We are going to create our LocationSelect page as an independent modal that can be popped up in your application wherever you require it. When the modal is dismissed it will pass back the location information to whatever page called it.

Run the following command to generate the LocationSelect page:

ionic g page LocationSelect

We are also going to create a couple of providers to help us with the functionality for this application, so let’s create those as well:

Run the following command to generate the required providers:

ionic g provider ConnectivityService
ionic g provider GoogleMaps

We will implement these in just a moment. We are also going to need to install a couple of Cordova plugins, and we will be using the Ionic Native packages to help use those.

Run the following commands to install the Network and Geolocation plugins:

ionic plugin add --save cordova-plugin-geolocation
npm install --save @ionic-native/geolocation
ionic plugin add --save cordova-plugin-network-information
npm install --save @ionic-native/network

In order for the Google Maps functionality to work well with TypeScript, you will need to run the following command:

npm install @types/google-maps --save

If you do not install the types for Google Maps, you will get errors that complain about not knowing what google is. Finally, we are going to need to set up these providers and plugins in our app.module.ts file.

Modify src/app/app.module.ts to reflect the following:

import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { LocationSelect } from '../pages/location-select/location-select';
import { Connectivity } from '../providers/connectivity-service';
import { GoogleMaps } from '../providers/google-maps';
import { Network } from '@ionic-native/network';
import { Geolocation } from '@ionic-native/geolocation';

@NgModule({
  declarations: [
    MyApp,
    HomePage,
    LocationSelect
  ],
  imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage,
    LocationSelect
  ],
  providers: [
    StatusBar,
    SplashScreen,
    Connectivity,
    GoogleMaps,
    Network,
    Geolocation,
    {provide: ErrorHandler, useClass: IonicErrorHandler}
  ]
})
export class AppModule {}

2. Implement the Dependencies

There’s quite a bit of setup required for this tutorial that is related to Google Maps, but it is all stuff I have covered previously. We will be loading in Google Maps dynamically so that in the case that the user is not currently online when launching the application, or that they go offline when using the application, the app will continue to function as expected.

If you would like to read in more detail about exactly what is happening with the following steps I would recommend reading Creating an Advanced Google Map Component in Ionic. We will mostly just be copying and pasting for these initial steps.

Modify src/providers/connectivity-service.ts to reflect the following:

import { Injectable } from '@angular/core';
import { Network } from '@ionic-native/network';
import { Platform } from 'ionic-angular';
import { Observable } from 'rxjs/Observable';

declare var Connection;

@Injectable()
export class Connectivity {

  onDevice: boolean;

  constructor(public platform: Platform, public network: Network){
    this.onDevice = this.platform.is('cordova');
  }

  isOnline(): boolean {
    if(this.onDevice && this.network.type){
      return this.network.type != 'none';
    } else {
      return navigator.onLine; 
    }
  }

  isOffline(): boolean {
    if(this.onDevice && this.network.type){
      return this.network.type == 'none';
    } else {
      return !navigator.onLine;   
    }
  }

  watchOnline(): any {
    return this.network.onConnect();
  }

  watchOffline(): any {
    return this.network.onDisconnect();
  }

}

Modify src/providers/google-maps.ts to reflect the following:

import { Injectable } from '@angular/core';
import { Platform } from 'ionic-angular';
import { Connectivity } from './connectivity-service';
import { Geolocation } from '@ionic-native/geolocation';

@Injectable()
export class GoogleMaps {

  mapElement: any;
  pleaseConnect: any;
  map: any;
  mapInitialised: boolean = false;
  mapLoaded: any;
  mapLoadedObserver: any;
  currentMarker: any;
  apiKey: string = "YOUR_API_KEY";

  constructor(public connectivityService: Connectivity, public geolocation: Geolocation) {

  }

  init(mapElement: any, pleaseConnect: any): Promise<any> {

    this.mapElement = mapElement;
    this.pleaseConnect = pleaseConnect;

    return this.loadGoogleMaps();

  }

  loadGoogleMaps(): Promise<any> {

    return new Promise((resolve) => {

      if(typeof google == "undefined" || typeof google.maps == "undefined"){

        console.log("Google maps JavaScript needs to be loaded.");
        this.disableMap();

        if(this.connectivityService.isOnline()){

          window['mapInit'] = () => {

            this.initMap().then(() => {
              resolve(true);
            });

            this.enableMap();
          }

          let script = document.createElement("script");
          script.id = "googleMaps";

          if(this.apiKey){
            script.src = 'http://maps.google.com/maps/api/js?key=' + this.apiKey + '&callback=mapInit&libraries=places';
          } else {
            script.src = 'http://maps.google.com/maps/api/js?callback=mapInit';       
          }

          document.body.appendChild(script);  

        } 
      } else {

        if(this.connectivityService.isOnline()){
          this.initMap();
          this.enableMap();
        }
        else {
          this.disableMap();
        }

        resolve(true);

      }

      this.addConnectivityListeners();

    });

  }

  initMap(): Promise<any> {

    this.mapInitialised = true;

    return new Promise((resolve) => {

      this.geolocation.getCurrentPosition().then((position) => {

        let latLng = new google.maps.LatLng(position.coords.latitude, position.coords.longitude);

        let mapOptions = {
          center: latLng,
          zoom: 15,
          mapTypeId: google.maps.MapTypeId.ROADMAP
        }

        this.map = new google.maps.Map(this.mapElement, mapOptions);
        resolve(true);

      });

    });

  }

  disableMap(): void {

    if(this.pleaseConnect){
      this.pleaseConnect.style.display = "block";
    }

  }

  enableMap(): void {

    if(this.pleaseConnect){
      this.pleaseConnect.style.display = "none";
    }

  }

  addConnectivityListeners(): void {

    this.connectivityService.watchOnline().subscribe(() => {

      setTimeout(() => {

        if(typeof google == "undefined" || typeof google.maps == "undefined"){
          this.loadGoogleMaps();
        } 
        else {
          if(!this.mapInitialised){
            this.initMap();
          }

          this.enableMap();
        }

      }, 2000);

    });

    this.connectivityService.watchOffline().subscribe(() => {

      this.disableMap();

    });

  }

}

IMPORTANT: In order for Google Maps to work, you will need to overwrite:

apiKey: string = "YOUR_API_KEY";

with your own API key. If you do not have one or do not know how to create one, you should read this.

Although the code above is mostly unchanged from the tutorial I link, there is one important change and that is:

script.src = 'http://maps.google.com/maps/api/js?key=' + this.apiKey + '&callback=mapInit&libraries=places';

Notice that at the end of this URL we include &libraries=places, this will include the Google Places API on top of the normal Google Maps API.

3. Implement the Location Select Page

Now that we have our dependencies set up, we can move on to creating the LocationSelect page itself. We will start off by implementing the template because I think it will help give context to the functionality we will add in the TypeScript file.

Modify src/pages/location-select/location-select.html to reflect the following:

<ion-header>
    <ion-navbar color="primary">
        <ion-buttons left>
            <button ion-button (click)="close()">Cancel</button>
        </ion-buttons>
        <ion-buttons right>
            <button [disabled]="saveDisabled" ion-button (click)="save()">Save</button>
        </ion-buttons>
    </ion-navbar>

    <ion-toolbar>
        <ion-searchbar [(ngModel)]="query" (ionInput)="searchPlace()"></ion-searchbar>
    </ion-toolbar>

    <ion-list>
        <ion-item *ngFor="let place of places" (touchstart)="selectPlace(place)">{{place.description}}</ion-item>
    </ion-list>

</ion-header>

<ion-content>

    <div #pleaseConnect id="please-connect">
        <p>Please connect to the Internet...</p>
    </div>

    <div #map id="map">
        <ion-spinner></ion-spinner>
    </div>

</ion-content>

In the <ion-content> section we have an element for where the map will be injected, and an element for the message that displays when no Internet connection is available. Details on how this works is available in the advanced Google Maps tutorial that I linked above.

The content area is just a normal map, the interesting stuff in terms of this tutorial happens in the header. As well as having some buttons to dismiss the modal, we also have an additional toolbar in the header area. This contains a search bar that will define the query that is used with the Google Places API to predict some locations. We then also have a list that will display the results of those predictions (and give the user the ability to select one of those).

A touchstart event binding is used for the places that will display in the list because if a click event is used it would require the user to tap twice to select a place (once to remove focus from the search bar, another to trigger a click on a particular place). The downside of this is that this will only work on devices, or through something like the Chrome DevTools mobile emulator (so that touch events are triggered rather than mouse events).

In order to get everything in the template displaying correctly, we will also need to add a few styles.

Modify src/pages/location-select/location-select.scss to reflect the following:

page-location-select {

  ion-list {
    margin: 0 !important;
  }

  #please-connect {
    position: absolute;
    background-color: #000;
    opacity: 0.5;
    width: 100%;
    height: 100%;
    z-index: 1;
  }

  #please-connect p {
      color: #fff;
      font-weight: bold;
      text-align: center;
      position: relative;
      font-size: 1.6em;
      top: 30%;
  }

  .scroll-content {
      overflow: hidden;
  }

  #map {
      width: 100%;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
  }

}

Now let’s move on to the interesting stuff. We are going to implement the Google Places functionality in the TypeScript file for the LocationSelect page.

Modify src/pages/location-select/location-select.ts to reflect the following:

import { NavController, Platform, ViewController } from 'ionic-angular';
import { Component, ElementRef, ViewChild, NgZone } from '@angular/core';
import { Geolocation } from '@ionic-native/geolocation';
import { GoogleMaps } from '../../providers/google-maps';

@Component({
  selector: 'page-location-select',
  templateUrl: 'location-select.html'
})
export class LocationSelect {

    @ViewChild('map') mapElement: ElementRef;
    @ViewChild('pleaseConnect') pleaseConnect: ElementRef;

    latitude: number;
    longitude: number;
    autocompleteService: any;
    placesService: any;
    query: string = '';
    places: any = [];
    searchDisabled: boolean;
    saveDisabled: boolean;
    location: any;  

    constructor(public navCtrl: NavController, public zone: NgZone, public maps: GoogleMaps, public platform: Platform, public geolocation: Geolocation, public viewCtrl: ViewController) {
        this.searchDisabled = true;
        this.saveDisabled = true;
    }

    ionViewDidLoad(): void {

        let mapLoaded = this.maps.init(this.mapElement.nativeElement, this.pleaseConnect.nativeElement).then(() => {

            this.autocompleteService = new google.maps.places.AutocompleteService();
            this.placesService = new google.maps.places.PlacesService(this.maps.map);
            this.searchDisabled = false;

        }); 

    }

    selectPlace(place){

        this.places = [];

        let location = {
            lat: null,
            lng: null,
            name: place.name
        };

        this.placesService.getDetails({placeId: place.place_id}, (details) => {

            this.zone.run(() => {

                location.name = details.name;
                location.lat = details.geometry.location.lat();
                location.lng = details.geometry.location.lng();
                this.saveDisabled = false;

                this.maps.map.setCenter({lat: location.lat, lng: location.lng}); 

                this.location = location;

            });

        });

    }

    searchPlace(){

        this.saveDisabled = true;

        if(this.query.length > 0 && !this.searchDisabled) {

            let config = {
                types: ['geocode'],
                input: this.query
            }

            this.autocompleteService.getPlacePredictions(config, (predictions, status) => {

                if(status == google.maps.places.PlacesServiceStatus.OK && predictions){

                    this.places = [];

                    predictions.forEach((prediction) => {
                        this.places.push(prediction);
                    });
                }

            });

        } else {
            this.places = [];
        }

    }

    save(){
        this.viewCtrl.dismiss(this.location);
    }

    close(){
        this.viewCtrl.dismiss();
    }   

}

There is a bit going on here, but if we step through it function by function it is all reasonably straightforward. In the ionViewDidLoad() function we initialise the Google Maps as usual (again, this is covered in the other Google Maps tutorial), but we also do something a little different. Once the map has initialised, we create new instances of an AutocompleteService and a PlacesService. Both of these come from the Google Maps Places API, and will allow us to get a list of predictions for a partial address and also grab details about a specific address (like the latitude and longitude).

The searchPlace function is what handles getting the predictions and populating the <ion-list> in the header of the template. All we do is call the getPlacePredictions method on the AutocompleteService and that will return us a bunch of predictions. We then push each of those predictions into the places array, which is what is used to populate the <ion-list> in the template.

The selectPlace function is triggered when the user taps on a prediction in the list. We then pass that prediction into the PlacesService to grab the details for that place. We set some of the information that is returned from that onto our location object, and we call the setCenter method on the map to change the position of the map to that location.

The save function simply hands passing back whatever place was selected to the page that launched the modal.

4. Launch the Location Select Page

Our LocationSelect page is implemented now, and that was the hard part, so now we just have to launch it from somewhere.

Modify src/pages/home/home.ts to reflect the following:

import { Component } from '@angular/core';
import { NavController, ModalController } from 'ionic-angular';
import { LocationSelect } from '../location-select/location-select';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

    constructor(public navCtrl: NavController, public modalCtrl: ModalController) {

    }

    launchLocationPage(){

        let modal = this.modalCtrl.create(LocationSelect);

        modal.onDidDismiss((location) => {
            console.log(location);
        });

        modal.present();    

    }

}

This is super simple, because all we need to do is create a new modal using the LocationSelect page and add an onDidDismiss handler. If the user selects a location, the information for that location will be passed back to this function, and then you can do whatever you want with it.

Now, all we need to do is trigger that function.

Modify src/pages/home/home.html to reflect the following:

<ion-header>
  <ion-navbar>
    <ion-title>
      Ionic Blank
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>

    <button ion-button (click)="launchLocationPage()">Launch</button>

</ion-content>

Summary

The Google Places API manages to turn an awkward and complex problem into a reasonably manageable one. I like this solution in particular because it allows you to easily drop this LocationSelect page anywhere you like in your application, and receive data back in a nice and useable format.

What to watch next...

  • Thanks for this tutorial, but am face with a problem, I did install “npm install @types/google-maps –save” but am still getting error that says: “Cannot find name ‘GoogleMaps’ “. What am I missing?

    • Tzvi Gregory Kaidanov

      Did you find an answer?

  • Leandro Garcia

    Good job, thanks.
    I have one question. I got this error: Error: Uncaught (in promise): TypeError: Cannot read property ‘AutocompleteService’ of undefined
    I believe that we need to inform an input at AutocompleteService() method.
    this.autocompleteService = new google.maps.places.AutocompleteService();

    • nawel

      have u solved this problem??

      • Leandro Garcia

        Yes, my fault. 🙁 Tks.

      • Uğur Öksüz

        Hi, how did you solve your problem?
        I’m getting the same error.

      • Leandro Garcia

        I call the Google API at my index.html.

      • Uğur Öksüz

        Ok, thanks. I fixed it.

      • Mark Dunn

        How did you fix it? Same issue here, Uncaught (in promise): TypeError: Cannot read property ‘AutocompleteService’ of undefined

      • Dinan Rangga M

        up

      • reinz
      • Stanley Masaku

        How did u call it I’m still having the issue

      • Leandro Garcia

        I had called the Google API and removed it. After I removed it worked. Pardon my English.

      • Stanley Masaku

        could you send me your code?

      • Leandro Garcia

        Nothing diferent from this post.

      • Stanley Masaku

        Were did you remove the call method in the index.html exactly

  • Luciel Costa

    This is giving error in part of the code:

    Typescript Error
    Cannot find name ‘google’.

    How does it solve?

  • Ashu Patil

    I got this error: Error: Uncaught (in promise): No provider for Http!injection Error
    How to solve it???

  • Ashu Patil

    I solved this error by just importing “import { HttpModule }from ‘@angular/http’;”
    in app.module.ts

  • sylvain Rojas

    Hi there,
    Thanks for this nice tutorials. Unfortunately, I have a spinner running and “Please Connect to the internet..” message. I’ve run $ ionic cordova platform add browser and $ionic cordova run browser.
    What did I missed? Thanks for your help.

  • Eduardo Alberto Marcó
  • Eduardo Alberto Marcó

    I have a problem with init:
    init(mapElement: any, pleaseConnect: any):

  • Balaji Gopal

    Hi ,
    I am getting the below error:
    Cannot read property ‘getDetails’ of null
    How do we solve this? Instead of place id I tried passing the reference also but still?

    • Dinan Rangga M

      up

  • Dinan Rangga M
  • Dinan Rangga M

    “mapLoaded is declared but never used”

  • Ivan More Flores
    • reinz

      this is because failed to get lat & long..