How to dynamically load external scripts in Angular

In Angular applications sometimes it’s necessary to use external libraries like Google Maps, as in any other application as well. In this post I’ll show how to create a service for this job.

The basic idea is to use

document.createElement('script')

to add the script source and append it to the DOM head element. Two things need to be addressed specifically however:

1) Async functionality to await the script loaded event
2) Preventing multiple loading of the same script

The scripts service should haveĀ a ‘load’ function to load a single or multiple scripts.

A config array provides all the loadable scripts with a name and source attribute.

const myScripts = [
  { name: 'googleMaps', src: 'https://maps.googleapis.com/maps...'}
];

@Injectable()
export class ScriptService {

  private scripts: any = {};

  constructor() {
    myScripts.forEach((script: any) => {
      this.scripts[script.name] = {
        loaded: false,
        src: script.src
      };
    });
  }

  // load a single or multiple scripts
  load(...scripts: string[]) {
    const promises: any[] = [];
    // push the returned promise of each loadScript call 
    scripts.forEach((script) => promises.push(this.loadScript(script)));
    // return promise.all that resolves when all promises are resolved
    return Promise.all(promises);
  }
  
  // load the script
  loadScript(name: string) {
    return new Promise((resolve, reject) => {
      // resolve if already loaded
      if (this.scripts[name].loaded) {
        resolve({script: name, loaded: true, status: 'Already Loaded'});
      } else {
        // load script
        const script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = this.scripts[name].src;
        // cross browser handling of onLoaded event
        if (script.readyState) {  // IE
          script.onreadystatechange = () => {
            if (script.readyState === 'loaded' || script.readyState === 'complete') {
              script.onreadystatechange = null;
              this.scripts[name].loaded = true;
              resolve({script: name, loaded: true, status: 'Loaded'});
            }
          };
        } else {  // Others
          script.onload = () => {
            this.scripts[name].loaded = true;
            resolve({script: name, loaded: true, status: 'Loaded'});
          };
        }
        script.onerror = (error: any) => resolve({script: name, loaded: false, status: 'Loaded'});
        // finally append the script tag in the DOM
        document.getElementsByTagName('head')[0].appendChild(script);
      }
    });
  }

}

Using it in a component would look something like this:

constructor(scriptService: ScriptService) {
  scriptService
    // one or more arguments
    .load('googleMaps')
    .then(data => {
      // script is loaded, use it
      this.googleMapsApiLoaded = true;
    });
}