Loading an external javascript file dynamically within an Angular component

Currently, I'm in the process of developing an Angular application with Angular 4 and CLI. One of my challenges is integrating the SkyScanner search widget into a specific component.

For reference, you can check out this Skyscanner Widget Example.

To make this work, I need to include an external script:

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

My dilemma lies in how to properly reference this file. Placing it directly into my index.html causes the widget not to load unless I do a full page refresh. It seems like the script's manipulation of the DOM during loading doesn't find the necessary elements on time.

So, the question now is: What is the appropriate way to load the script only when the component containing the Skyscanner widget is loaded?

Answer №1

One way to handle loading external JavaScript when a component loads is demonstrated in the following code snippet:

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

    }
}

Answer №2

I encountered a similar issue where I was importing multiple libraries at the end of my HTML file, each containing numerous methods, listeners, events, and more. In my case, I did not need to call a specific method.

Here is an example of 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 -->

Unfortunately, this approach did not work for me. However, I found a solution that proved to be helpful: Milad's response

  1. Remove the script calls from the app.component.html and link these scripts in the app.component.ts file instead.

  2. In ngOnInit(), utilize a method to append the libraries like so:

``

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

This solution worked for me with Angular 6. Hopefully, it proves helpful to others facing similar issues.

Answer №3

If you want to load a script dynamically in Angular, you can create your own custom directive like this:

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

}

You can then use this directive in any component template by adding the following code:

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

For instance, if you need to load JQuery dynamically, include the below snippet in your component's template:

<i appLoadScript  [script]="'/assets/baker/js/jquery.min.js'"></i>

Answer №4

I successfully implemented 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;
  }

Following that, I executed it in the following manner

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

UPDATE: Utilizing the new renderer Api, it can be modified as shown below

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

Check out on StackBlitz

Answer №5

Firstly, make sure to include loader.js in your assets directory. Next, open your angular-cli.json file and add the following line under "scripts":

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

After that, you need to declare the variable skyscanner in your typings.d.ts file like this:

declare var skyscanner:any;

Once you've done that, you can start using it by calling the function:

skyscanner.load("snippets","2");

Answer №6

The provided solution is accurate, but it may not be effective due to the browser requiring extra time to parse the script after downloading it. In such cases, if a variable is being utilized from the loaded script, it should be accessed within the `onload` event of the newly created HTML script element. Below, you can find an enhanced version of the original answer:

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) => {
        //Perform actions once the script has been loaded and parsed by the browser
    });
}

loadScript() {
    let node = undefined;
    let isFound = false;
    const scripts = document.getElementsByTagName('script')
    for (let i = 0; i < scripts.length; ++i) {
        // Check if the script already exists in the 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;
}

Answer №7

Although it's a little late, I personally prefer approaching it this way (the 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 can implement something like this:

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

This code has been tested on Angular versions 6 and 7.

Answer №8

If you have the angular-cli.json file, there is a simple solution to declare a script. Add the following line to your angular-cli.json file:

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

Next, make sure you declare skyscanner in your component like this:

declare var skyscanner:any;

That's all you need to do! Hopefully, this explanation was helpful for you.

Answer №9

Note: The following instructions are specifically for external JavaScript links. Step 1. Insert your Angular script into the index.html file at the bottom of the body section is recommended. I have tried various methods without success.

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

There are two ways to achieve this: For Angular 5, within your component folder, add the following code at the top:

declare var feather:any;

Then, in your class, call the required method. For example:

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

This should execute your code successfully! Alternatively, for older versions, you can try the following approach:

//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're having trouble with my code, you can also refer to this link. Hope this information is beneficial!

Answer №10

Finally, after numerous attempts with different code variations, I got it to work!

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

    });
  }

Answer №11

My situation required me to load multiple files that had dependencies on each other (such as a component using bootstrap which in turn relies on a jquery plugin that also depends on jquery). These files all initialized immediately upon loading, assuming they were loaded synchronously on a webpage. However, most solutions out there assume the files are unrelated or require manual initialization after everything is loaded, causing issues with missing variables in my specific setup.

To solve this issue, I implemented a Promise chain instead of a Promise list like @carlitoxenlaweb did (which would resolve everything in parallel). This way, each file is only loaded once the previous one has finished initializing:

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

Answer №12

If the source URL allows for invoking a global function, it is possible to set up a personalized event handler using that feature.

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>

After dispatching our custom event to the window object, we can proceed to listen for it by utilizing Angular's @HostListener decorator 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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the correct way to test setInterval() statements within Angular?

Here is a simple code snippet I am working on: public async authenticate(username: string, password: string) { const authenticationResponse = await this.dataProvider.authenticate(username, password); if (authenticationResponse.result.code == 0) { ...

Deploying assets in Angular using a specified path address

When deploying Angular to a path other than the root, there is an issue with asset paths: any '/assets' path in templates or style sheets does not get properly prefixed with the deployment path. I am looking to create an IIS rewrite rule that ca ...

Customize button appearance within mat-menu in Angular versions 2 and above

Is there a way to style my mat-menu to function similar to a modal dialog? I am having difficulty with the styling aspect and need advice on how to move the save and reset buttons to the right while creating space between them. I have attempted to apply st ...

Angular 4 Issue: Child Routing Dysfunction

I'm encountering an issue with the child routing in Angular 4. The parent routing is functioning correctly, but when I hover over "Create New Account," it remains on the Account page instead of redirecting to localhost:4200/account/create-account. The ...

The function you are trying to call is not valid... the specified type does not have any call signatures [ts 2349

Having some trouble using functions from Observable Plot example with a marimekko chart in my TypeScript project. I encountered an error on this particular line: setXz(I.map((i) => sum.get(X[i]))) The code snippet causing the issue is as follows: fu ...

Troubleshooting display glitches on Bootstrap modals in Internet Explorer 11 and Microsoft Edge

I have been encountering rendering artifacts consistently across various versions of IE11 and Edge, on different devices with varying graphics cards and drivers, as well as different update statuses of Windows 10. The screenshots indicate some of these ar ...

How to Integrate FullCalendar with Your Angular Application

Having some confusion with installing Fullcalendar in my Angular 8 project. I followed the instructions on the Fullcalendar website and installed the package under @fullcalendar using npm install --save @fullcalendar/angular, but then came across examples ...

Encountering a typescript error: Attempting to access [key] in an unsafe manner on an object of

I have recently developed a thorough equality checking function. However, I am encountering an issue with the highlighted lines in my code. Does anyone have any suggestions on how to rectify this problem (or perhaps explain what the error signifies)? Her ...

Tips for showcasing all values in a nested array

In my Vue application, I am dealing with a nested array where users can select one date and multiple times which are saved as one object. The challenge I am facing now is how to display the selected date as a header (which works fine) and then list all the ...

When I try to reverse the words in a string, I am not receiving the desired order

Currently delving into TypeScript, I have set myself the task of crafting a function that takes in a string parameter and reverses each word within the string. Here is what I aim to achieve with my output: "This is an example!" ==> "sihT ...

The resolve.alias feature in webpack is not working properly for third-party modules

Currently, I am facing an issue trying to integrate npm's ng2-prism with angular2-seed. The problem arises when importing angular2/http, which has recently been moved under @angular. Even though I expected webpack's configuration aliases to hand ...

Tips for preventing the need to convert dates to strings when receiving an object from a web API

I am facing an issue with a class: export class TestClass { paymentDate: Date; } Whenever I retrieve an object of this class from a server API, the paymentDate field comes as a string instead of a Date object. This prevents me from calling the ...

Learn how to retrieve data from a JSON server in Angular 8 and then sort that data in a table by utilizing checkboxes

Currently, I'm in the middle of an Angular project where I could use some assistance on how to filter data through checkboxes within a table. The setup involves a home component that displays data from a JSON server in a tabular format using a service ...

Using a loop variable within a callback function in JavaScript/TypeScript: Tips and tricks

I have a method in my TypeScript file that looks like this: getInitialBatches() { var i = 0; for (var dto of this.transferDTO.stockMovesDTOs) { i++; this.queryResourceService .getBatchIdUsingGET(this.batchParams) ...

Customize the text color of select list options in Angular 5

Is there a way to style the foreground colors of select list options differently in this dropdown code? <select id="tier" class="form-control" [(ngModel)]="tierId"> <option *ngFor="let m of tierList" value="{{m.tier}}" > {{m.option ...

What is the method for selecting the desired month on a primeng calendar with multiple selection enabled?

I am looking for a solution to make my inline primeNg Calendar display the month of a specific date in the model, and when I remove dates from the model, I want it to show the current month. I have tried using defaultDate={{value}} and minDate={{value}}, a ...

What is the best way to inject a service instance into the implementation of an abstract method?

In my Angular application, I have a service that extends an abstract class and implements an abstract method. @Injectable({ providedIn: 'root', }) export class ClassB extends ClassA { constructor( private service : ExampleService) { s ...

ViewChild with the focus method

This particular component I'm working on has a hidden textarea by default : <div class="action ui-g-2" (click)="toggleEditable()">edit</div> <textarea [hidden]="!whyModel.inEdition" #myname id="textBox_{{whyModel.id}}" pInputTextarea f ...

The error message "Type 'string | number' is not assignable to type 'number'" indicates a type mismatch in the code, where a value can be either

I encountered an error code while working with AngularJS to create a countdown timer. Can someone please assist me? //Rounding the remainders obtained above to the nearest whole number intervalinsecond = (intervalinsecond < 10) ? "0" + intervalinseco ...

What could be the reason for the malfunctioning of the basic angular routing animation

I implemented a basic routing Angular animation, but I'm encountering issues where it's not functioning as expected. The animation definition is located in app.component.ts with <router-outlet></router-outlet> and two links that shoul ...