Sunday, September 26, 2021

Loading External Scripts Dynamically In Angular

The easiest way to add external dependencies to your Angular projects is through npm. The second best option is to add script tags to the web page that hosts the application. However, that option only works if you have access to the host page. Barring that, you’ll have to load your JavaScript files dynamically. That’s actually fairly easy to do in JavaScript; in Angular, you have to make use of some of its built-in objects to accomplish it. In this tutorial, we’ll learn a couple of strategies for attaching external JS files using a custom service.

Strategy 1: Append a Script Using the src Attribute

Many JavaScript libraries are available via content delivery networks (CDNs). These are optimized for serving JS libraries very quickly. There are a few big CDNs that host multiple JS libraries (like Google’s CDN and cdnjs); other libraries have their own dedicated CDN. For the purposes of this tutorial, we’ll be loading the Google APIs Client Library for Browser JavaScript, also known as GAPI. It’s a set of client libraries for calling Google APIs in a variety of languages like Python, Java, and Node. It’s used in Google Sign-in, Google Drive, and thousands of internal and external web pages for easily connecting with Google APIs.

Our service will append a SCRIPT tag to the document body and set the src attribute to “https://apis.google.com/js/api.js”.

The ScriptService

To achieve its objective, the ScriptService requires two things:

  1. a reference to the document body
  2. a way to create elements

We can access component elements using decorators such as ViewChild, ViewChildren, but to reference the document itself, we can inject the Document object via the constructor. That will allow us to append the script to the document.body.

Angular provides the Renderer2 class to implement custom rendering. As such, it has everything you need to work with DOM elements, including an appendChild() method. We’ll pass it directly to loadJsScript() from the calling component.

Here’s the full service code:

import { Renderer2, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';

export class ScriptService {
 
  constructor(
    @Inject(DOCUMENT) private document: Document
  ) { }
 
 /**
  * Append the JS tag to the Document Body.
  * @param renderer The Angular Renderer
  * @param src The path to the script
  * @returns the script element
  */
  public loadJsScript(renderer: Renderer2, src: string): HTMLScriptElement {
    const script = renderer.createElement('script');
    script.type = 'text/javascript';
    script.src = src;
    renderer.appendChild(this.document.body, script);
    return script;
  }
}

Invoking the loadJsScript() Method in Angular and JavaScript

In the AppComponent, we’ll call the loadJsScript() method and use our new script. Both the Renderer2 and ScriptService are injected as constructor arguments. Then, in ngOnInit, we’ll call loadJsScript(). It returns the script element as an HTMLScriptElement. That gives us access to numerous properties and methods that make working with the script much easier. For instance, we can bind methods to the onload and onerror events.

One gotcha that often frustrates developers who are new to dynamic script loading in Angular is that TypeScript won’t let you reference the global script object (such as $ in jQuery) because it doesn’t know what it is. You can’t define it using const or let, because that would require you to prefix it with this., which does not refer to the global variable, but the one defined at the component level. Instead, we have to declare it and give it a type of any, i.e. declare let gapi: any;.

Here then is the full AppComponent code:

import { Component, OnInit, Renderer2 } from "@angular/core";
import { ScriptService } from "./services/script.service";

const SCRIPT_PATH = 'https://apis.google.com/js/api.js';
declare let gapi: any;

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {

  constructor(
    private renderer: Renderer2,
    private scriptService: ScriptService
  ) { }
 
  ngOnInit() {
    const scriptElement = this.scriptService.loadJsScript(this.renderer, SCRIPT_PATH);
    scriptElement.onload = () => {
     console.log('Google API Script loaded');
      console.log(gapi);

      // Load the JavaScript client library.
      // (the init() method has been omitted for brevity)
      gapi.load('client', this.init);
    }
    scriptElement.onerror = () => {
      console.log('Could not load the Google API Script!');
    }
  }
}

Strategy 2: Append a Data Block Script Using the text Attribute

In some cases, the script element is used as data block, which contains JSON-LD (type=”application/ld+json”). JSON-LD is a Resource Description Framework (RDF) serialization that allows you to publish Linked (or Structured) Data using JSON. This structured data can be used by any interested consumer. Here’s a sample script:

<script type="application/ld+json">
{
    "@context": "http://schema.org",
    "@type": "WebSite",
    "url": "http://website.com",
    "name": "wbs",
    "description": "Web Studio"
}
</script>

The service code to add a JSON-LD script is not much different than JavaScript; in addition to the different type, it sets the script text to the results of the JSON.stringify() method. It converts a JSON object to a JSON string:

public setJsonLd(renderer: Renderer2, data: any): void {
  let script = renderer.createElement('script');
  script.type = 'application/ld+json';
  script.text = `${JSON.stringify(data)}`;
  renderer.appendChild(this.document.body, script);
}

Notice that the setJsonLd() method does not return an HTMLScriptElement, since the script contents are local and will be available as soon as it’s appended to the document.

const JSON_LD_DATA  = `
{
  "@context": "http://schema.org",
  "@type": "WebSite",
  "url": "http://website.com",
  "name": "wbs",
  "description": "Web Studio"
}
`;

ngOnInit() {
  this.scriptService.setJsonLd(this.renderer, JSON_LD_DATA);
  console.log('JSON_LD_DATA Script loaded');
}

You’ll find the full demo for this tutorial on codesandbox.io.

Read: How to Optimize Angular Applications

Conclusion

In this tutorial we learned a couple of strategies for attaching external JS files using of Angular’s built-in objects. Although npm is still the preferred way to import external libraries into your projects, the techniques presented here today will provide an excellent alternative.

Rob Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Popular Articles

Featured