I had an opportunity to develop a travel reservation feature using Goole Maps auto-complete suggestion (GCP MAPs API) with Nuxt.js & TypeScript.
This post might help someone do the same development 🙂
Demo.
Here is the step to make it.
- Activate GCP Maps API
- Add the api key to
nuxt-config.js
- Install
load-google-maps-api
- Implement the Map component
Activate GCP Maps API
You need to activate the GCP to get the api key.
- Maps JavaScript API
- Places API
You can get the api key by activating those in Google Cloud Platform.
Please be careful about the access control on the key. (IP, host restriction)
Add the api key to nuxt-config.js
Please add the api key to nuxt-config.js
as an environment variable.
module.exports = {
...
env: {
gmapKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
},
...
}
Install load-google-maps-api
$ yarn add load-google-maps-api
or
$ npm i -S load-google-maps-api
If you are with TypeScript, please install @types/load-google-maps-api
.
$ yarn add @types/load-google-maps-api
or
$ npm i -S @types/load-google-maps-api
Implement Map component
├── components
│ └── Map.vue
└── index.vue
index.vue
Simply index.vue
just imports Map component.
The @onChangeLocation
receives the location info from the Map component.
<template>
<div class="container">
<Map @onChangeLocation="updateLocation" />
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import Map from '~/components/Map.vue'
type location = {
name: string
address: string
coordinate: string
}
@Component({
name: 'Index',
components: {
Map
}
})
export default class Index extends Vue {
private updateLocation(props: location) {
console.log(props)
}
}
</script>
<style>
/* Sample `apply` at-rules with Tailwind CSS
.container {
@apply min-h-screen flex justify-center items-center text-center mx-auto;
}
*/
.container {
margin: 0 auto;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
</style>
Map.vue
It is optional to setting the default location information.
<template>
<section class="map w-full max-w-xl">
<div class="flex flex-wrap">
<div class="w-full">
<label
class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
for="grid-password"
>
Map search (Auto-complete)
</label>
<input
ref="input"
v-model="locationName"
class="appearance-none block w-full bg-gray-200 text-gray-700 border border-gray-200 rounded py-3 px-4 mb-3 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
type="text"
placeholder="Serch word..."
/>
</div>
</div>
<div ref="map" class="map-main" />
<div ref="infoContent" class="map-info">
<img :src="placeIcon" class="map-info__icon" />
<span class="map-info__title">{{ placeName }}</span>
<br />
<span>{{ placeAddress }}</span>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import loadGoogleMapsApi from 'load-google-maps-api'
type coord = {
lat: number
lng: number
}
@Component({
name: 'Map'
})
export default class Map extends Vue {
// Component variables
private locationName: string = ''
private locationAddress: string = ''
private locationCoordinate!: coord
private placeIcon: string = ''
private placeName: string = ''
private placeAddress: string = ''
// Goole map variables
private gmap: any = {}
private map: any
private mapService: any
private mapAutoComplete: any
private mapInfoWindow: any
private mapMarker: any
private defaultZoom: number = 14.0
private coord: coord = {
lat: 32.80093,
lng: 130.70064
}
mounted() {
this.initMap()
}
private async initMap() {
this.gmap = await loadGoogleMapsApi({
key: process.env.gmapKey,
libraries: ['places'],
language: 'en'
})
// Initialize
this.mapAutoComplete = new this.gmap.places.Autocomplete(this.$refs.input)
this.map = new this.gmap.Map(this.$refs.map, {
center: new this.gmap.LatLng(this.coord.lat, this.coord.lng),
zoom: this.defaultZoom,
mapTypeControl: false,
streetViewControl: false
})
this.mapInfoWindow = new this.gmap.InfoWindow()
this.mapMarker = new this.gmap.Marker({
map: this.map
})
// For auto-complete
this.initAutoComplete()
// For getting map info in clicking map
this.mapService = new this.gmap.places.PlacesService(this.map)
this.map.addListener('click', this.handleClickPOI.bind(this))
}
private initAutoComplete() {
this.mapAutoComplete.bindTo('bounds', this.map)
this.mapAutoComplete.setFields([
'geometry',
'icon',
'name',
'formatted_address'
])
this.mapInfoWindow.setContent(this.$refs.infoContent)
this.mapAutoComplete.addListener('place_changed', () => {
this.onClickLocation()
})
}
private onClickLocation() {
this.mapInfoWindow.close()
this.mapMarker.setVisible(false)
const place = this.mapAutoComplete.getPlace()
if (place.geometry.viewport) {
this.map.fitBounds(place.geometry.viewport)
} else {
this.map.setCenter(place.geometry.location)
}
this.mapMarker.setPosition(place.geometry.location)
this.mapMarker.setVisible(true)
// Display info window
this.placeIcon = place.icon
this.placeName = place.name
this.placeAddress = place.formatted_address
this.mapInfoWindow.open(this.map, this.mapMarker)
this.locationName = place.name
this.locationAddress = place.formatted_address
this.locationCoordinate = {
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng()
}
this.updateLocation()
}
private handleClickPOI(e: any) {
const self = this
if (e.placeId) {
this.mapService.getDetails({ placeId: e.placeId }, (
place: any,
status: string
) => {
if (status === 'OK') {
self.locationName = place.name
self.locationAddress = place.formatted_address
self.locationCoordinate = {
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng()
}
self.updateLocation()
}
})
}
}
private updateLocation() {
this.$emit('onChangeLocation', {
name: this.locationName,
address: this.locationAddress,
coordinate: this.locationCoordinate
})
}
}
</script>
<style lang="scss" scoped>
.map {
width: 100%;
&-main {
width: 100%;
height: 400px;
}
&-info {
&__icon {
display: inline-block;
width: 16px;
height: 16px;
}
&__title {
font-weight: bold;
}
}
}
</style>
load-google-map-api のimport
import loadGoogleMapsApi from 'load-google-maps-api'
Create Google Map instance
We need the Google Map instance to create instances mentioned in the next section.
The Google Map instance is initialized in mounted()
.
It cannot be initialized in created()
because the created()
is fired in SSR which caused an error. It is because the Google Map api refers to the window
object in the browser.
Please not the created()
is called in both in the server and the client, while the mounted()
is called only in CSR.
this.gmap = await loadGoogleMapsApi({
key: process.env.gmapKey,
libraries: ['places'],
language: 'en'
})
The language
property is useful for i18n integration.
If the this.gmap
instance is inilialized in the Nux plugin feature, the property cannot be changed dynamically.
Create instances of Google Map
Ceate instances in initMap()
.
- AutoComplete (auto-complete form)
- Map (displaying the map)
- InfoWindow (display facility information in tooltip)
- Marker (markers in the map)
- PlaceService (searching facilities)
AutoComplete(auto-complete form)
this.mapAutoComplete = new this.gmap.places.Autocomplete(this.$refs.input)
Map (displaying the map)
this.map = new this.gmap.Map(this.$refs.map, {
center: new this.gmap.LatLng(this.coord.lat, this.coord.lng),
zoom: this.defaultZoom,
mapTypeControl: false,
streetViewControl: false
})
center: specify the center coordination in the map
zoom: zoom level
mapTypeControl: whether showing the map controller or not
streetViewControl: whether showing the street view controller or not
InfoWindow (display facility information in tooltip)
This is an instance showing a tooltip with the selected facility information. It is dynamically updated when a searched item is selected.
this.mapInfoWindow = new this.gmap.InfoWindow()
Marker (map marker)
Iterally it is a instance shoing a marker on the map.
this.mapMarker = new this.gmap.Marker({
map: this.map
})
PlaceService (fetching facility info)
The PlaceServce
instance is used when a facility on the map is clicked for fetching the information (getDetails()
).
The getDetails()
is called in an event listener.
this.mapService = new this.gmap.places.PlacesService(this.map)
this.map.addListener('click', this.handleClickPOI.bind(this))
this.map.addListener('click', this.handleClickPOI.bind(this))
private handleClickPOI(e: any) {
const self = this
if (e.placeId) {
this.mapService.getDetails({ placeId: e.placeId }, (
place: any,
status: string
) => {
if (status === 'OK') {
self.locationName = place.name
self.locationAddress = place.formatted_address
self.locationCoordinate = {
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng()
}
self.updateLocation()
}
})
}
}
this.mapService.getDetails()
returns data like this.
e.g.
{
address: "1802-2 Oyatsu, 益城町 上益城郡 Kumamoto 861-2204, Japan"
coordinate: {lat: 32.8369716, lng: 130.8628225}
name: "Kumamoto Airport"
}
Initialize auto-complete
private initAutoComplete() {
this.mapAutoComplete.bindTo('bounds', this.map)
this.mapAutoComplete.setFields([
'geometry',
'icon',
'name',
'formatted_address'
])
this.mapAutoComplete.addListener('place_changed', () => {
this.onClickLocation()
})
this.mapInfoWindow.setContent(this.$refs.infoContent)
}
this.mapAutoComplete.bindTo('bounds', this.map)
This is used if you want to narrow down the searching area.
this.mapAutoComplete.setFields()
Specify the items you want the API to return.
This time has these three: geometory
, icon
, name
, and formatted_address
.
this.mapAutoComplete.addListener()
Register a callback function to the event listener. The function is called when an item in the suggested list is selected.
When the lister detectsplace_changed
event, the location information is shown in the map using InfoWindow and Marker instances.
private onClickLocation() {
this.mapInfoWindow.close()
this.mapMarker.setVisible(false)
const place = this.mapAutoComplete.getPlace()
if (place.geometry.viewport) {
this.map.fitBounds(place.geometry.viewport)
} else {
this.map.setCenter(place.geometry.location)
}
this.mapMarker.setPosition(place.geometry.location)
this.mapMarker.setVisible(true)
// Display info window
this.placeIcon = place.icon
this.placeName = place.name
this.placeAddress = place.formatted_address
this.mapInfoWindow.open(this.map, this.mapMarker)
this.locationName = place.name
this.locationAddress = place.formatted_address
this.locationCoordinate = {
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng()
}
this.updateLocation()
}