Map based event managed with MEVN stack and Vuetify - Part 2

This is the second part in a series about building a map-based event manager with MEVN stack and Vuetify. You can find the rest of the series here

What we’re doing

  • Google Maps integration using vue2-google-maps
  • Using Vuetify to set the map position and size
  • Adding reactive map markers showing the events
  • Setting custom marker icons
  • Panning the map smoothly when clicking marker

Adding the map component

First make sure you have a google maps API key. If you don’t, get one here

Adding vue2-google-maps

We’re going to use the vue2-google-maps library to access the Maps API. This will give us reactive 2-way bindings for the events on the map.

$ yarn add vue2-google-maps
$ yarn add @babel/runtime-corejs2

you need to add the @babel/runtime-corejs2 package because of an issue with the vue2-google-maps library

in main.js:

import * as VueGoogleMaps from 'vue2-google-maps';

Vue.use(VueGoogleMaps, {
  load: {
    key: 'your api key',
    libraries: 'places'
  }
});

We’ll use the places library to get the place name as a default for the event title.

Creating the Map component

Create a new file: Map.vue, in your components dir. This will encapsulate all the map stuff.

Paste the following into the new file:

<template>
  <div>
    <gmap-map
      ref="mapRef"
      style="height: 100%"
      :center="location"
      :zoom="14"
    >
      <gmap-marker
        v-for="(event, index) in events"
        :key="'marker'+index"
        :position="event.position"
        :icon="require('../assets/event_marker.png')"
        @click="panTo(event.position)"
      ></gmap-marker>
      <gmap-circle
        v-for="(event, index) in events"
        :key="'circle'+index"
        :center="event.position"
        :options="{
            strokeColor: '#000',
            strokeOpacity: 0,
            strokeWeight: 0,
            fillColor: '#ff6ada',
            fillOpacity: 0.25,
            radius: 500
            }"
        @click="panTo(event.position)"
      ></gmap-circle>
    </gmap-map>
  </div>
</template>

<script>
export default {
  name: "Map",
  data() {
    return {
      map: null,
      events: [
        {
          position: {
            lng: 34.77,
            lat: 32.09
          }
        },
        {
          position: {
            lng: 34.78,
            lat: 32.08
          }
        }
      ],
      location: {
        lng: 34.77,
        lat: 32.09
      }
    };
  },
  mounted() {
    this.$refs.mapRef.$mapPromise.then(map => {
      this.map = map;
    });
  },
  methods: {
    panTo(position) {
      if (!this.map) return;

      this.map.panTo(position);
    }
  }
};
</script>


<style scoped="true">
</style>

Let’s unpack this

<gmap-map
    ref="mapRef"
    style="height: 100%"
    :center="location"
    :zoom="14"
>

The main map component, with center coordinates and initial zoom as props.

<gmap-marker
    v-for="(event, index) in events"
    :key="'marker'+index"
    :position="event.position"
    :icon="require('../assets/event_marker.png')"
    @click="panTo(event.position)"
    ></gmap-marker>

We’re looping over the events data field (initialized statically for now, later from mongo), and creating a <gmap-marker> with the event’s position. We’re injecting it with a custom icon from the assets dir, and a click handler which pans to the marker’s position.

<gmap-circle
    v-for="(event, index) in events"
    :key="'circle'+index"
    :center="event.position"
    :options="{
        strokeColor: '#000',
        strokeOpacity: 0,
        strokeWeight: 0,
        fillColor: '#ff6ada',
        fillOpacity: 0.25,
        radius: 500
        }"
    @click="panTo(event.position)"
    ></gmap-circle>

The same but with nice pink circles :) Notice the usage of :options prop for passing custom circle properties.

Because we’re using vue directives to render the markers and circles, they are reactive and changes in the data will reflect on the map.

mounted() {
    this.$refs.mapRef.$mapPromise.then(map => {
        this.map = map;
    });
},

This is how we wait for the map to be initialized before we can use methods like panTo.

panTo(position) {
    if (!this.map) return;

    this.map.panTo(position);
}

This will pan the map smoothly to the marker’s position, instead of centering abruptly. The condition deals with the edge case of clicking a marker before we can use pan.

Positioning the map using Vuetify layout

Now let’s add our new Map component to the main dashboard layout. In Dashboard.vue, replace the contents of <v-content> with the following:

<v-content>
    <v-container fluid fill-height pa-0>
    <v-layout
        justify-center
        align-center
    >
        <v-flex fill-height d-flex xs12>
            <Map></Map>
        </v-flex>
    </v-layout>
    </v-container>
</v-content>

Vuetify’s layout system is built around the hierarchy of: v-container -> v-layout -> v-flex, with <v-layout> functioning as rows, and <v-flex> as colums. In our case we want to position only one item - our map component - in the center of the container, so we use justify-center and align-center. Because Vuetify works with a 12 column system, the xs12 sets the width of the cell to fill the <v-layout> in all screen sizes (extra small and up). The fill-height attribute sets the height to fill the container. To get rid of the padding, we use pa-0 (padding-all-0)

Finally import the Map component:

import Map from './Map';

export default {
    components: {
        Map,
    },
    ...
}

And there you have it, a map component centered in the main container showing custom reactive markers.

The code for this part is available here.


Jonathan Perry
Written by@Jonathan Perry
Fullstack dev - I like making products fast

GitHubMediumTwitter