loading...
Cover image for Inject JavaScript Or CSS At Runtime And On Demand

Inject JavaScript Or CSS At Runtime And On Demand

daviddalbusco profile image David Dal Busco Originally published at Medium ・4 min read

One Trick A Day (35 Part Series)

1) How To Call The Service Worker From The Web App Context 2) Replace Environment Variables In Your Index.html 3 ... 33 3) Inject JavaScript Or CSS At Runtime And On Demand 4) Sometimes You Just Need A Dumb Library 5) Internationalization with Gatsby 6) How To Declare And Use Ionic Modals With Stencil 7) Get App Name And Version In Angular 8) Deploy Apps And Functions To Firebase From A Mono Repo With GitHub Actions 9) Starting In A New Company? Think Npmrc And Git Name 10) Test Angular Pipes With Services 11) Gatsby Tricks: Viewport, CSS Modules Transition And i18n Tricks 12) Takeover The Cordova Facebook Plugin Maintenance 13) Protect Your HTTP Firebase Cloud Functions 14) Create A Menu For Your Gatsby Website Without Libs 15) Create A Modal For Your Angular App Without Libs 16) Add A Slider To You Angular App 17) Test Angular Components and Services With HTTP Mocks 18) Merge Two Objects And Array To Object In JavaScript 19) JSX For Angular Developers 20) More JSX For Angular Developers 21) Create Your Own NPM Cli 22) Third Party Service Providers. Be transparent to each other! 23) React And Web Workers 24) Angular Testing: Mock Private Functions 25) React, Web Workers and IndexedDB 26) React, Web Workers, IndexedDB and ExcelJS 27) GitHub Actions: Hide And Set Angular Environment Variables 28) JavaScript Useful Functions 29) Deeplinking in Ionic Apps With Branch.io 30) Follow-up: Web Push Notifications And PWA In 2020 31) Angular And Web Workers 32) Git Commands I Always Forget 33) An Open Source Medium Like WYSIWYG Editor 34) Currency Picker And Formatter With Ionic React 35) Develop A Konami Code For Any Apps With Stencil

I challenged my self to share a blog post each and every single day until end of the current quarantine in Switzerland, the 19th April 2020. Thirty-two days left until hopefully better days.

In this third blog post, I would like to share with you a trick we are using in our open source project DeckDeckGo but also one which has been shared by Cory McArthur, an incredible user experience engineer of Sworkit.

Commonly you are including your dependencies in your app bundle, but some of these might be used only in certain circumstances. For instance, if you are using Firebase UI to handle your authentication flow or if like us, you create a Web Component which act as a wrapper around another library like Prismjs, you might want to load these only when really needed.

Even though a lazy loading pattern might be use in your app, depending of your UX and routing, you might rarely face the case where such libraries are fetched even if actually not needed.

But no worries, here’s a trick to solve such requirement by injecting either a script or css in your page on demand and at runtime.

Load Conditionally A Script

Let’s try to develop a new Web Component with Stencil which fetch a script when mounted. To do so, we run the following commands in a terminal:

npm init stencil
cd my-component
npm install

Once the project created, we edit the component and add a first test in order to verify if our script has not been added to the DOM before, because our component can be use multiple times in a page and we want to load our script only once.

import { Component, h } from '@stencil/core';

@Component({
  tag: 'my-component',
  styleUrl: 'my-component.css',
  shadow: true
})
export class MyComponent {

  async componentDidLoad() {
    const scripts = document.querySelector('[myscript-loaded]');

    if (!scripts) {
      // TODO: load script
    }
  }

  render() {
    return <div>Hello, World!</div>;
  }
}

Finally we can add our effective implementation which summarized works like the following: we create a new deferred <script/> which references the library or component we would like to load. Before adding it to the header of our page, we attach two events to handle both success or error .

import { Component, h } from '@stencil/core';

@Component({
  tag: 'my-component',
  styleUrl: 'my-component.css',
  shadow: true
})
export class MyComponent {

  async componentDidLoad() {
    const scripts = document.querySelector('[myscript-loaded]');

    if (!scripts) {
      const script = document.createElement('script');

      script.onload = async () => {
        script.setAttribute('myscript-loaded', 'true');
      };

      script.onerror = async ($err) => {
        console.error($err);
      };

      script.src = 'https://unpkg.com/myscript.js';
      script.defer = true;

      document.head.appendChild(script);
    }
  }

  render() {
    return <div>Hello, World!</div>;
  }
}

And…that’s it 🎉. By injecting the script in the header, the browser notices the change and proceeds it as it would normally do with any scripts.

Cory’s Generic Functions

Above solution is cool but generic functions are way cooler and handier 😉. Therefore here is Cory’s awesome solution to load any JavaScript or CSS on demand:

function injectJS(id: string, src: string): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    if (!document) {
      resolve();
      return;
    }

    if (document.getElementById(id)) {
      resolve('JS already loaded.');
      return;
    }
    const script = document.createElement('script');

    script.id = id;
    script.async = true;
    script.defer = true;
    script.src = src;

    script.addEventListener('load', () => resolve('JS loaded.'));

    script.addEventListener('error', () => reject('Error script.'));
    script.addEventListener('abort', () => reject('Aborted.'));

    document.head.appendChild(script);
  });
}

function injectCSS(id: string, src: string): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    if (!document) {
      resolve();
      return;
    }

    if (document.getElementById(id)) {
      resolve('CSS already loaded.');
      return;
    }

    const link = document.createElement('link');
    link.id = id;
    link.setAttribute('rel', 'stylesheet');
    link.setAttribute('href', src);

    link.addEventListener('load', () => resolve('CSS loaded.'));

    link.addEventListener('error', () => reject('Error css.'));
    link.addEventListener('abort', () => reject('CSS aborted.'));

    document.head.appendChild(link);
  });
}

Such utilities can notably use to load Firebase UI only when needed:

await injectJS('firebase-ui-script', 'https://cdn.firebase.com/libs/firebaseui/4.0.0/firebaseui.js');
await injectCSS('firebase-ui-css', 'https://cdn.firebase.com/libs/firebaseui/4.0.0/firebaseui.css');

Summary

One downside of the above solution, I’m agree, is the fact that you are handling a version number in, kind of, the middle of your code but to me, that’s a small trade of to be able to fetch some libraries only when needed because of the particular requirements of the UX of our editor, DeckDeckgo.

I warmly thank Cory for having shared his solution and also for having answered some of my questions, when I developed our authentication’s flow. Not all heroes wear capes, you are super Cory 🙏

Stay home, stay safe!

David

Cover photo by Aditya Saxena on Unsplash

One Trick A Day (35 Part Series)

1) How To Call The Service Worker From The Web App Context 2) Replace Environment Variables In Your Index.html 3 ... 33 3) Inject JavaScript Or CSS At Runtime And On Demand 4) Sometimes You Just Need A Dumb Library 5) Internationalization with Gatsby 6) How To Declare And Use Ionic Modals With Stencil 7) Get App Name And Version In Angular 8) Deploy Apps And Functions To Firebase From A Mono Repo With GitHub Actions 9) Starting In A New Company? Think Npmrc And Git Name 10) Test Angular Pipes With Services 11) Gatsby Tricks: Viewport, CSS Modules Transition And i18n Tricks 12) Takeover The Cordova Facebook Plugin Maintenance 13) Protect Your HTTP Firebase Cloud Functions 14) Create A Menu For Your Gatsby Website Without Libs 15) Create A Modal For Your Angular App Without Libs 16) Add A Slider To You Angular App 17) Test Angular Components and Services With HTTP Mocks 18) Merge Two Objects And Array To Object In JavaScript 19) JSX For Angular Developers 20) More JSX For Angular Developers 21) Create Your Own NPM Cli 22) Third Party Service Providers. Be transparent to each other! 23) React And Web Workers 24) Angular Testing: Mock Private Functions 25) React, Web Workers and IndexedDB 26) React, Web Workers, IndexedDB and ExcelJS 27) GitHub Actions: Hide And Set Angular Environment Variables 28) JavaScript Useful Functions 29) Deeplinking in Ionic Apps With Branch.io 30) Follow-up: Web Push Notifications And PWA In 2020 31) Angular And Web Workers 32) Git Commands I Always Forget 33) An Open Source Medium Like WYSIWYG Editor 34) Currency Picker And Formatter With Ionic React 35) Develop A Konami Code For Any Apps With Stencil

Posted on by:

daviddalbusco profile

David Dal Busco

@daviddalbusco

Creator of DeckDeckGo | Organizer of the Ionic Zürich Meetup

Discussion

markdown guide
 

I found this code a while back, and I can't remember where I found it... kudos goes to the actual originator 😁