10

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!!'
  }
}

3 Answers 3

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));
}
Sign up to request clarification or add additional context in comments.

Comments

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
}

1 Comment

cheers... it works to load... but how would I execute it? and mimic the async functionality?
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.

6 Comments

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!
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.
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.
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.
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 Project
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.