When I was first diving into modern/es6 JavaScript and learning about modules and bundlers and so on, I wasn’t sure how to interact with the Google Maps API in a module-driven fashion.
Surprisingly, the interwebs was returning very little on the matter at that time.
I eventually realized that you should have a separate loader class to inject the Google Maps script, with a promise that upon resolution provides a callback for a map rendering function.
Here’s what that looks like:
Google Maps API Class
// Map/GoogleMapsApi.js
/**
* GoogleMapsApi
* Class to load google maps api with api key
* and global Callback to init map after resolution of promise.
*
* @exports {GoogleMapsApi}
* @example MapApi = new GoogleMapsApi();
* MapApi.load().then(() => {});
*/
class GoogleMapsApi {
/**
* Constructor
* @property {string} apiKey
*/
constructor(gApiKey) {
this.apiKey = gApiKey;
if (!window._GoogleMapsApi) {
this.callbackName = '_GoogleMapsApi.mapLoaded';
window._GoogleMapsApi = this;
window._GoogleMapsApi.mapLoaded = this.mapLoaded.bind(this);
}
}
/**
* Load
* Create script element with google maps
* api url, containing api key and callback for
* map init.
* @return {promise}
* @this {_GoogleMapsApi}
*/
load() {
if (!this.promise) {
this.promise = new Promise(resolve => {
this.resolve = resolve;
if (typeof window.google === 'undefined') {
const script = document.createElement('script');
script.src = `//maps.googleapis.com/maps/api/js?key=${this.apiKey}&callback=${this.callbackName}`;
script.async = true;
document.body.append(script);
} else {
this.resolve();
}
});
}
return this.promise;
}
/**
* mapLoaded
* Global callback for loaded/resolved map instance.
* @this {_GoogleMapsApi}
*/
mapLoaded() {
if (this.resolve) {
this.resolve();
}
}
}
export default GoogleMapsApi
Now we can reference the load()
method to call a map renderer function and pass in an api key:
import GoogleMapsApi from './GoogleMapsApi'
const gApiKey = 'xxxxxxxxxxxxxxxxxxxxx'
const gmapApi = new GoogleMapsApi(gApiKey)
gmapApi.load().then(() => {
renderMap()
})
Cool, now we’re cooking. But, let’s built it out some. For better reusability, how’s about we:
- Set map options with data attributes (lat, lng, zoom, etc)
- House map stylers in separate file
- Use a custom Marker and Infowindow, also saved in a separate file
First, let’s create our map markup with data attributes for options:
Markup
// map.html
<section class="map">
<div class="map__wrap">
<div
class="=map__map js-map"
data-lat="40.436821"
data-lng="-79.908803"
data-address="2005 Beechwood Blvd, Pittsburgh, PA 15217"
data-title="Marker Title"
data-zoom="14">
</div>
</div>
</section>
Custom Marker Template
// Map/marker.tmpl.js
/**
* Marker Template
* @param {obj} data - property data/json
*/
function markerTmpl(data) {
const url = encodeURIComponent(data.address)
return `
<article class="marker-box">
<div class="marker-box__wrap">
<div class="marker-box__grid">
<div class="marker-box__main">
<span class="marker-box__title">${data.title}
<address class="marker-box__address">
{data.address}
</address>
<a class="marker-box__btn btn-line" href="https://www.google.com/maps/place/${url}/">
Get Directions
</a>
</div>
</div>
</div>
</article>`
export default markerTmpl
Stylers and Icon
// Map/stylers.js
export const stylers = {
styles: [
{
"featureType": "administrative",
"elementType": "labels.text.fill",
"stylers": [ {
"color": "#444444"
} ]
},
...
And, finally, our Map and marker rendering functions:
Map Functions
// Map/index.js
import GoogleMapsApi from './GoogleMapsApi'
import { stylers } from './stylers'
import markerTmpl from './marker.tmpl'
/**
* Location Map
* Main map rendering function that uses our GMaps API class
* @param {string} el - Google Map selector
*/
export function LocationMap(el) {
const gApiKey = 'xxxxxxxxxxxxxxxxxxxxx'
const gmapApi = new GoogleMapsApi(gApiKey)
const mapEl = document.querySelector(el)
const data = {
lat: parseFloat(mapEl.dataset.lat ? mapEl.dataset.lat : 0),
lng: parseFloat(mapEl.dataset.lng ? mapEl.dataset.lng : 0),
address: mapEl.dataset.address,
title: mapEl.dataset.title ? mapEl.dataset.title: "Map",
zoom: parseFloat(mapEl.dataset.zoom ? mapEl.dataset.zoom: 12),
}
// Call map renderer
gmapApi.load().then(() => {
renderMap(mapEl, data)
})
}
/**
* Render Map
* @param {map obj} mapEl - Google Map
* @param {obj} data - map data
*/
function renderMap(mapEl, data) {
const options = {
mapTypeId: google.maps.MapTypeId.ROADMAP,
styles: stylers.styles,
zoom: data.zoom,
center: {
lat: data.lat,
lng: data.lng
}
}
const map = new google.maps.Map(mapEl, options)
renderMarker(map, data)
}
/**
* Render Marker
* Renders custom map marker and infowindow
* @param {map element} mapEl
* @param {object} data
*/
function renderMarker(map, data) {
const icon = {
url: stylers.icons.red,
scaledSize: new google.maps.Size(80, 80)
}
const tmpl = markerTmpl(data)
const marker = new google.maps.Marker({
position: new google.maps.LatLng(data.lat, data.lng),
map: map,
icon: icon,
title: data.title,
content: tmpl,
animation: google.maps.Animation.DROP
})
const infowindow = new google.maps.InfoWindow()
handleMarkerClick(map, marker, infowindow)
}
/**
* Handle Marker Click
*
* @param {map obj} mapEl
* @param {marker} marker
* @param {infowindow} infoWindow
*/
function handleMarkerClick(map, marker, infowindow) {
google.maps.event.addListener(marker, 'click', function() {
infowindow.setContent(marker.content)
infowindow.open(map, marker)
})
google.maps.event.addListener(map, 'click', function(event) {
if (infowindow) {
infowindow.close(map, infowindow)
}
})
}
Now we can just import and init it all, passing in our map selector:
// app.js
import * as Map from './Map'
Map.MapInit('.js-map')
Github
You can go snag the code on Gihtub, Right Here
Thanks for reading.