Load Google Maps after click on consent button

The best way to learn something is to try and build things! So let's build a real-world example by loading and initialize a google map when the user clicks on a button.

$ mkdir loady loady/assets
$ cd loady
$ touch index.html assets/main.js
<!-- index.html -->
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Load scripts after consent</title>
    <style>
      html,
      body {
        height: 100%;
        margin: 0;
        padding: 0;
      }
      #map {
        width: 100%;
        height: 100%;
        display: grid;
        place-content: center;
      }
      #load {
        border: 1px solid #eee;
        background: transparent;
        padding: 6px 10px;
        font-size: 16px;
        cursor: pointer;
        border-radius: 3px;
      }
    </style>
  </head>
  <body>
    <div id="map">
      <button id="load">Click me to load js</button>
    </div>
    <script src="./assets/main.js"></script>
  </body>
</html>

Cool, that's all we need to get started. At the moment, nothing happens, so let's fill the main.js file!

// main.js
const loady = (trigger, storageItem, script, attributes) => {}

loady is our main function that handles the loading of the Google Maps JavaScript API. It takes four arguments, which do the following: trigger is the clicked button, storageItem is the name of the item in localStorage to look for consent, script is the URL to the API script, and attributes are added to the script element, like defer or async.

// main.js
const triggerButton = document.querySelector('#load')
const storageItem = 'mapsConsent'
const script = 'https://maps.googleapis.com/maps/api/js?key=YOUR-API-KEY&callback=initMap'
const attributes = {}

const loady = (trigger, storageItem, script, attributes) => {}

loady(triggerButton, storageItem, script, attributes)

At the moment nothing happens, but we fed our loady function with all information it needs! Now let's move on and build some logic into it.

Check trigger and localStorage

First, we need to make sure that the trigger is a DOM element, and the browser supports localStorage.

// main.js
// ...
const loady = (trigger, storageItem, script, attributes) => {
  if (trigger instanceof HTMLElement === false) {
    throw new Error('Trigger is not a DOM Element.')
  }

  if (typeof localStorage === undefined) {
    throw new Error('localStorage is not available in this browser.')
  }
}
// ...

Add eventListener to trigger

Ok, now that we're sure that the trigger is a DOM element and localStorage works, let's add an eventListener to the trigger and the function that runs on click.

// main.js
// ...
const loady = (trigger, storageItem, script, attributes) => {
  // ...
  const run = () => {
    localStorage.setItem(storageItem, true) // set storageItem in localStorage to 'true'
    const scriptElement = createScriptElement() // function that will create our script element
    appendScriptElement(scriptElement) // function that will append our script to the DOM
  }

  trigger.addEventListener('click', () => run())
}
// ...

Create script element and append it to the DOM

Like you saw in our run function, we need to more functions: one that creates our script element and on that appends it to the head (or before the closing body if you wish).

// main.js
// ...
const loady = (trigger, storageItem, script, attributes) => {
  // ...
  const createScriptElement = () => {
    const el = document.createElement('script')
    el.src = script
    for (let [key, value] of Object.entries(attributes)) {
      el.setAttribute(key, value)
    }
    return el
  }

  const appendScriptElement = (el) => {
    document.getElementsByTagName('head')[0].appendChild(el) // appends the script to the head
  }
  // ...
}
// ...

Write a basic google maps initialization

Before we can test loady, we need to write the function that initializes google maps.

// main.js
// ...
window.initMap = () => {
  let map = new google.maps.Map(document.getElementById('map'), {
    center: { lat: 52.28155, lng: 8.04235 },
    zoom: 15,
  })
}

const loady = (trigger, storageItem, script, attributes) => {
  // ...
}
// ...

Now spin up a small server and let's test loady!

$ php -S localhost:8181

If you visit our test site and click on the button you should see... a map! Nice! But loady has two small problems we need to tackle.

Check if consent is already given

At the moment, every time you reload the site you'll need to click the button again, even if you gave the consent before. To avoid this, we check the localStorage at the beginning, and trigger the run function if our storageItem is already set to true.

// main.js
// ...
const loady = (trigger, storageItem, script, attributes) => {
  // ...
  let consent = localStorage.getItem(storageItem)
  // ...
  const run = () => {
    localStorage.setItem(storageItem, true)
    consent = localStorage.getItem(storageItem) // override localStorage item with new value
    const scriptElement = createScriptElement()
    appendScriptElement(scriptElement)
  }

  trigger.addEventListener('click', () => run())
  if (consent === 'true') trigger.click() // Trigger run function
}
// ...

Now our script gets loaded if the consent was given before, even on reload.

Check if the script is already appended

One last thing we need to check is if the script is already appended. Otherwise, it could happen that the script will be appended twice. Add the following isAlreadyAppended function and adjust our appendScriptElement to check the status.

// main.js
// ...
const loady = (trigger, storageItem, script, attributes) => {
  // ...
  const isAlreadyAppended = () => {
    // check if script with src is already in DOM
    const scripts = Array.from(document.getElementsByTagName('script'))
    let result = false
    scripts.forEach((s) => (s.src == script ? (result = true) : null))
    return result
  }

  // ...
  const appendScriptElement = (el) => {
    const isAppended = isAlreadyAppended()
    if (!isAppended) {
      // append script if it's not already in the DOM
      document.getElementsByTagName('head')[0].appendChild(el)
    }
  }
  // ...
}
// ...

Done! Our small little loady function successfully loads scripts after a consent click! You could expand loady to, for example, run a callback after adding the script, or make it work with async/await, etc.

The complete script

// Set our variables
const triggerButton = document.querySelector('#load') // our trigger button
const storageItem = 'mapsConsent' // key of the item to check if consent is given
const script = 'https://maps.googleapis.com/maps/api/js?key=AIzaSyCTpR8Iqp8AFDWbP64uPK9wKTJThDfD4os&callback=initMap' // script to load
const attributes = {} // could contain something like { 'defer': true }

// Basic google maps init function
window.initMap = () => {
  let map = new google.maps.Map(document.getElementById('map'), {
    center: { lat: 52.28155, lng: 8.04235 },
    zoom: 15,
  })
}

// Our main function to handle script loading
const loady = (trigger, storageItem, script, attributes) => {
  // Check if trigger parameter is a DOM element
  if (trigger instanceof HTMLElement === false) {
    throw new Error('Trigger is not a DOM Element.')
  }

  // Check if browser supports localStorage
  if (typeof localStorage === undefined) {
    throw new Error('localStorage is not available in this browser.')
  }

  // Read the localStorage item, to check if consent is already given
  let consent = localStorage.getItem(storageItem)

  // Function to check if script is already appended to the DOM
  const isAlreadyAppended = () => {
    const scripts = Array.from(document.getElementsByTagName('script'))
    let result = false
    scripts.forEach((s) => (s.src == script ? (result = true) : null))
    return result
  }

  // Function that creates our script element. Also adds attributes if that
  // parameter is filled with an object
  const createScriptElement = () => {
    const el = document.createElement('script')
    el.src = script
    for (let [key, value] of Object.entries(attributes)) {
      el.setAttribute(key, value)
    }
    return el
  }

  // Function that appends script to the Head. Could also be adjusted to prepend
  // it before the closing </body> if you want to.
  const appendScriptElement = (el) => {
    const isAppended = isAlreadyAppended()
    if (!isAppended) {
      document.getElementsByTagName('head')[0].appendChild(el)
    }
  }

  // Our run function that will be trigger if the user a) clicks on our trigger
  // element or b) if consent is already given and found in localStorage.
  const run = () => {
    localStorage.setItem(storageItem, true)
    consent = localStorage.getItem(storageItem)
    const scriptElement = createScriptElement()
    appendScriptElement(scriptElement)
  }

  // Add eventListener to trigger element and check if consent is already given
  trigger.addEventListener('click', () => run())
  if (consent === 'true') trigger.click()
}

// Run loady
loady(triggerButton, storageItem, script, attributes)