Dev

ES6 Google Maps

Working with the Google Maps API, all es6 like.

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.

Read Next

Audio API Gooey Player

Read Story