Create a simple Vue component to use Google Maps
Tags: vue, googemaps, maps
Published: 2020-12-29 01:21:45

There are some libraries over there that we may install in our projects to use Google Maps, but considering the amount of KB's to add and that is another dependence that sometimes can be deprecated some day, I got in the position to create my own component, also I didn't going to use too many features.

The code I'm going to share can be useful to use the basics of Google Maps, or even to start your own component and give it more features that can be useful for your projects.

## Obtaining the API Key

To use Maps JavaScript API, you'll need an API Key, which is a unique identifier used to authenticate the requests associated with your project for payment accounting purposes.
To get an API key, do the following:

Go to the GCP Console: https://cloud.google.com/console/google/maps-apis/overview

Use the drop-down menu to select or create the project that you want a new API Key for
Click the button to open the sidebar and go to _API & Services > Credentials_

On the _Credentials_ page, click on _Create Credentials > API key_. The API Key Created dialog will show the new API Key.

You can read more about the Google Maps API Key in the documentation: https://developers.google.com/maps/documentation/javascript/get-api-key

Now create an environment file at the root of the project, called `.env`, the content must be like

```
VUE_APP_MAPS_API_KEY="API_KEY"
```

## Initializing the library

You can initialize the library with a function that returns a promise, which is resolved once the Google Maps library is downloaded in the browser and initialized.

[Here is a basic example](https://developers.google.com/maps/documentation/javascript/adding-a-google-map) of how to create a map and add markers.

To actively use the Google Maps library in your Vue project, create a new script, `/src/utils/gmaps.js`.

```javascript
const API_KEY = process.env.VUE_APP_MAPS_API_KEY
const CALLBACK_NAME = 'initMap'

let initialized = !!window.google
let resolveInitPromise
let rejectInitPromise

// This promise handles the initialization
// status of the google maps script
const initPromise = new Promise((resolve, reject) => {
  resolveInitPromise = resolve
  rejectInitPromise = reject
})

export default function init() {
  // if google maps is already init
  // the `initPromise` should be resolved
  if (initialized) return initPromise

  initialized = true
  // the callback function is called
  // by the Google maps script if it is
  // successfully loaded
  window[CALLBACK_NAME] = () => resolveInitPromise(window.google)

  // we inject a new tag into the Head
  // of the HTML to load the Google Maps script
  const script = document.createElement('script')
  script.async = true
  script.defer = true
  script.src = `https://maps.googleapis.com/maps/api/js?key=${API_KEY}&callback=${CALLBACK_NAME}`
  script.onerror = rejectInitPromise
  document.querySelector('head').appendChild(script)

  return initPromise
}

```

## Creating a component to display the map

I'm gonna call the component `GoogleMaps` and for so create the component at `/src/components/GoogleMaps.vue`.

Let's add the markup at the template and the only CSS that's really mandatory

```vue
<template>
  <div class="map" ref="map"></div>
</template>

<script></script>

<style scoped>
.map {
  width: 100%;
  height: 400px;
}
</style>
```

That's all the required code at the markup, we may add more in dependence of the layout of our application.

The `mounted` lifecycle hook is asynchronous, and it waits for the Google Maps library to be initialized so that it can proceed to render the map using the `drawMap` method, adding a marker to represent the user's location on the map.

```vue
<script>
import gmapsInit from '@/utils/gmaps'
import { isNumber, isArray } from 'util'

export default {
  name: 'GoogleMaps',

  data() {
    return {
      google: null,
      map: null,
      innerMarkers: [],
      userMarker: null
    }
  },
  
  async mounted() {
    try {
      // init and wait for the Google script is mounted
      this.google = await gmapsInit()

      // if the location is already set, for example
      // when returning back to this view from another one
      if ('lat' in this.myLocation && this.myLocation.lat) {
        this.drawMap()
        // set the current location
        this.addMarker(this.myLocation)
      }
    } catch (err) {
      console.log('ERROR:', err)
    }
  },

  computed: {
    myLocation() {
      // if the coordinates is not set
      if (!('lat' in this.center) && !isNumber(this.center.lat)) {
        return null
      }
      // return the object expected by Google Maps
      return { lat: this.center.lat, lng: this.center.lng }
    }
  },

  methods: {
    drawMap() {
      if (this.myLocation.lat && this.myLocation.lng) {
        // creating the map object, displaying it in the $el DOM object
        this.map = new this.google.maps.Map(this.$refs['map'], {
          zoom: 18,
          center: this.myLocation
        })

        // center the canvas of the map to the location of the user
        this.map.setCenter(this.myLocation)
      }
    },

  }
}
</script>
```

We need to pass some data to the component, and I'm going to set as required `prop` for the component is `center`, which contains the coordinates for the center of the map and can receive a collection of markers via a prop with the same name.

```vue
<script>
  props: {
    center: {
      type: Object,
      required: true
    },
    markers: {
      type: Array
    }
  },
</script>
```

Then the last thing is to add reactivity in front of the change of the position of the current location of the user, for so is also required to add some methods to work with the markers.

```vue
<script>
  methods: {
        // add a marker with a blue dot to indicate the user location
    setUserMarker(location) {
      this.userMarker = new this.google.maps.Marker({
        position: location,
        map: this.map
      })
    },

    // Adds a marker to the map and push to the array
    addMarker(location) {
      // the marker positioned at `myLocation`
      const marker = new this.google.maps.Marker({
        position: location,
        map: this.map
      })
      this.innerMarkers.push(marker)
    },

    // Sets the map on all markers in the array
    setAllMarkersInMap(map) {
      for (let i = 0; i < this.innerMarkers.length; i++) {
        this.innerMarkers[i].setMap(map)
      }
    },

    // Removes the markers from the map, but keeps them in the array
    clearMarkers() {
      this.setAllMarkersInMap(null)
    },

    // Deletes all markers in the array by removing references to them
    deleteMarkers() {
      this.clearMarkers()
      this.innerMarkers = []
    }
  },

  watch: {
    marker: function(newVal) {
      if (isArray(newVal)) {
        // clear the markers
        this.clearMarkers()

        for (let i = 0; i < newVal.length; i++) {
          let position = newVal[i]
          if (
            'lat' in position &&
            isNumber(position.lat) &&
            isNumber(position.lng)
          ) {
            // set the current location
            this.addMarker(position)
          }
        }
      }
    }
  }
</script>
```

It is quite easy and now you can render maps in your application with a marker, with something like

```vue
<template>
  <GoogleMaps :center="{ ...coordinates }" />
<template>
```

Placing all the code for the `GoogleMap` component together

```vue
<template>
  <div class="map" ref="map"></div>
</template>

<script>
import gmapsInit from '@/utils/gmaps'
import { isNumber, isArray } from 'util'

export default {
  name: 'GoogleMaps',
  props: {
    center: {
      type: Object,
      required: true
    },
    markers: {
      type: Array
    }
  },
  async mounted() {
    try {
      // init and wait for the Google script is mounted
      this.google = await gmapsInit()

      // if the location is already set, for example
      // when returning back to this view from another one
      if ('lat' in this.myLocation && this.myLocation.lat) {
        this.drawMap()
        // set the current location
        this.addMarker(this.myLocation)
      }
    } catch (err) {
      console.log('ERROR:', err)
    }
  },
  data() {
    return {
      google: null,
      map: null,
      innerMarkers: [],
      userMarker: null
    }
  },
  computed: {
    myLocation() {
      // if the coordinates is not set
      if (!('lat' in this.center) && !isNumber(this.center.lat)) {
        return null
      }
      // return the object expected by Google Maps
      return { lat: this.center.lat, lng: this.center.lng }
    }
  },
  methods: {
    drawMap() {
      if (this.myLocation.lat && this.myLocation.lng) {
        // creating the map object, displaying it in the $el DOM object
        this.map = new this.google.maps.Map(this.$refs['map'], {
          zoom: 18,
          center: this.myLocation
        })

        // center the canvas of the map to the location of the user
        this.map.setCenter(this.myLocation)
      }
    },
    // add a marker with a blue dot to indicate the user location
    setUserMarker(location) {
      this.userMarker = new this.google.maps.Marker({
        position: location,
        map: this.map
      })
    },
    // Adds a marker to the map and push to the array
    addMarker(location) {
      // the marker positioned at `myLocation`
      const marker = new this.google.maps.Marker({
        position: location,
        map: this.map
      })
      this.innerMarkers.push(marker)
    },
    // Sets the map on all markers in the array
    setAllMarkersInMap(map) {
      for (let i = 0; i < this.innerMarkers.length; i++) {
        this.innerMarkers[i].setMap(map)
      }
    },
    // Removes the markers from the map, but keeps them in the array
    clearMarkers() {
      this.setAllMarkersInMap(null)
    },
    // Deletes all markers in the array by removing references to them
    deleteMarkers() {
      this.clearMarkers()
      this.innerMarkers = []
    }
  },
  watch: {
    marker: function(newVal) {
      if (isArray(newVal)) {
        // clear the markers
        this.clearMarkers()

        for (let i = 0; i < newVal.length; i++) {
          let position = newVal[i]
          if (
            'lat' in position &&
            isNumber(position.lat) &&
            isNumber(position.lng)
          ) {
            // set the current location
            this.addMarker(position)
          }
        }
      }
    }
  }
}
</script>

<style scoped>
.map {
  width: 100%;
  height: 400px;
}
</style>
```

Hoping it can be helpful for your project!