223

I have this module which componentize the external library together with additional logic without adding the <script> tag directly into the index.html:

import 'http://external.com/path/file.js'
//import '../js/file.js'

@Component({
    selector: 'my-app',
    template: `
        <script src="http://iknow.com/this/does/not/work/either/file.js"></script>
        <div>Template</div>`
})
export class MyAppComponent {...}

I notice the import by ES6 spec is static and resolved during TypeScript transpiling rather than at runtime.

Anyway to make it configurable so the file.js will be loading either from CDN or local folder? How to tell Angular 2 to load a script dynamically?

Mamun
  • 66,969
  • 9
  • 47
  • 59
CallMeLaNN
  • 8,328
  • 7
  • 59
  • 74
  • Possible duplicate of [ES6 variable import name in node.js?](http://stackoverflow.com/questions/29168433/es6-variable-import-name-in-node-js) – Felix Kling Dec 28 '15 at 11:51
  • If you are working on the latest version of Angular, a more recent and up-to-date implementation can be found here https://www.htmlgoodies.com/javascript/loading-external-scripts-dynamically-in-angular/ – Kevy Granero Mar 30 '23 at 10:09

23 Answers23

204

You can use following technique to dynamically load JS scripts and libraries on demand in your Angular project.

script.store.ts 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'}
];

script.service.ts is an injectable service that will handle the loading of script, copy script.service.ts as it is

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 this ScriptService wherever you need it and load js libs like this

this.script.load('filepicker', 'rangeSlider').then(data => {
    console.log('script loaded ', data);
}).catch(error => console.log(error));
isherwood
  • 58,414
  • 16
  • 114
  • 157
Rahul Kumar
  • 5,120
  • 5
  • 33
  • 44
  • 12
    This works brilliantly. Shaves over 1MB off initial load size - I have a lot of libraries! – Our_Benefactors Jun 01 '17 at 20:41
  • 6
    Looks like I spoke too soon. This solution does NOT work in iOS Safari. – Our_Benefactors Jun 09 '17 at 18:24
  • after loading the script check whether it's appending to head of your dom – Rahul Kumar Jun 10 '17 at 05:06
  • Perhaps I'm not injecting correctly, but I'm getting `this.script.load is not a function` – Towelie Jun 16 '18 at 02:24
  • Thanks. This works, but I am getting my script (a background animation) loaded in every other component. I want it to be loaded only on my first component (it is a login page). What can I do to achieve that? Thanks – Joel Azevedo Aug 21 '18 at 08:53
  • I'm trying load dependent scripts for FlowPlayer with this method (flowplayer, flowplayer.hlsjs, flowplayer.hlsjs.light) but seems I'm having console errors about the first one, from second and third (`flowplayer is not defined`). Any ideas about that? Thanks! – Cesar Sep 18 '18 at 20:48
  • Check if it getting appended to the head? – Rahul Kumar Sep 19 '18 at 04:36
  • It doesn't add in order. – SaamTehraani Oct 11 '18 at 03:04
  • It doesn't scripts add in order because the code is using Promise.all(promises) for loading the scripts, you can modify this section of the code to resolve all promises sequentially – Rahul Kumar Oct 11 '18 at 06:00
  • Are you injecting script service properly – Rahul Kumar Jan 15 '19 at 14:52
  • @mirypoko I believe that I went with a simpler solution [here](https://stackoverflow.com/questions/34489916/how-to-load-external-scripts-dynamically-in-angular/42766146?noredirect=1#answer-49981918) Hope this helps – Towelie Jan 15 '19 at 22:51
  • @RahulKumar, I have added one `Script` in `ScriptStore`. I have implemented as you said. But when I call `this.scriptService.load('scriptName');` then it gets called twice before `script.onload()` called for first call. I have checked that in `private scripts: any = {};` variable of `ScriptService`, I have only one `Script` entry. – Junaid Feb 27 '19 at 08:43
  • What appenned if you change route and component? do scripts remain in head tag? if the answer is YES, it would not be really correct. is there a way to remove scripts from the head if i am not in the specific component? – HouseFragance May 27 '19 at 10:08
  • hi Rahul, thanks for the reply. In your opinion, how should I remove that script from the tag? Where i have to do this check? I wouldn't know how to do it – HouseFragance May 28 '19 at 07:58
  • You can remove the script from of the document on ngOnDestroy() event of the component. – Rahul Kumar May 29 '19 at 05:46
  • 2
    How do you call function that are in the js file? are they attached to the global window? – pdiddy Sep 23 '19 at 12:47
  • 1
    I modified your load function for sequential loading. Here is what I did. ```load(...scripts: string[]) {let promise = Promise.resolve(); scripts.forEach(script => { promise = promise.then(() => this.loadScript(script)).then(res => console.log(res));});}``` Thank you for your answer. – Willie Jul 29 '20 at 18:45
  • The question now is how do you call the functions loaded in the JS file? – James Ikubi Dec 18 '20 at 15:26
  • I need to call a script with some variables values, like an id and a token. How can I do that? – Suyog Jun 02 '21 at 07:20
  • how to call the loaded function ? – xzeemo Jun 09 '22 at 12:10
  • Thank you! Works very well for appending js files for micro frontends. – Gokce Akcan Feb 03 '23 at 14:09
73

If you're using system.js, you can use System.import() at runtime:

export class MyAppComponent {
  constructor(){
    System.import('path/to/your/module').then(refToLoadedModule => {
      refToLoadedModule.someFunction();
    }
  );
}

If you're using webpack, you can take full advantage of its robust code splitting support with require.ensure :

export class MyAppComponent {
  constructor() {
     require.ensure(['path/to/your/module'], require => {
        let yourModule = require('path/to/your/module');
        yourModule.someFunction();
     }); 
  }
}
Bruno Bruzzano
  • 477
  • 7
  • 21
drew moore
  • 31,565
  • 17
  • 75
  • 112
  • 1
    It seems like I need to use module loader specifically in the component. Well, that's fine since `System` is the future of loader, I think. – CallMeLaNN Dec 28 '15 at 10:31
  • @CallMeLaNN bingo :-) – drew moore Dec 29 '15 at 02:31
  • 6
    `TypeScript Plugin for Sublime Text` does not happy with `System` in the TypeScript code: `Cannot find name 'System'` but no error during transpiling and running. Both `Angular2` and `System` script file already added into `index.html`. Anyway to `import` the `System` and make the plugin happy? – CallMeLaNN Dec 29 '15 at 05:34
  • 1
    but what if you are not using system.js! – Murhaf Sousli Jan 09 '16 at 02:26
  • It will work, but your console will be overcrowded with errors. – Viktor Apr 15 '16 at 06:49
  • @MurhafSousli if you're using webpack, see edit. if you have a better solution, please post it! – drew moore Apr 15 '16 at 08:31
  • 2
    @drewmoore I don't remember why I said so, `require()`/`import` should work fine as your answer now, +1 – Murhaf Sousli Apr 15 '16 at 08:37
  • and if you don't use any of those? What's the old school's way? – Saeed Neamati Apr 25 '17 at 17:10
  • 22
    From what I can tell these options don't work for scripts on the internet, just for local files. This doesn't seem to answer the original question as asked. – WillyC May 02 '17 at 21:26
  • Hello. Should I import something in my component to be able to use System.import or require.ensure? I've tried to use them and both rise errors because my component doesn't recognize System nor require.ensure. Thanks in advance! – Thisisalexis Apr 02 '18 at 17:55
  • 1
    @Thisisalexis Yes, not sure how this answer got accepted. see the answer below with renderer2 – mchl18 Dec 17 '20 at 16:06
  • how to call the loaded function please? – xzeemo Jun 09 '22 at 12:10
66

This might work. This Code dynamically appends the <script> tag to the head of the html file on button clicked.

const url = 'http://iknow.com/this/does/not/work/either/file.js';

export class MyAppComponent {
    loadAPI: Promise<any>;

    public buttonClicked() {
        this.loadAPI = new Promise((resolve) => {
            console.log('resolving promise...');
            this.loadScript();
        });
    }

    public loadScript() {
        console.log('preparing to load...')
        let node = document.createElement('script');
        node.src = url;
        node.type = 'text/javascript';
        node.async = true;
        node.charset = 'utf-8';
        document.getElementsByTagName('head')[0].appendChild(node);
    }
}
Rahul Kumar
  • 5,120
  • 5
  • 33
  • 44
ng-darren
  • 1,022
  • 7
  • 11
  • 2
    Thanx man Worked for me. You can now load it on page load also using ngonit method. So button click is not required. – Niraj Sep 19 '16 at 12:11
  • I am using the same way.. but not worked for me . can u please help me out for the same ?? this.loadJs("javascriptfile path"); public loadjs(file) { let node = document.createElement('script'); node.src = file; node.type = 'text/javascript'; node.async = true; node.charset = 'utf-8'; document.getElementsByTagName('head')[0].appendChild(node); } – Mitesh Shah Oct 22 '16 at 08:14
  • Are you trying to do it on click or on page load? Could you tell us more about the result or error? – ng-darren Oct 25 '16 at 00:48
  • hello, I have tried to implement this and working fine but `new Promise` is not resolved. Can you tell me for `Promise` shall I have to import anything ? – user3145373 ツ Dec 15 '16 at 05:23
  • Hi, I didn't import anything special when I use Promise. – ng-darren Dec 16 '16 at 07:07
  • Hello @ng-darren, It worked well but do you have any idea how I can use PRODUCTION variable used in project's environment constant. Environment variable is exported as export const Environment = { PRODUCTION: false } Also, I am using this dynamically imported javascript only on some route's of project. – Akash Jain Nov 21 '17 at 10:13
  • I used this technique, it worked at first, but when I need to load multiple js libs, in order, then its showing some problem. It shows all libs in the same order in browser console but the files that comes later in the order doesn't get the reference of its previous files. I used `window.setTimeout( ... , 50)` for my made scripts but for js libs its difficult !!! – Ankur Shah Nov 23 '17 at 10:00
  • I get this error -> GET http://iknow.com/this/does/not/work/either/file.js 403 (Forbidden) – Techdive Nov 27 '18 at 08:09
  • that works for me but seems that scripts are loaded always async even set async=false.that cause some loading problems – mrapi May 18 '19 at 09:47
  • I tried wraping all of this in a `if(!window.library)`. Somehow, the condition gets always failed even if the library is loaded once – Saurabh Tiwari Aug 27 '21 at 18:19
36

Yet another option would be to utilize scriptjs package for that matter which

allows you to load script resources on-demand from any URL

Example

Install the package:

npm i scriptjs

and type definitions for scriptjs:

npm install --save @types/scriptjs

Then import $script.get() method:

import { get } from 'scriptjs';

and finally load script resource, in our case Google Maps library:

export class AppComponent implements OnInit {
  ngOnInit() {
    get("https://maps.googleapis.com/maps/api/js?key=", () => {
        //Google Maps library has been loaded...
    });
  }
}

Demo

Vadim Gremyachev
  • 57,952
  • 20
  • 129
  • 193
  • 4
    Great, but instead of npm install --save @types/scriptjs, it should be npm install --save-dev @types/scriptjs (or just npm i -D @types/scriptjs) – Youness Houdass Jun 29 '20 at 07:27
  • segment makes a bunch of function calls after the script is loaded. how would we achieve that with this example? i would need access to the `analytics` or `script` object I guess that it places globally? – fIwJlxSzApHEZIl Mar 01 '22 at 20:14
  • also this broke my google maps component when I switched to this method. i'm assuming it's because the google maps component loaded before `scriptjs` was able to resolve it. probably because `scriptjs` runs in `ngOnInit(` which is after the google maps component has already loaded. This is most likely an issue with my implementation of the google maps component not being asynchronous but just wanted to throw that out there. Script loading and asynchronous javascript suck. – fIwJlxSzApHEZIl Mar 01 '22 at 20:18
  • @fIwJlxSzApHEZIl did you solve it at the end? – Lukaesch Aug 01 '22 at 18:43
  • @fIwJlxSzApHEZIl were you able solve it at the end? Please respond – Rahul Aug 08 '23 at 13:01
33

I have modified @rahul kumars 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
}
Joel
  • 8,502
  • 11
  • 66
  • 115
  • 1
    You forget set the `async` to `true`. – Will Huang Jun 03 '17 at 16:34
  • I have a question, when we append the script and navigate between routes, lets say I loaded script on home and browse to about and returned back to home, is there a way to reinit the current script ? Means I'd like to remove previously loaded one and load it again? Thank you very much – d123546 Jun 16 '17 at 09:40
  • @d123546 Yes, if you'd want that it is possible. But you'd have to write some logic that removes the script tag. Have a look here: https://stackoverflow.com/questions/14708189/remove-specific-script-tag-in-head-tag-by-onclick-event – Joel Jun 16 '17 at 09:42
  • Thanks Joel, but is there a way to somehow remove the functions initialized by the script ? Currently Im facing a problem in my angular4 project, because Im loading script which initialize jquery slider, so when the first route loads it displayed properly, but then when I navigate to other route and return back to my route, script is loaded twice and slider is not loading anymore :( Could you advice me on this please – d123546 Jun 16 '17 at 10:18
  • @d123546 can't really help you, sorry. The code here should not load the same script twice. That is the purpose of saving the loaded scripts in the `scripts` array – Joel Jun 16 '17 at 11:56
  • The thing is if I load it and navigate it stops working , I need to initialize my scripts written in jQuery again – d123546 Jun 16 '17 at 12:52
  • 2
    There is an error in the line `private scripts: {ScriptModel}[] = [];`. It should be `private scripts: ScriptModel[] = [];` – Chris Haines Jan 28 '19 at 16:11
  • 1
    @d123546 you should initialize the service only once. Use ``@Injectable({providedIn: 'root'})`` and add the service **only** into the ``providers:[]`` array of *app.module.ts* – Jiyan Akgül Jan 05 '21 at 10:18
29

You can load multiple scripts dynamically like this in your component.ts file:

 loadScripts() {
    const dynamicScripts = [
     'https://platform.twitter.com/widgets.js',
     '../../../assets/js/dummyjs.min.js'
    ];
    for (let i = 0; i < dynamicScripts.length; i++) {
      const 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);
    }
  }

and call this method inside the constructor,

constructor() {
    this.loadScripts();
}

Note : For more scripts to be loaded dynamically, add them to dynamicScripts array.

Aswin Sanakan
  • 655
  • 8
  • 10
  • 3
    I have to say, this looks simple but works well, very easy to implement :-) – Pierre Jun 07 '18 at 14:01
  • 4
    The problem with this type of script injection is that since Angular is a SPA, this scripts basically stays on the browser cache even after you remove the scripts tag from the DOM. – j4rey Dec 13 '18 at 07:49
  • 1
    Awesome answer ! Saved my day, It works with Angular 6 & 7. Tested ! – Nadeeshan Herath Feb 13 '19 at 12:35
  • To send data to external js and and get data from external js , Call this function at app.component.ts and use declare var d3Sankey:any at any feature level module's component. It's working. – gnganapath Nov 12 '19 at 19:49
  • This worked for me with Angular 13. Thank you very much! – Richard K. Campion Feb 16 '22 at 00:00
22

Hi you can use Renderer2 and elementRef with just a few lines of code:

constructor(private readonly elementRef: ElementRef,
          private renderer: Renderer2) {
}
ngOnInit() {
 const script = this.renderer.createElement('script');
 script.src = 'http://iknow.com/this/does/not/work/either/file.js';
 script.onload = () => {
   console.log('script loaded');
   initFile();
 };
 this.renderer.appendChild(this.elementRef.nativeElement, script);
}

the onload function can be used to call the script functions after the script is loaded, this is very useful if you have to do the calls in the ngOnInit()

Hetdev
  • 1,425
  • 2
  • 21
  • 29
9

I have done this code snippet with the new renderer api

 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;
  }

And then call it like this

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

StackBlitz

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

I have a good way to dynamically load scripts! Now I use ng6, echarts4 (>700Kb ) ,ngx-echarts3 in my project. when I use them by ngx-echarts's docs, I need import echarts in angular.json : "scripts":["./node_modules/echarts/dist/echarts.min.js"] thus in the login module, page while loading scripts.js, this is big file! I don't want it.

So, I think angular loads each module as a file, I can insert a router resolver to preload js, then begin the module loading!

// PreloadScriptResolver.service.js

/**动态加载js的服务 */
@Injectable({
  providedIn: 'root'
})
export class PreloadScriptResolver implements Resolve<IPreloadScriptResult[]> {
  // Here import all dynamically js file
  private scripts: any = {
    echarts: { loaded: false, src: "assets/lib/echarts.min.js" }
  };
  constructor() { }
  load(...scripts: string[]) {
    const promises = scripts.map(script => this.loadScript(script));
    return Promise.all(promises);
  }
  loadScript(name: string): Promise<IPreloadScriptResult> {
    return new Promise((resolve, reject) => {
      if (this.scripts[name].loaded) {
        resolve({ script: name, loaded: true, status: 'Already Loaded' });
      } else {
        const script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = this.scripts[name].src;
        script.onload = () => {
          this.scripts[name].loaded = true;
          resolve({ script: name, loaded: true, status: 'Loaded' });
        };
        script.onerror = (error: any) => reject({ script: name, loaded: false, status: 'Loaded Error:' + error.toString() });
        document.head.appendChild(script);
      }
    });
  }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<IPreloadScriptResult[]> {
   return this.load(...route.routeConfig.data.preloadScripts);
  }
}

Then in the submodule-routing.module.ts ,import this PreloadScriptResolver:

const routes: Routes = [
  {
    path: "",
    component: DashboardComponent,
    canActivate: [AuthGuardService],
    canActivateChild: [AuthGuardService],
    resolve: {
      preloadScripts: PreloadScriptResolver
    },
    data: {
      preloadScripts: ["echarts"]  // important!
    },
    children: [.....]
}

This code works well, and its promises that: After js file loaded, then module begin load! this Resolver can use in many routers

Andrew Brēza
  • 7,705
  • 3
  • 34
  • 40
申君健
  • 81
  • 1
  • 1
5

An Angular universal solution; I needed to wait for a particular element to be on the page before loading a script to play a video.

import {Inject, Injectable, PLATFORM_ID} from '@angular/core';
import {isPlatformBrowser} from "@angular/common";

@Injectable({
  providedIn: 'root'
})
export class ScriptLoaderService {

  constructor(
    @Inject(PLATFORM_ID) private platformId: Object,
  ) {
  }

  load(scriptUrl: string) {
    if (isPlatformBrowser(this.platformId)) {
      let node: any = document.createElement('script');
      node.src = scriptUrl;
      node.type = 'text/javascript';
      node.async = true;
      node.charset = 'utf-8';
      document.getElementsByTagName('head')[0].appendChild(node);
    }
  }
}
grg
  • 5,023
  • 3
  • 34
  • 50
Rusty Rob
  • 16,489
  • 8
  • 100
  • 116
3

In my case, I've loaded both the js and css visjs files using the above technique - which works great. I call the new function from ngOnInit()

Note: I could not get it to load by simply adding a <script> and <link> tag to the html template file.

loadVisJsScript() {
  console.log('Loading visjs js/css files...');
  let script = document.createElement('script');
  script.src = "../../assets/vis/vis.min.js";
  script.type = 'text/javascript';
  script.async = true;
  script.charset = 'utf-8';
  document.getElementsByTagName('head')[0].appendChild(script);    
 
  let link = document.createElement("link");
  link.type = "stylesheet";
  link.href = "../../assets/vis/vis.min.css";
  document.getElementsByTagName('head')[0].appendChild(link);    
}
bob.mazzo
  • 5,183
  • 23
  • 80
  • 149
  • I want to load the CSS file from an external web source. Currently, i am facing the error saying "Not allowed to load local resource".. Please suggest. – Karan Jun 19 '18 at 06:29
  • I tried this way. Despite it's working well for Scripts files, it doesn't work for Stylesheets – Kr1 Feb 06 '19 at 11:22
3

for those of you who would like to load styles dynamically too. (based on @Rahul Kumar brilliant answer)

script.store.ts

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

export const StyleStore: Scripts[] = [
    { name: 'fancybox-css', src: 'https://cdn.jsdelivr.net/gh/fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.css' }
];

export const ScriptStore: Scripts[] = [
    { name: 'jquery', src: 'https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js' },
    { name: 'other', src: '[other script source]'}
];

script-loader.service.ts

import { Injectable } from '@angular/core';
import { ScriptStore, StyleStore } from '../../stores/script.store';

@Injectable({
  providedIn: 'root'
})
export class ScriptLoaderService {

  private scripts: any = {};
  private styles: any = {};

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

    StyleStore.forEach((script: any) => {
      this.styles[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);
  }

  loadStyles(...scripts: string[]) {
    var promises: any[] = [];
    scripts.forEach((script) => promises.push(this.loadStyle(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') as any;
        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);
      }
    });
  }

  loadStyle(name: string) {
    return new Promise((resolve, reject) => {
      //resolve if already loaded
      if (this.styles[name].loaded) {
        resolve({ script: name, loaded: true, status: 'Already Loaded' });
      }
      else {
        //load style
        let style = document.createElement('link') as any;
        style.type = "text/css";
        style.rel = "stylesheet";
        style.href = this.styles[name].src;
        if (style.readyState) {  //IE
          style.onreadystatechange = () => {
            if (style.readyState === "loaded" || style.readyState === "complete") {
              style.onreadystatechange = null;
              this.styles[name].loaded = true;
              resolve({ style: name, loaded: true, status: 'Loaded' });
            }
          };
        } else {  //Others
          style.onload = () => {
            this.styles[name].loaded = true;
            resolve({ style: name, loaded: true, status: 'Loaded' });
          };
        }
        style.onerror = (error: any) => resolve({ style: name, loaded: false, status: 'Loaded' });
        document.getElementsByTagName('head')[0].appendChild(style);
      }
    });
  }

}

app.component.ts

constructor(private scriptLoaderService: ScriptLoaderService) {
  this.scriptLoaderService.loadStyles('fancybox-css').then(x => {
    this.scriptLoaderService.load('jquery', 'fancybox').then(data => {
    }).catch(error => console.log(error));
  });
}
F.H.
  • 1,456
  • 1
  • 20
  • 34
2

@rahul-kumar 's solution works good for me, but i wanted to call my javascript function in my typescript

foo.myFunctions() // works in browser console, but foo can't be used in typescript file

I fixed it by declaring it in my typescript :

import { Component } from '@angular/core';
import { ScriptService } from './script.service';
declare var foo;

And now, i can call foo anywhere in my typecript file

Z3nk
  • 365
  • 4
  • 17
  • if i import multiple scripts foo,foo1,foo2 and i know only during the time of execution how to do the declaration dynamically? – natrayan Aug 17 '19 at 01:23
2

I had the same issue for the below link.I solved it in a very easy way.

https://www.gstatic.com/charts/loader.js

I needed to access the google variable in the below code. But it didn't work when I just put it in the angular class.

google.charts.load("current", {packages:['corechart']});
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
    var data = google.visualization.arrayToDataTable([
        ["Element", "Density", { role: "style" } ],
        ["Copper", 8.94, "dodgerblue"],
        ["Silver", 10.49, "dodgerblue"],
        ["Gold", 19.30, "dodgerblue"],
        ["Platinum", 21.45, "color: dodgerblue"]
    ]);
var view = new google.visualization.DataView(data);
view.setColumns([0, 1,
    { calc: "stringify",
        sourceColumn: 1,
        type: "string",
        role: "annotation" },
    2]);

var options = {
    title: "Density of Precious Metals, in g/cm^3",
    width: 600,
    height: 400,
    bar: {groupWidth: "50%"},
    legend: { position: "none" },
};
var chart = new google.visualization.ColumnChart(document.getElementById("columnchart_values"));
chart.draw(view, options);

}

I create a global variable on top of the ts class with the same name(google) and then that variable refer to the required one automatically.(because it is global scope) Then the problem is solved.

declare var google: any;
2

I wanted to be able to:

  • Add a script when the app is being bootstrapped
  • Not do it from a component, because it doesn't feel like it's any component's responsibility
  • Not do it from a directive, because of the same reason as the component
  • Not do it from a service, because unless there's some kind of heavy logic related to an existing service, this doesn't belong IMO to a service
  • Avoid doing it in a module. A module could be fine but it's not as flexible as just using DI and since Angular 15 standalone components are stable so why bother with a module

That said, in order to do that even before the app is bootstrapped, it's a bit tricky. Because we don't have a renderer available at that stage AND we don't have access to an elementRef containing a nativeElement.

So here's my take on it:

export const YOUR_EXT_LIB_URL_TOKEN = new InjectionToken<string>('YOUR_EXT_LIB_URL_TOKEN');

export const YOUR_SETUP: Provider = {
  provide: APP_INITIALIZER,
  multi: true,
  useFactory: (
    doc: InjectionTokenType<typeof DOCUMENT>,
    rendererFactory: RendererFactory2,
    yourExternalLibToken: string,
  ) => {
    const renderer = rendererFactory.createRenderer(null, null);

    const script = renderer.createElement('script');
    script.type = 'text/javascript';
    script.src = yourExternalLibToken;
    renderer.appendChild(doc.body, script);

    return () => true;
  },
  deps: [DOCUMENT, RendererFactory2, YOUR_EXT_LIB_URL_TOKEN],
};

Then all you have to do is provide YOUR_EXT_LIB_URL_TOKEN and pass the YOUR_SETUP provider as well.

This way, everything is injected through DI and is super flexible. For example you could provide the YOUR_SETUP token in a shared library, and provide YOUR_EXT_LIB_URL_TOKEN in different apps that use the shared library.

maxime1992
  • 22,502
  • 10
  • 80
  • 121
1

@d123546 I faced the same issue and got it working now using ngAfterContentInit (Lifecycle Hook) in the component like this :

import { Component, OnInit, AfterContentInit } from '@angular/core';
import { Router } from '@angular/router';
import { ScriptService } from '../../script.service';

@Component({
    selector: 'app-players-list',
    templateUrl: './players-list.component.html',
    styleUrls: ['./players-list.component.css'],
    providers: [ ScriptService ]
})
export class PlayersListComponent implements OnInit, AfterContentInit {

constructor(private router: Router, private script: ScriptService) {
}

ngOnInit() {
}

ngAfterContentInit() {
    this.script.load('filepicker', 'rangeSlider').then(data => {
    console.log('script loaded ', data);
    }).catch(error => console.log(error));
}
Waseem
  • 19
  • 3
1

This solution worked for me :

1 ) create a new class named URLLoader

export class URLLoader {
  constructor() {

  }

  loadScripts() {

    const dynamicScripts = [
      'URL 1',
      'URL 2',
      'URL n'
    ];

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

}

2 ) extend the class URLLoader and invoke loadScripts method from the component class

export class AppComponent extends URLLoader implements OnInit {  

   constructor(){}

   ngOnInit() {
   super.loadScripts();
   }

}
Anis KCHAOU
  • 830
  • 1
  • 11
  • 11
1

I find this solution much cleaner, first import HttpClientJsonpModule in your module and then do something like this

this.apiLoaded = this.httpClient.jsonp(environment.AnyApiUrl, 'callback')
  .pipe(
    map(() => true),
    catchError(() => of(false)),
  );

in your template:

<app-component *ngIf="apiLoaded | async"></app-component>

This solution is in official Angular Google Maps' docs here.

azerafati
  • 18,215
  • 7
  • 67
  • 72
  • Beware: it will yield error if your API doesn't call callback, i.e. it's a static script instead of a valid JSONP. – izogfif Mar 27 '23 at 18:45
1

Angular has the logic to prevent users directly interfere with the html output. So you have to let Angular to inject the tag by giving that direction in angular.json file.

First, you have to get the script file. There are two ways:

  1. Download the script file (eg. somelibrary.js)
  • place it on the assets folder
  • put the script's relative path, into the "scripts" section of the angular.json file:
"scripts": [
  "src/assets/somelibrary.js"
]
  1. Install the script with npm/yarn:
  • put the script's relative path, into the "scripts" section of the angular.json file:
"scripts": [
  "./node_modules/somelibrary/dist/somelibrary.min.js"
]
otsili
  • 81
  • 1
  • 4
0

a sample can be

script-loader.service.ts file

import {Injectable} from '@angular/core';
import * as $ from 'jquery';

declare let document: any;

interface Script {
  src: string;
  loaded: boolean;
}

@Injectable()
export class ScriptLoaderService {
public _scripts: Script[] = [];

/**
* @deprecated
* @param tag
* @param {string} scripts
* @returns {Promise<any[]>}
*/
load(tag, ...scripts: string[]) {
scripts.forEach((src: string) => {
  if (!this._scripts[src]) {
    this._scripts[src] = {src: src, loaded: false};
  }
});

let promises: any[] = [];
scripts.forEach((src) => promises.push(this.loadScript(tag, src)));

return Promise.all(promises);
}

 /**
 * Lazy load list of scripts
 * @param tag
 * @param scripts
 * @param loadOnce
 * @returns {Promise<any[]>}
 */
loadScripts(tag, scripts, loadOnce?: boolean) {
loadOnce = loadOnce || false;

scripts.forEach((script: string) => {
  if (!this._scripts[script]) {
    this._scripts[script] = {src: script, loaded: false};
  }
});

let promises: any[] = [];
scripts.forEach(
    (script) => promises.push(this.loadScript(tag, script, loadOnce)));

return Promise.all(promises);
}

/**
 * Lazy load a single script
 * @param tag
 * @param {string} src
 * @param loadOnce
 * @returns {Promise<any>}
 */
loadScript(tag, src: string, loadOnce?: boolean) {
loadOnce = loadOnce || false;

if (!this._scripts[src]) {
  this._scripts[src] = {src: src, loaded: false};
}

return new Promise((resolve, reject) => {
  // resolve if already loaded
  if (this._scripts[src].loaded && loadOnce) {
    resolve({src: src, loaded: true});
  }
  else {
    // load script tag
    let scriptTag = $('<script/>').
        attr('type', 'text/javascript').
        attr('src', this._scripts[src].src);

    $(tag).append(scriptTag);

    this._scripts[src] = {src: src, loaded: true};
    resolve({src: src, loaded: true});
  }
 });
 }
 }

and usage

first inject

  constructor(
  private _script: ScriptLoaderService) {
  }

then

ngAfterViewInit()  {
this._script.loadScripts('app-wizard-wizard-3',
['assets/demo/default/custom/crud/wizard/wizard.js']);

}

or

    this._script.loadScripts('body', [
  'assets/vendors/base/vendors.bundle.js',
  'assets/demo/default/base/scripts.bundle.js'], true).then(() => {
  Helpers.setLoading(false);
  this.handleFormSwitch();
  this.handleSignInFormSubmit();
  this.handleSignUpFormSubmit();
  this.handleForgetPasswordFormSubmit();
});
unos baghaii
  • 2,539
  • 4
  • 25
  • 42
0
import { Injectable } from '@angular/core';
import * as $ from 'jquery';

interface Script {
    src: string;
    loaded: boolean;
}

@Injectable()
export class ScriptLoaderService {
    public _scripts: Script[] = [];

    /**
     * @deprecated
     * @param tag
     * @param {string} scripts
     * @returns {Promise<any[]>}
     */
    load(tag, ...scripts: string[]) {
        scripts.forEach((src: string) => {
            if (!this._scripts[src]) {
                this._scripts[src] = { src: src, loaded: false };
            }
        });

        const promises: any[] = [];
        scripts.forEach(src => promises.push(this.loadScript(tag, src)));

        return Promise.all(promises);
    }

    /**
     * Lazy load list of scripts
     * @param tag
     * @param scripts
     * @param loadOnce
     * @returns {Promise<any[]>}
     */
    loadScripts(tag, scripts, loadOnce?: boolean) {
        debugger;
        loadOnce = loadOnce || false;

        scripts.forEach((script: string) => {
            if (!this._scripts[script]) {
                this._scripts[script] = { src: script, loaded: false };
            }
        });

        const promises: any[] = [];
        scripts.forEach(script => promises.push(this.loadScript(tag, script, loadOnce)));

        return Promise.all(promises);
    }

    /**
     * Lazy load a single script
     * @param tag
     * @param {string} src
     * @param loadOnce
     * @returns {Promise<any>}
     */
    loadScript(tag, src: string, loadOnce?: boolean) {
        debugger;
        loadOnce = loadOnce || false;

        if (!this._scripts[src]) {
            this._scripts[src] = { src: src, loaded: false };
        }

        return new Promise((resolve, _reject) => {
            // resolve if already loaded
            if (this._scripts[src].loaded && loadOnce) {
                resolve({ src: src, loaded: true });
            } else {
                // load script tag
                const scriptTag = $('<script/>')
                    .attr('type', 'text/javascript')
                    .attr('src', this._scripts[src].src);

                $(tag).append(scriptTag);

                this._scripts[src] = { src: src, loaded: true };
                resolve({ src: src, loaded: true });
            }
        });
    }

    reloadOnSessionChange() {
        window.addEventListener('storage', function(data) {
            if (data['key'] === 'token' && data['oldValue'] == null && data['newValue']) {
                document.location.reload();
            }
        });
    }
}
Aniket
  • 552
  • 4
  • 13
0

You can dynamically load scripts in a component using the Renderer2 class from @angular/core.

import { Component, Renderer2 } from '@angular/core';

@Component({
  selector: 'app-root',
  template: '<p>Dynamic script loading example</p>',
})
export class AppComponent {
  constructor(private renderer: Renderer2) {}

  ngOnInit() {
    const script = this.renderer.createElement('script');
    this.renderer.setAttribute(script, 'src', 'https://example.com/script.js');
    this.renderer.appendChild(document.head, script);
  }
}
Abhishek Pandey
  • 349
  • 3
  • 12
-3

You can use Google Tag Manager to manage your external scripts without going into code. That s a perfect solution for non-tech users and tech users.

Satyam Dorville
  • 317
  • 2
  • 10