Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Load script file on demand with vanilla js

I use an accordion or tabs. When clicking on a tab, I want to display a map or something that require a big javascript file to load.

To not load the big javascript when it is not needed, I want to load it only when the tab is clicked.

How can I do that? I've created a good starting point with accordion and a click event.

  • I use vanilla js (pure javascript - no jQuery)
  • I use it on a webpage (not nodejs)
  • I'm fine if it works with modern browsers (IE not needed)

window.addEventListener('DOMContentLoaded', () => {
  document.querySelector('[data-trigger]').addEventListener('click', () => {
    console.log('Load script from file');
    // CDN example - https://cdnjs.cloudflare.com/ajax/libs/mapbox-gl/1.12.0/mapbox-gl.js
  });
});
ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

label {
  display: flex;
  align-items: center;
}

label:before {
  content: '';
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath d='M13.172 12l-4.95-4.95 1.414-1.414L16 12l-6.364 6.364-1.414-1.414z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  width: 24px;
  height: 24px;
}

input[type=checkbox] {
  display: none;
}

input[type=checkbox]:checked ~ h2 label:before {
  transform: rotate(90deg);
}

p {
  display: none;
}

input[type=checkbox]:checked ~ h2 ~ p {
  display: block;
}
<ul>
  <li>
    <input type="checkbox" id="faq-1">
    <h2 data-trigger>
      <label for="faq-1">Click to load map</label>
    </h2>
    <p>Gummies marzipan croissant chupa chups.</p>
  </li>

  <li>
    <input type="checkbox" id="faq-2">
    <h2>
      <label for="faq-2">Something else</label>
    </h2>
    <p>Cookie bear claw carrot cake croissant.</p>
  </li>
</ul>
like image 222
Jens Törnell Avatar asked Oct 08 '20 05:10

Jens Törnell


2 Answers

You could dynamically create a script tag and add it to the document whenever the button is pressed. This will trigger the script to immediately be loaded. With the onload callback you can run your code, like creating a map, the moment the script has actually loaded.

Do add { once: true } as the third parameter for addEventListener as it will ensure that the trigger can only be clicked once and won't be able to append the script more than once.

function createMap() {
  const map = new mapboxgl.Map({
    ...
  });
}

window.addEventListener('DOMContentLoaded', () => {
  document.querySelector('[data-trigger]').addEventListener('click', () => {
    console.log('Load script from file');
    
    const script = document.createElement('script');
    script.id = 'mapboxgljs';
    script.src = 'https://cdnjs.cloudflare.com/ajax/libs/mapbox-gl/1.12.0/mapbox-gl.js'
    script.async = true;
    script.onload = function() {
      console.log('script loaded, you can use it now.');
      createMap();
    }
    document.body.append(script);
  }, { once: true }); // Make sure that the button can only be pressed once.
});
like image 114
Emiel Zuurbier Avatar answered Nov 13 '22 09:11

Emiel Zuurbier


As you're comfortable with only supporting modern browsers, you could use the dynamic import() function to load the file:

window.addEventListener('DOMContentLoaded', () => {
  document.querySelector('[data-trigger]').addEventListener('click', async() => {
    const module = await import('https://cdnjs.cloudflare.com/ajax/libs/mapbox-gl/1.12.0/mapbox-gl.js');
    console.log(`module is loaded: ${Object.keys(mapboxgl)}`);
  });
});
ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

label {
  display: flex;
  align-items: center;
}

label:before {
  content: '';
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath d='M13.172 12l-4.95-4.95 1.414-1.414L16 12l-6.364 6.364-1.414-1.414z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  width: 24px;
  height: 24px;
}

input[type=checkbox] {
  display: none;
}

input[type=checkbox]:checked~h2 label:before {
  transform: rotate(90deg);
}

p {
  display: none;
}

input[type=checkbox]:checked~h2~p {
  display: block;
}
<ul>
  <li>
    <input type="checkbox" id="faq-1">
    <h2 data-trigger>
      <label for="faq-1">Click to load map</label>
    </h2>
    <p>Gummies marzipan croissant chupa chups.</p>
  </li>

  <li>
    <input type="checkbox" id="faq-2">
    <h2>
      <label for="faq-2">Something else</label>
    </h2>
    <p>Cookie bear claw carrot cake croissant.</p>
  </li>
</ul>

import() returns a promise which is resolved on successful fetch, and resolves immediately on subsequent calls (in my testing), so you wouldn't have to deal with that specifically.

For your specific mapbox-gl script, the value returned won't be useful, as the script isn't written as an ES6 module, but the side-effects of the script running will be in place (in this case, the mapboxgl variable being assigned onto globalThis). In scenarios where the loaded script is a module (using export correctly), you can use the resolved value directly to avoid polluting the global namespace.

like image 24
msbit Avatar answered Nov 13 '22 08:11

msbit