How to load a 3rd party script from web dynamically into Angular2 component
Asked Answered
A

3

9

I am trying to load a 3rd party script from web, instead of making a local copy of it and be able to use the 3rd party script's global variables and functions after the script loads.

Update:

  • Here is an example of what I am trying to achieve in plain JavaScript where clicking on Visa Checkout button opens Visa Checkout dialog: Plunker JS link
  • Here is the Angular2 version of it where I need help: Plunker Angular2 link

Issue: Component below is not loading the script from web

import {Component} from '@angular/core'

@Component({
  selector: 'custom',
  providers: [],
  template: `
    <div>
      <h2>{{name}}</h2>
      <img class="v-button" role="button" alt="Visa Checkout" src="https://sandbox.secure.checkout.visa.com/wallet-services-web/xo/button.png">
      <script src="https://sandbox-assets.secure.checkout.visa.com/checkout-widget/resources/js/integration/v1/sdk.js">
</script>
    </div>
  `
})
export class CustomComponent {
  constructor() {
    this.name = 'Custom Component works!!'
  }
}
Acquittance answered 9/6, 2016 at 15:12 Comment(0)
T
11

You can dynamically load JS scripts and libraries on demand in your Angular 2/4 project using this technique.

Create ScriptStore in script.store.ts that will contain the path of the script either locally or on a remote server and a name that will be used to load the script dynamically:

interface Scripts {
    name: string;
    src: string;
}  

export const ScriptStore: Scripts[] = [
    {name: 'filepicker', src: 'https://api.filestackapi.com/filestack.js'},
    {name: 'rangeSlider', src: '../../../assets/js/ion.rangeSlider.min.js'}
];

Create script.service.ts to provide ScriptService as an injectable service that will handle the loading of script files. Include this code:

import {Injectable} from "@angular/core";
import {ScriptStore} from "./script.store";

declare var document: any;

@Injectable()
export class ScriptService {

private scripts: any = {};

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

load(...scripts: string[]) {
    var promises: any[] = [];
    scripts.forEach((script) => promises.push(this.loadScript(script)));
    return Promise.all(promises);
}

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
            let script = document.createElement('script');
            script.type = 'text/javascript';
            script.src = this.scripts[name].src;
            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'});
            document.getElementsByTagName('head')[0].appendChild(script);
        }
    });
}

}

Inject ScriptService wherever you need it and load scripts like this:

constructor(
    private scriptService: ScriptService
) { }

ngOnInit() {
    this.scriptService.load('filepicker', 'rangeSlider').then(data => {
        console.log('script loaded ', data);
    }).catch(error => console.log(error));
}
Trapezohedron answered 13/3, 2017 at 14:27 Comment(0)
D
3

I have modified Rahul Kumar's answer so that it uses Observables instead:

import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { Observer } from "rxjs/Observer";

@Injectable()
export class ScriptLoaderService {
    private scripts: {ScriptModel}[] = [];

    public load(script: ScriptModel): Observable<ScriptModel> {
        return new Observable<ScriptModel>((observer: Observer<ScriptModel>) => {
            var existingScript = this.scripts.find(s => s.name == script.name);

            // Complete if already loaded
            if (existingScript && existingScript.loaded) {
                observer.next(existingScript);
                observer.complete();
            }
            else {
                // Add the script
                this.scripts = [...this.scripts, script];

                // Load the script
                let scriptElement = document.createElement("script");
                scriptElement.type = "text/javascript";
                scriptElement.src = script.src;

                scriptElement.onload = () => {
                    script.loaded = true;
                    observer.next(script);
                    observer.complete();
                };

                scriptElement.onerror = (error: any) => {
                    observer.error("Couldn't load script " + script.src);
                };

                document.getElementsByTagName('body')[0].appendChild(scriptElement);
            }
        });
    }
}

export interface ScriptModel {
    name: string,
    src: string,
    loaded: boolean
}
Declinatory answered 10/5, 2017 at 11:42 Comment(1)
cheers... it works to load... but how would I execute it? and mimic the async functionality?Preterite
M
2

There are two ways that this can be accomplished.

  1. Reference a type definition file for the the 3rd party script that you are adding. Type definition files typically end in .d.ts and are basically an interface for the functions of the script. If there is not a pre-defined type definition file you can create one your self with the functions that you need. (I prefer this method because some IDEs will give you the method signature as part of the intellisense)
  2. Create a variable at the top of the TypeScript class that represents the library that you are using with a type of any;

Example using AutoMapperTS:

Type Definition:

/// <reference path="../node_modules/automapper-ts/dist/automapper.d.ts" />

@Component({
    selector: "my-app",
})
export class AppComponent {
    constructor() {
        automapper.map("JSON", "myType", jsonObj);
    }
}

(The reference in this example may be different depending on your editor. This example is using Visual Studio. Try dragging the file you want to reference onto the editor window to see if your IDE will create the reference for you.)

Declaration:

declare var automapper: any;

@Component({
    selector: "my-app",
})
export class AppComponent {
    constructor() {
        automapper.map("JSON", "myType", jsonObj);
    }
}

The 3rd party JS file can be loaded using the standard <script> tag import. The above methods are for the TS compiler so that it won't fail with an unknown variable exception.

Menell answered 9/6, 2016 at 16:53 Comment(6)
Thanks for the quick response. I'm new to Angular and I didn't quite understand your response. Can you take a look at my Updated question with Plunker examples? Really appreciate help!Acquittance
I took a look at your Plunker but the site is down right now. The first issue I see is that you should not be including script tags in the template of a component as Angular strips them out. 3rd part scripts should be loaded in the index.html. I will take another look once Plunker is back up.Menell
Although I would like to see how it works by including the script in index.html, I would prefer to lazy load the script along with when my-component. In this particular case, I also need to access the Global variables of the script loaded from web in my-component.Acquittance
My research indicates that you can use System.import to lazy load in script files. I attempted this in a fork of your plunk but was unsuccessful. However, the performance gain that you will get from loading in this one script at a later time rather than up front in the index.html will be next to nil.Menell
Thank you! As you mentioned, I have added the script in index.html and followed the instructions at Include External JavaScript Libraries In An Angular 2 TypeScript ProjectAcquittance
Great! If you feel like my answer is correct for your question then I would appreciate you accepting it.Menell

© 2022 - 2024 — McMap. All rights reserved.