Decoupling from Google Maps

@andycallaghan
andycallaghan.com

Not this kind of coupling

But maybe a bit like this...

How to use third party Javascripts without it ruining your code

@andycallaghan
andycallaghan.com

Overview

  • How Google Maps has been used from 2005
  • Why this is bad for modern JS apps
  • What even IS coupling?
  • Solution one: Web component
  • Solution two: React component

Single page apps

festicket.com/festivals

Ye olde Google Maps

<head>
  <script
    type="text/javascript"
    src=”//maps.googleapis.com/maps/api/js?key=KEY"></script>
</head>

<div id=”map”></div>

<script>
  var map = new google.maps.Map(document.getElementById(‘map’), {
    ...
  })
  window.google.maps.GeolocationService.geocode("10 Downing Street, London")
</script>

On the page, not in our app's webpack

window.google.maps only available after DCL 🤨

Who can see the problems here?

JS loaded synchronously, first class citizen

Let's make it defer loading


<script
  src=”//maps.googleapis.com/maps/api/js?key=KEY&callback=initMap"
  defer async>
</script>

We've made it even worse...

<div id=”map”></div>

<script>
  function initMap() {
    var map = new google.maps.Map(document.getElementById(‘map’), {
      ...
    })
    
    window.google.maps.GeolocationService.geocode("10 Downing Street, London")
  }
</script>

<script src=”//maps.googleapis.com/maps/api/js?key=KEY&callback=initMap"
  async defer>
</script>

This ties our code to GMaps running initMap callback

JS load no longer in a predictable order

 

initMap is the entry point to our whole app 🙃

window.google.maps is not available at first

We rely on window.google

Real Airbrake.io client-side error

This is coupling

Our application's code relies on Google's JS forever

Your code relies on window.google

"the degree of interdependence between software modules"

Does it matter?

Spaghetti

Modern apps have lots of components

GMaps becomes THE app on the page, not a component

Other components written for it

yourgolftravel.com

Your dependence extends

So, what can we do?

  • Simple apps: HTML component
  • Most large apps: React component

HTML component

const gmap = async (e) => {
  return new Promise(async fulfilled => {
     e.style.height = e.style.width = e.getAttribute('size')
     let latlng = e.getAttribute('latlng').split(',')

     await load('https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&v=3')

     let map = new google.maps.Map(e, {
       center: new self.google.maps.LatLng(latlng[0], latlng[1]),
       zoom:   14
     });

     fulfilled(map)
  })
}

document.addEventListener("DOMContentLoaded", function(event) {
  let controls = document.querySelectorAll('div[control=gmap]')
  Promise.all(
    controls.map(control => {
      gmap(control)
    })
  )
})

<div control='gmap' size='200' latlng='53.9623241,-1.1169397'></div>

HTML component

const gmap = async (e) => {
  return new Promise(async fulfilled => {
     e.style.height = e.style.width = e.getAttribute('size')
     let latlng = e.getAttribute('latlng').split(',')

     await load('https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&v=3')

     let map = new google.maps.Map(e, {
       center: new self.google.maps.LatLng(latlng[0], latlng[1]),
       zoom:   14
     });

     fulfilled(map)
  })
}

document.addEventListener("DOMContentLoaded", function(event) {
  let controls = document.querySelectorAll('div[control=gmap]')
  Promise.all(
    controls.map(control => {
      gmap(control)
    })
  )
})

<div control='gmap' size='200' latlng='53.9623241,-1.1169397'></div>

Async loading, only once

google.maps encapsulated inside a function

Control when it's loaded

Minimal data in the DOM

W3C's WebComponents

Plans to standardise this started in 2014

developers.google.com/web/fundamentals/web-components/customelements

w3.org/wiki/WebComponents

React component

import React, { Component } from 'react'
import { withScriptjs, withGoogleMap, GoogleMap, Marker } from "react-google-maps"

const MapComponent = withScriptjs(
  withGoogleMap((props) =>
    <GoogleMap defaultZoom={10} defaultCenter={{ lat: 53.9623241, lng: -1.1169397 }}>
      <Marker position={{ lat: 53.9623241, lng: -1.1169397 }} />
    </GoogleMap>
  ))

class App extends Component {
  render() {
    return (
      <divf>
        <header>
          <h1>React Google Maps</h1>
        </header>

        <MapComponent
          googleMapURL="https://maps.googleapis.com/maps/api/js?v=3"
        />
      </div>
    );
  }
}

React component

import React, { Component } from 'react'
import { withScriptjs, withGoogleMap, GoogleMap, Marker } from "react-google-maps"

const MapComponent = withScriptjs(
  withGoogleMap((props) =>
    <GoogleMap defaultZoom={10} defaultCenter={{ lat: 53.9623241, lng: -1.1169397 }}>
      <Marker position={{ lat: 53.9623241, lng: -1.1169397 }} />
    </GoogleMap>
  ))

class App extends Component {
  render() {
    return (
      <div>
        <header>
          <h1>React Google Maps</h1>
        </header>

        <MapComponent
          googleMapURL="https://maps.googleapis.com/maps/api/js?v=3"
        />
      </div>
    );
  }
}

Compiled script handling markers & async loading

Componentised element

Data out of the DOM

Other interactions with the map become components

Thanks!

@AndyCallaghan

andycallaghan.com

yourgolftravel.com

All code at

github.com/acallaghan/coupling-google-maps

Made with Slides.com