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

I am struggling to comprehend the concept of dependency injection. Is there anyone available to provide a clear explanation for me?

I am working on a NestJS application and trying to integrate a task scheduler. One of the tasks involves updating data in the database using a UserService as shown below: import { Injectable, Inject, UnprocessableEntityException, HttpStatus, } fro ...

getting TypeScript configured with webpack

I am currently using Typescript to develop a back-end API utilizing graphql and express. To manage the project development and building process, I have implemented webpack. As part of my setup, I am employing raw-loader in order to load graphql schemas an ...

Looking to troubleshoot an error in my TypeScript reducer implementation. How can I fix this issue?

I'm currently using typescript along with react, redux, and typessafe-actions. However, I've come across a particular error that I need help with. The error message reads: "No overload matches this call. Overload 1 of 2, '(...items: Concat ...

What is the best way to retain all checkbox selections from a form upon submission?

I have a batch of checkboxes that correspond to the profiles I intend to store in the database. HTML: <tr *ngFor="let profile of profiles | async"> <input type='checkbox' name="profiles" value='{{profile.id}}' ng-model=&apo ...

Updating a value in an array in Angular using the same ID

I have an array of buildings that looks like this: const buildings = [ { id: 111, status: false, image: 'Test1' }, { id: 334, status: true, image: 'Test4' }, { id: 243, status: false, image: 'Test7' }, { id: 654, stat ...

Creating click event handler functions using TypeScript

I encountered an issue when trying to set up an event listener for clicks. The error message I received was that classList does not exist on type EventTarget. class UIModal extends React.Component<Props> { handleClick = (e: Event) => { ...

What is the best way to trigger a useReducer dispatch function from a different file that is not a React component, without relying on Redux?

In my project, I have a function component that shows a game board named "EnemyBoardComponent.tsx" const enemyBoardReducer = (state:number[][], action:Action)=>{ switch(action.type){ case "update": { return EnemyBoard.getGrid(); ...

How can I set ion-option to be selected by tapping instead of having to click OK?

Currently, I am developing a mobile application and have encountered a scenario in which I utilized ion-option with ion-select, resulting in successful functionality. However, I am now aiming to remove the OK/CANCEL button so that users do not need to clic ...

One-Of-A-Kind Typescript Singleton Featuring the Execute Method

Is it feasible to create a singleton or regular instance that requires calling a specific method? For instance: logger.instance().setup({ logs: true }); OR new logger(); logger.setup({ logs: true }); If attempting to call the logger without chaining the ...

Expanding the MatBottomSheet's Width: A Guide

The CSS provided above is specifically for increasing the height of an element, but not its width: .mat-bottom-sheet-container { min-height: 100vh; min-width: 100vw; margin-top: 80px; } Does anyone have a solution for expanding the width of MatBott ...

Configuring the array interval in an Angular application

I'm facing an issue while attempting to use the setInterval() function to update text for the user every 3 seconds. My attempt at looping with a for loop has not been successful - I only see 'test 03' and nothing else. I'm struggling ...

Encountering the following error message: "E11000 duplicate key error collection"

Currently, I am in the process of developing an ecommerce platform using the MERN stack combined with TypeScript. As part of this project, I am working on a feature that allows users to provide reviews for products. The goal is to limit each user to only o ...

Looking to update properties for elements within the Angular Material 16 mat-menu across the board?

Currently working with Angular Material 16 and I have a question regarding the height of buttons inside the mat-menu element. Here is an example of the code: <button mat-icon-button> <mat-icon>more_vert</mat-icon> </button> ...

Can we modify the auto-import format from `~/directory/file` to `@/directory/file`?

I have a small issue that's been bugging me. I'm working on a project using Nuxt3 and Vscode. When something is auto-imported, Vscode uses the ~/directory/file prefix instead of the preferred @/directory/file. Is there an easy way to configure Vs ...

Which rxjs operator functions similarly to concatmap, but delays the firing of the next request until the current one is completed?

Imagine if I need to make multiple api calls with a high risk of encountering race conditions. If I send 3 requests simultaneously to update the same data on the server, some of the information could be lost. In order to prevent this data loss, I want to ...

Discovering the element on which the value has decreased - a helpful guide

I am attempting to create a drag and drop feature for simple calculations. Users can drag the UP value and drop it on either BH or OT. The issue I'm facing is that I cannot identify the ID where the user dropped, and the onDrop() function is not work ...

Exploring Vue 3.3: Understanding Generics and Dynamic Properties

I'm currently diving into the generics feature in vue 3.3 and I've been pondering about defining the type of an incoming prop based on another prop value. This is my current component structure: export interface OptionProps { id: string | numb ...

Steps for incorporating a toggle feature for displaying all or hiding all products on the list

Looking for some guidance: I have a task where I need to display a limited number of products from an array on the page initially. The remaining items should only be visible when the user clicks the "Show All" button. Upon clicking, all items should be rev ...

The foundation grid system is experiencing difficulties when implemented on an Angular form

After successfully installing Foundation 6 on my Angular project, I am facing an issue with the grid system not working properly. Despite numerous attempts to troubleshoot and debug, I have not been able to resolve this issue. If anyone has any insights or ...

Accessing router params in Angular2 from outside the router-outlet

I am currently working on a dashboard application that includes a treeview component listing various content nodes, along with a dashboard-edit component that displays editable content based on the selected branch of the tree. For example, the tree struct ...