Angular 11, js not working after routing in angular

2.4k Views Asked by At

enter image description hereI have been doing R&D over a bug in angular(not calling it a bug). But In SPA, js loses its scope after routing to a different component. I have read almost every answer available on the internet. Let me put it out in a simpler manner what the problem is.

  1. Scope ends after we route to a different element(we are using external js)
  2. Since my js are interconnected, it all had to load at once. so loading a particular js through component.ts method is not feasible as it again changes the scope of DOM and the other features stop working.

so if anyone is encountering such issues, let's discuss here.

Updated Image: enter image description here

My CODE : Script-loader.ts

import { Injectable } from '@angular/core'; @Injectable({
providedIn: 'root' }) export class ScriptLoaderService {

private scripts: any = {};

load(...scripts: string[]) {
    this.scripts = scripts;
    let promises: any[] = [];
    scripts.forEach((script) => promises.push(this.loadScript(script)));
    return Promise.all(promises);
}

loadScript(name: string) {
    return new Promise((resolve, reject) => {
        let script = (document.createElement('script') as any);
        script.type = 'text/javascript';
        script.src = name;
        script.async = false;
        script.defer = false;
        


        if (script.readyState) {  //IE
            script.onreadystatechange = () => {
                if (script.readyState === 'loaded' || script.readyState === 'complete') {
                    script.onreadystatechange = null;
                    resolve({script: name, loaded: true, status: 'Loaded'});
                }
            };
        } else {  //Others
            script.onload = () => {
                resolve({script: name, loaded: true, status: 'Loaded'});
            };
        }

        script.onerror = (error: any) => resolve({script: name, loaded: false, status: 'Loaded'});
        document.getElementsByTagName('head')[0].appendChild(script);
    });
}

} Blockquote

blogs.component.ts

import { Component, OnInit, AfterViewInit } from '@angular/core'; import { ScriptLoaderService } from '../script-loader.service'; declare var $: any;

@Component({ selector: 'app-blogs', templateUrl: './blogs.component.html', styleUrls: ['./blogs.component.css'] }) export class BlogsComponent implements OnInit, AfterViewInit {

constructor(private scriptloader: ScriptLoaderService) { }

ngAfterViewInit(): void { this.scriptloader.load( 'assets/js/app.js', 'assets/data/tipuedrop_content.js', 'assets/js/global.js', 'assets/js/navbar-v1.js', 'assets/js/main.js', 'assets/js/widgets.js',

  'assets/js/lightbox.js',
  'assets/js/feed.js',
  
);

   }

ngOnInit(): void { }

}

Blogs.module.ts

import { NgModule } from '@angular/core';

import { Routes, RouterModule } from "@angular/router"; import { BlogsComponent } from './blogs.component';

const routes: Routes = [ { path: "", component: BlogsComponent } ];

@NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class BlogsModule { }

videos.component.ts

import { Component, OnInit, AfterViewInit } from '@angular/core'; import { ScriptLoaderService } from '../script-loader.service'; declare var $: any; @Component({ selector: 'app-videos',
templateUrl: './videos.component.html', styleUrls: ['./videos.component.css'] }) export class VideosComponent implements OnInit, AfterViewInit {

constructor(private scriptloader: ScriptLoaderService) { }
ngAfterViewInit(): void { this.scriptloader.load( 'assets/js/app.js', 'assets/data/tipuedrop_content.js', 'assets/js/global.js', 'assets/js/navbar-v1.js', 'assets/js/main.js',

  'assets/js/widgets.js',
  
  'assets/js/lightbox.js',
  'assets/js/feed.js',
);

   }

ngOnInit(): void { }

}

videos.module.ts

import { NgModule } from '@angular/core';

import { Routes, RouterModule } from "@angular/router"; import { VideosComponent } from './videos.component';

const routes: Routes = [ { path: "", component: VideosComponent } ];

@NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class VideosModule { }

App-routing.module.ts

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { PreloadingStrategy,
PreloadAllModules } from "@angular/router"; import { BlogsModule } from './blogs/blogs.module'; import { FeedModule } from './feed/feed.module'; import { HangoutModule } from './hangout/hangout.module'; import { HomeModule } from './home/home.module'; import { VideosModule } from './videos/videos.module';

const routes: Routes = [

{ path:"feed", loadChildren: "./feed/feed.module#FeedModule" }, { path:"read", loadChildren: "./blogs/blogs.module#BlogsModule" }, { path:"watch", loadChildren: "./videos/videos.module#VideosModule" }, { path:'home', loadChildren:"./home/home.module#HomeModule" }, { path:"hangout", loadChildren:"./hangout/hangout.module#HangoutModule" }

];

@NgModule({ imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })], exports: [RouterModule] }) export class AppRoutingModule { }

Now what is happening that when I am routing from blogs to videos. It is adding all the js below previous ones again. I don't know if it is a good practice or not. Here is the DOM HEAD to show what's happening

enter image description here

P.S. There is no exact solution which will work for you.Please write your own js in component.ts. You can import jquery and write qwery

3

There are 3 best solutions below

2
Mehdi Daustany On

You can add your JS libraries or even your CSS files into angular.json file for higher scope purposes (Globally):

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "mehdi-daustany": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
            "assets": [
              "src/assets",
              "src/favicon.png"
            ],


            "styles": [
              "src/public-styles.css"
            ],

            "scripts": [
              "node_modules/jquery/dist/jquery.min.js",
              "node_modules/@microsoft/signalr/dist/browser/signalr.min.js",
              "src/assets/common/js/components/util.js"
            ]


          } ...

Importing these files compiled as a file into your DOM, named: scripts.js and styles.js. So import them by your project logic sequences.

5
Mehdi Daustany On

Other way.

You can load your scripts on ngOnInit() or ngAfterViewInit() of your component, I prefer ngAfterViewInit(), but how:

First, create a .ts file named "script-loader.service.ts" or what ever you want. Paste the following code in it.

import { Injectable } from '@angular/core';
@Injectable()
export class ScriptLoaderService {

    private scripts: any = {};

    load(...scripts: string[]) {
        this.scripts = scripts;
        let promises: any[] = [];
        scripts.forEach((script) => promises.push(this.loadScript(script)));
        return Promise.all(promises);
    }

    loadScript(name: string) {
        return new Promise((resolve, reject) => {
            let script = (document.createElement('script') as any);
            script.type = 'text/javascript';
            script.src = name;

            if (script.readyState) {  //IE
                script.onreadystatechange = () => {
                    if (script.readyState === 'loaded' || script.readyState === 'complete') {
                        script.onreadystatechange = null;
                        resolve({script: name, loaded: true, status: 'Loaded'});
                    }
                };
            } else {  //Others
                script.onload = () => {
                    resolve({script: name, loaded: true, status: 'Loaded'});
                };
            }

            script.onerror = (error: any) => resolve({script: name, loaded: false, status: 'Loaded'});
            document.getElementsByTagName('head')[0].appendChild(script);
        });
    }

}

Then, provide this service in a top module, for example app.module.ts

import { NgModule } from '@angular/core';
import { ScriptLoaderService } from './script-loader.service';

@NgModule({
    imports: [
    ],
    providers: [
        ScriptLoaderService
    ],
    declarations: [
    ],
    exports: [
                ]
})
export class AppModule { }

Now it's time to use this service, lets assume you want to use it in ngAfterViewInit() initialization method, so you have to implement AfterViewInit in your target component; for example:

import { Component, AfterViewInit } from '@angular/core';
import { ScriptLoaderService } from '@shared/utils/script-loader.service';

@Component({
    templateUrl: './your.component.html'
})

export class YourComponent implements AfterViewInit {

    constructor(
        private scriptLoader: ScriptLoaderService) {

    }

    ngAfterViewInit() {
        this.scriptLoader.load(
            'assets/dist/jquery.min.js',
            'assets/ckeditor/ckeditor.js'
        );
    }
}

By this way, every time that your component lazy loaded by angular router, needed scripts initialize without hard refreshing your page.

0
Buchi On

if you still see this issue after trying the suggested answer of dynamically importing the scripts, with the help https://stackoverflow.com/a/67171327/11801411

you might be repeating the same id in different components. i encountered this issue while working with apex chart. it happened I had an id of #chart in my product component, as well as another id #chart in my dashboard component. Although they might appear not to both be present in the dom at the same time, i had issues with some scripts when I was navigating.

Just ensure to use different id's for your element even if they are not present in the same component

Angular Community