September 21, 2018

Communicating Between Vue 2 Components and Google Maps

by Connor Leech

Communicating Between Vue 2 Components and Google Maps

Vue.js is a progressive JavaScript framework that inherits from the JavaScript framework legacy of the likes of Angular.js 1.x, Backbone.js and jQuery. React grew out of this ecosystem and they have similar aspects like the component architecture. Vue.js tends to be popular in Asia and is used by Alibaba in production at scale. React is used at Facebook and many startups as well as Walmart Labs. Both are great options for building advanced user interfaces; in this tutorial we’ll focus on Vue.js.

In this tutorial, we’ll create a front-end web page that shows a Google Map and asynchronously updates across different parts of the page. Asynchronously means that updates happen on the page without an additional page load that wipes the user’s screen. We store the front-end state of the application on the client-side and can update the database with an npm package such as axios.

To get started, the Vue core team maintains a command line interface (CLI) called vue-cli. Using vue-cli you can create starter front-end templates that have build systems configured. Configuring a custom build system for a front-end application can be a tedious process, a barrier to entry for beginners and not core to what you want to build. Relying on these starter templates can provide a huge head start in terms of initial development time, but can be problematic as development continues. Some starter templates do so much that you’ll spend considerable time trying to figure out how the template works instead of building your application!

One lightweight starter template we like to use for Vue.js front-end sites is maintained by the Vue.js core team and called webpack-simple. Get started and install it by running:

npm install -g vue-cli  
vue init webpack-simple my-project  
cd my-project  
npm install  
npm run dev  

Now we have a static website that we can build our application on top of, with .vue files! Webpack is already configured for us. You can write Vue without all this but we wouldn’t recommend it. This is easier. If you install a new Laravel app you will have a similar build setup. Our application does not require a database so we’ll stick to a static, front-end only site.

Setup

The next step in building our application is to incorporate Google Maps. You will need to obtain a Google Maps API key. Within your index.html file, add the script tag that points to the Google Maps API v3 JavaScript CDN:

<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_KEY_HERE"></script>  

We’ll use Bootstrap 4 for styling later. Drop this link tag to the bootstrap CDN in the head of the document:

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">

Let’s render the map in a Vue component. We’re going to want to send events from the map to other parts of the screen. Create three components for map, table view and sidebar.

Ultimately our application will look like:

Final App Preview

There are three components in different parts of the page — the map component, list view and sidebar. The map will display the marker. The sidebar has a button to add a marker and the list view will display a table of all the markers on the map.

To start making a component, create a .vue file in the src folder. Reference the file in main.js using the ES6 import syntax.

import Vue from 'vue'  
import App from './App.vue'  
import GoogleMap from './GoogleMap'  
import ListView from './ListView'  
import Sidebar from './Sidebar'

Vue.component('GoogleMap', GoogleMap);  
Vue.component('ListView', ListView);  
Vue.component('Sidebar', Sidebar);

new Vue({  
 el: '#app',
 render: h => h(App)
});

Each component, to get started will look like this:

<template>  
   <div>
       <h1>Template name</h1>
   </div>
</template>

<style>  
</style>

<script>  
 export default {
   data(){
      return {};
   }
 }
</script>  

These component .vue files are a mix of JavaScript, HTML and CSS styles. All logic for one part of the application can live in the component’s .vue files and global functions can live in main.js.

Our Vue JavaScript and webpack bundler will render our App.vue file in index.html like a normal static website. Update the App.vue file to look like:

<div id="app" class="container">  
 <div class="row">
   <div class="col-4">
     <sidebar></sidebar>
   </div>
   <div class="col-8">
     <google-map></google-map>
   </div>
 </div>
 <div class="row">
   <list-view></list-view>
 </div>
</div>  

This renders all of our components on the page as we’ve loaded them up and made them available to the templates by the Vue.component() calls in main.js.

Render the Google Map

In order to render a Google Map, we’ll need to specify a width and height for the container HTML element, a start location and zoom level for the map. Define these as props so they can be configured if we decide to reuse the component with different values. Leaving the props blank will revert to the return value of the default function. The data function returns an object with keys as the state of the application. It sounds complicated but it’s not too bad.

<template>  
   <div>
       <h1>Map is here</h1>
       <div id="map" class="h-500"></div>
   </div>
</template>

<style scoped>  
   #map {
       margin: 0 auto;
       background: gray;
   }
   .h-500 {
       height:500px;
   }
</style>  
<script>  
 let sanfrancisco = [37.782685, -122.411364];

 export default {
   props: {
     'latitude': {
       type: Number,
       default() {
         return sanfrancisco[0]
       }
     },
     'longitude': {
       type: Number,
       default() {
         return sanfrancisco[1]
       }
     },
     'zoom': {
       type: Number,
       default() {
         return 14
       }
     },
   },
   mounted() {
     this.$map = new google.maps.Map(document.getElementById('map'), {
       center: new google.maps.LatLng(this.latitude, this.longitude),
       zoom: this.zoom
     });
   },
   data(){
       return {};
   }
 }
</script>  

We define optional properties for the component like view, latitude and longitude. When the component is mounted to the DOM, the mounted function will run and create a new Google Maps instance bound to the $map variable attached to the component object that selects our HTML by id. While running npm run dev, you should be able to view a map in your browser at this point.

Send Events Between Components

To illustrate how to send events between Vue components, we’re going to make a button in the sidebar component that will add markers to the map. The marker information will be displayed in the list view component. To communicate between components in this fashion, we’ll use what’s popularly called an Event Bus. You can think about it like a global object with some special Vue method magic. Define your event bus in the main.js file:

window.EventBus = new Vue({  
 data(){
   return {
     sanfrancisco: [37.78268, - 122.41136]
   }
 }
});

Keep some global state in your event bus. If you need more structure for your application, you could use a full featured state management library like Vuex, which is maintained by the Vue.js core team. This will be fine for now.

Update the map component to send and receive events with an event bus:

<template>  
   <div>
       <h1>Map is here</h1>
       <div id="map" class="h-250"></div>
   </div>
</template>

<style scoped>  
   #map {
       margin: 0 auto;
       background: gray;
   }
   .h-250 {
       height:250px;
   }
</style>  
<script>  
 import Vue from 'vue';

 export default {
   props: {
     'latitude': {
       type: Number,
       default() {
         return EventBus.sanfrancisco[0]
       }
     },
     'longitude': {
       type: Number,
       default() {
         return EventBus.sanfrancisco[1]
       }
     },
     'zoom': {
       type: Number,
       default() {
         return 14
       }
     },
   },
   mounted() {
     this.$markers = [];
     this.$map = new google.maps.Map(document.getElementById('map'), {
       center: new google.maps.LatLng(this.latitude, this.longitude),
       zoom: this.zoom
     });
     Vue.nextTick().then(()=>{
       this.clearMarkers();
     });
   },
   created(){
     EventBus.$on('clear-markers', ()=>{
       this.clearMarkers();
       this.$markers = [];
     });
     EventBus.$on('add-marker', (data)=>{
       let marker = this.makeMarker(data.latitude, data.longitude);
       this.$markers.push(marker);
     });
   },
   data(){
       return {};
   },
   methods: {
     makeMarker(latitude, longitude) {
       return new google.maps.Marker({
         position: new google.maps.LatLng(latitude, longitude),
         icon: null,
         map: this.$map,
         title: null,
       });
     },
     clearMarkers(){
       for( let i = 0; i < this.$markers.length; i++ ){
         this.$markers[i].setMap(null);
       }
     }
   }
 }
</script>  

The EventBus.$on syntax accepts a function that runs when the event is emitted. To emit an event, we call EventBus.$emit and can optionally pass in a JavaScript object as a second parameter that then gets passed as an argument to the $on function.

Fire Events Across Components

The sidebar component is responsible for having two buttons. One button will add a marker to the map; the other will clear all of the markers from the application. We have two input fields to specify exactly where we’d like the marker to show up.

The step attribute on the input field will allow the user to increment or decrement the latitude or longitude by that amount for the marker they want to place. The v-model attribute binds the key to the object returned from the data function. The data function is key to enable the script tag and the HTML within Vue components to communicate with each other.

<template>  
   <div>
       <h1>Sidebar</h1>
       <input type="number" step="0.0001" class="form-control" v-model="latitude">
       <input type="number" step="0.0001" class="form-control" v-model="longitude">
       <button @click="addMarker" class="btn btn-danger">Add marker</button>
       <button @click="clearMarkers" class="btn btn-default">Clear markers</button>
   </div>
</template>  
<script>

 export default {
   data(){
     return {
       latitude: EventBus.sanfrancisco[0],
       longitude: EventBus.sanfrancisco[1]
     };
   },
   methods: {
     addMarker(){
       EventBus.$emit('add-marker', {
         latitude: this.latitude,
         longitude: this.longitude
       });
     },
     clearMarkers(){
       EventBus.$emit('clear-markers');
     }
   }
 }
</script>  

Here, we’ve rendered our buttons and input fields. When the user clicks the button, this triggers methods that fire events using our global eventbus. We respond to those events in other components using the EventBus.$on function syntax.

In the list view we’ll follow a similar development pattern to render information about the generated markers. This component only needs to respond to EventBus events so we use the EventBus.$on syntax. The add-marker event expects an object with the marker information, while the clear-marker events takes no parameters and wipes the local markers component state. Both of these listeners begin listening when the component is created initially within the browser.

<template>  
   <div>
       <h1>ListView</h1>
       <p>Marker count: {{ markers.length }}</p>
       <table class="table">
           <thead>
               <tr>
                   <th>Index</th>
                   <th>Latitude</th>
                   <th>Longitude</th>
               </tr>
           </thead>
           <tbody>
               <tr v-for="marker in markers">
                   <td>{{ marker.index }}</td>
                   <td>{{ marker.latitude }}</td>
                   <td>{{ marker.longitude }}</td>
               </tr>
           </tbody>
       </table>
   </div>
</template>  
<script>  
   export default {
     created(){
       EventBus.$on('add-marker', (marker)=>{
         marker['index'] = this.index;
         this.index++;
         this.markers.push(marker);
       });
       EventBus.$on('clear-markers', ()=>{
         this.markers = [];
         this.index = 0;
       });
     },
     data(){
       return {
         index: 0,
         markers: []
       };
     }
   }
</script>  

When a marker is added, we add it to the component’s markers array and render it to the screen. The v-for loop always stays updated with the data, rendering new information without a hard page reload, making interactions snappy.

Conclusion

In this tutorial, we’ve built a Vue.js web application from the ground up. The application communicates asynchronously across different parts of the webpage to render information for our users without a page reload.

You may have noticed that we are storing information about the markers in multiple places. This shouldn't be an issue and works great as long as we remember to update the state of the markers for each component that needs the information. We do that with the “add-marker” and “clear-markers” events. An alternative approach is to store the state in an object that acts as a single source of truth. If you are interested in an alternative pattern, we recommend some further reading on state management from scratch with Vue.js.

Try Jscrambler for Free

We hope this has been an informative introduction into how to communicate across components with Vue.js.

The source code for this tutorial is freely available on GitHub and the author can be reached for questions or (nice) comments on Twitter at @connor11528.