57

I am creating an Angular application using Angular 4 and the CLI. I am trying to add the SkyScanner search widget into one of my components.

Skyscanner Widget Example

Part of the implementation requires the addition of a new external script:

<script src="https://widgets.skyscanner.net/widget-server/js/loader.js" async></script>

I am not sure of the correct way to reference this file. If I add the script into my index.html file, the widget doesn't load unless a full page refresh is performed. I assume the script tries to manipulate the DOM on load and the elements don't exist when the script runs.

What is the correct way to load the script only when the component containing the Skyscanner widget is loaded?

infojolt
  • 5,244
  • 3
  • 40
  • 82
  • https://stackoverflow.com/questions/34489916/how-to-load-external-scripts-dynamically-in-angular/42766146#42766146 – Rahul Kumar May 24 '18 at 17:14

12 Answers12

97

Try to load external JavaScript on component load as below :

loadAPI: Promise<any>;

constructor() {        
    this.loadAPI = new Promise((resolve) => {
        this.loadScript();
        resolve(true);
    });
}

public loadScript() {        
    var isFound = false;
    var scripts = document.getElementsByTagName("script")
    for (var i = 0; i < scripts.length; ++i) {
        if (scripts[i].getAttribute('src') != null && scripts[i].getAttribute('src').includes("loader")) {
            isFound = true;
        }
    }

    if (!isFound) {
        var dynamicScripts = ["https://widgets.skyscanner.net/widget-server/js/loader.js"];

        for (var i = 0; i < dynamicScripts.length; i++) {
            let node = document.createElement('script');
            node.src = dynamicScripts [i];
            node.type = 'text/javascript';
            node.async = false;
            node.charset = 'utf-8';
            document.getElementsByTagName('head')[0].appendChild(node);
        }

    }
}
50

I had the same problem, but in my case, I was importing 10 libraries at the end of the html file, and these libraries have a lot of methods, listeners, events, and more, and in my case I didn't need to call a method specifically.

The example about what I had:

<!-- app.component.html -->

<div> 
 ...
</div>

<script src="http://www.some-library.com/library.js">
<script src="../assets/js/my-library.js"> <!-- a route in my angular project -->

As mentioned, it didn't work. Then, I find somehing that helped me: Milad response

  1. Remove the script calls in the app.component.html. You have to link these scripts in the app.component.ts file.

  2. In ngOnInit(), use a method to append the libraries, for example:

``

<!-- app.component.ts -->

export class AppComponent implements OnInit {
   title = 'app';
   ngOnInit() {
     this.loadScript('http://www.some-library.com/library.js');
     this.loadScript('../assets/js/my-library.js');
   }
  }

  public loadScript(url: string) {
    const body = <HTMLDivElement> document.body;
    const script = document.createElement('script');
    script.innerHTML = '';
    script.src = url;
    script.async = false;
    script.defer = true;
    body.appendChild(script);
  }
}

It functions for me. I use Angular 6, hope it helps.

8

I have done this code snippet

 addJsToElement(src: string): HTMLScriptElement {
    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = src;
    this.elementRef.nativeElement.appendChild(script);
    return script;
  }

And then call it like this

this.addJsToElement('https://widgets.skyscanner.net/widget-server/js/loader.js').onload = () => {
        console.log('SkyScanner Tag loaded');
}

EDIT: With new renderer Api it can be written like this

constructor(private renderer: Renderer2){}

 addJsToElement(src: string): HTMLScriptElement {
    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = src;
    this.renderer.appendChild(document.body, script);
    return script;
  }

StackBlitz

Eduardo Vargas
  • 8,752
  • 2
  • 20
  • 27
7

add loader.js to your assets folder then in your angular-cli.json

"scripts": ["./src/assets/loader.js",]

then add this to your typings.d.ts

 declare var skyscanner:any;

and you will be able to use it

  skyscanner.load("snippets","2");
Hamed Baatour
  • 6,664
  • 3
  • 35
  • 47
  • Does the last line I add (skyscanner.load...) get added in the component? Assuming so, I see an error saying cannot find name skyscanner. – infojolt May 26 '17 at 19:17
  • How would I use this for different build environments? I.e. Load a different script depending on whether it's an integration, test, or production build? – Carlo Bos Feb 21 '20 at 15:20
7

You can create your own directive to load script as below

import { Directive, OnInit, Input } from '@angular/core';

@Directive({
    selector: '[appLoadScript]'
})
export class LoadScriptDirective implements OnInit{

    @Input('script') param:  any;

    ngOnInit() {
        let node = document.createElement('script');
        node.src = this.param;
        node.type = 'text/javascript';
        node.async = false;
        node.charset = 'utf-8';
        document.getElementsByTagName('head')[0].appendChild(node);
    }

}

And you can use it anywhere in your components template as below

<i appLoadScript  [script]="'script_file_path'"></i>

For example, to dynamically load JQuery in your component, insert below code in your component's template

<i appLoadScript  [script]="'/assets/baker/js/jquery.min.js'"></i>
Anil Agrawal
  • 2,748
  • 1
  • 24
  • 31
2

The accepted answer is correct but won't work because browser takes little more time to parse the script after downloading it. So if any variable is being used from the loaded script, then it needs to be used on onload event of the newly created html script element. I have improved the accepted answer as mentioned below -

loadAPI: Promise<any>;

constructor() {
    this.loadAPI = new Promise((resolve) => {
        let node = this.loadScript();
        if (node) {
            node.onload = () => {
                resolve(true);
            };
        } else {
            resolve(true);
        }
    });
}

ngOnInit() {
    this.loadAPI
        .then((flag) => {
        //Do something when script is loaded and parsed by browser
    });
}

loadScript() {
    let node = undefined;
    let isFound = false;
    const scripts = document.getElementsByTagName('script')
    for (let i = 0; i < scripts.length; ++i) {
        // Check if script is already there in html
        if (scripts[i].getAttribute('src') != null && scripts[i].getAttribute('src').includes("loader")) {
          isFound = true;
        }
    }

    if (!isFound) {
        const dynamicScript = 'https://widgets.skyscanner.net/widget-server/js/loader.js';
        node = document.createElement('script');
        node.src = dynamicScript;
        node.type = 'text/javascript';
        node.async = false;
        node.charset = 'utf-8';
        document.getElementsByTagName('head')[0].appendChild(node);
        return node;
    }
    return node;
}
Bikash Gupta
  • 179
  • 3
  • 11
  • Thanks for the script with the change it works great for me. But I have a problem. The script from paypal that I want to include loads another js file which is then searched for at http://127.0.0.1:4200/ (and not found). Is there a way how I can make this secondary loading with require.js work properly? – Gurkenkönig Feb 16 '22 at 16:12
1

You can do one thing

if you have angular-cli.json

then you can declare to script

like

"scripts": ["../src/assets/js/loader.js"]

And then declare skyscanner in your component

like

declare var skyscanner:any;

Thats it!

Hope this help you

Jalay Oza
  • 772
  • 7
  • 11
  • I've added the script locally and included it in the angular-cli.json file. Unfortunately this doesn't work. Please could you explain what adding the declaration into the component is expected to do? – infojolt Jun 01 '17 at 09:50
  • For Angular 8, and I think 7, angular-cli.json is now angular.json and putting an external script in the array crashes the angular boot up. – Preston Sep 06 '19 at 23:04
  • This is the format per the docs and it doesn't crash the app: { "input": "src/external-module/main.js", "inject": false, "bundleName": "external-module" } https://angular.io/guide/workspace-config#styles-and-scripts-configuration – Preston Sep 08 '19 at 19:18
1

Note: This is specially for external js links! Step 1. Add your angular script into the index.html file at the bottom of your body is recommended! I tried all other ways but failed.

<!-- File Name: index.html and its inside src dir-->

<body class="">
  <app-root></app-root>

    <!-- Icons -->
        <script src="https://unpkg.com/feather-icons/dist/feather.min.js"></script>

</body>

Next there are two ways to do this... In Anguar5 use inside your component folder at the top type in this code

declare var feather:any;

and then inside your class call the method you need. For example

//FileName: dashboard.component.ts
import { Component, OnInit } from '@angular/core';
declare var feather:any;
export class DashboardComponent implements OnInit{
    ngOnInit(){
        feather.replace();
    }
}

and this should run your code! The other way which might work in older version. I have not checked!

//FileName: dashboard.component.ts
import { Component, OnInit } from '@angular/core';

export class DashboardComponent implements OnInit{

     ngOnInit(){
    
    
        let node = document.createElement('script');
        node.innerText='feather.replace()';
        node.type = 'text/javascript';
        node.async = false;
        node.charset = 'utf-8';
        
        document.getElementsByTagName('body')[0].appendChild(node);
    
    }

}

If you are not getting my code then also give try to this link too.

Hope this helps!

3rdi
  • 475
  • 4
  • 12
1

Little late but i prefer doing this way (service way)....

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

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

export const ScriptStore: Scripts[] = [
  { name: 'script-a', src: 'assets/js/a.js' },
  { name: 'script-b', src: 'assets/js/b.js' },
  { name: 'script-c', src: 'assets/js/c.js' }
];

declare var document: any;

@Injectable()
export class FileInjectorService {

  private scripts: any = {};

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

  loadJS(...scripts: string[]) {
    const promises: any[] = [];
    scripts.forEach((script) => promises.push(this.loadJSFile(script)));
    return Promise.all(promises);
  }

  loadJSFile(name: string) {
    return new Promise((resolve, reject) => {
      if (!this.scripts[name].loaded) {
        let script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = this.scripts[name].src;
        if (script.readyState) {
            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 {
            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);
      } else {
        resolve({ script: name, loaded: true, status: 'Already Loaded' });
      }
    });
  }

}

Then in my component i could do something like:

ngOnInit() {
  this.fileInjectorService.loadJS('script-a', 'script-c').then(data => {
    // Loaded A and C....
  }).catch(error => console.log(error));
}

Tested in Angular 6/7

0

This is working for me after so many code trials

ngOnInit() {
    this.loadFormAssets().then(() => {console.log("Script Loaded");}).catch(() => {console.log("Script Problem");});
  }

 public loadFormAssets() {
    return new Promise(resolve => {

      const scriptElement = document.createElement('script');
      scriptElement.src =this.urls.todojs;
      scriptElement.onload = resolve;
      document.body.appendChild(scriptElement);

      const scriptElement1 = document.createElement('script');
      scriptElement1.src =this.urls.vendorjs;
      scriptElement1.onload = resolve;
      document.body.appendChild(scriptElement1);

    });
  }
Vivek Pawar
  • 339
  • 3
  • 5
0

In my case I had to load a few different files that depend on one another (some sort of something that uses bootstrap that then uses a jquery plugin that then uses jquery) and they all init immediately on loading, assuming they are loaded in sync on a web page. All the other answers assume that you are loading files that are completely unrelated (or wait for you to init after everything gets loaded) - and with my setup this would throw all kinds of missing variable issues.

My solution was to create a Promise chain (not a Promise list like @carlitoxenlaweb did - which would resolve everything in parallel), so that the next file only gets loaded after the previous file has finished init:

    private myScripts = [
        '/assets/js/jquery-2.2.4.min.js',
        '/assets/js/bootstrap.min.js',
        '/assets/js/jquery.bootstrap.js',
        '/assets/js/jquery.validate.min.js',
        '/assets/js/somescript.js',
    ];
    private loadScripts() {
        let container:HTMLElement = this._el.nativeElement;
        let promise = Promise.resolve();
        for (let url of this.myScripts) {
            promise = promise.then(_ => new Promise((resolve, reject) => {
                let script = document.createElement('script');
                script.innerHTML = '';
                script.src = url;
                script.async = true;
                script.defer = false;
                script.onload = () => { resolve(); }
                script.onerror = (e) => { reject(e); }
                container.appendChild(script);
            }));
        }
    }
Guss
  • 30,470
  • 17
  • 104
  • 128
  • Will this method work even if the file is located in node_modules folder ? – Muhammad Ahsan Jan 08 '21 at 12:51
  • 1
    I'm guessing that will not work - this loading happens on runtime where the `node_modules` folder does not exist. You can copy the relevant files from the module you want to use, but probably you'd better use another method - such as importing the module in a typescript file (that will then be compiled in). – Guss Jan 08 '21 at 17:15
  • Yes, I tried to load from node_modules and it didn't worked. I had no choice but to copy the required things in assets js folder and loaded from that. – Muhammad Ahsan Jan 09 '21 at 18:06
0

Since the source URL allows you to invoke a global function, you can use that to setup a custom event handler.

index.html

<script 
  type="text/javascript"
  src="http://www.bing.com/api/maps/mapcontrol?callback=onBingLoaded&branch=release"
  async defer
></script>
<script>
  function onBingLoaded() {
    const event = new CustomEvent("bingLoaded");
    window.dispatchEvent(event);
  }
</script>

Now that we've dispatched our custom event to the window object, we can listen for it using the decorator @HostListener Angular provides in our component.

app.component.ts

export class AppComponent {
  @ViewChild('mapCanvas')
  mapCanvas!: ElementRef;
  private map!: Microsoft.Maps.Map;

  @HostListener('window:bingLoaded', ['$event'])
  defineMapCanvas() {
    this.map = new Microsoft.Maps.Map(
      this.mapCanvas.nativeElement,
      {
        credentials: [YOUR API KEY HERE],
        ...other options
      }
    );
  }

Reference: https://angular.io/api/core/HostListener

Joshua McCarthy
  • 1,739
  • 1
  • 9
  • 6