1
votes

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

2
Hvae you thought about putting whatever js you need to work across multiple components into a service and then calling on those functions/variables from the service?Dylan Anlezark
I have updated the code. I used this but it didn't work to I made a script.service.ts file and define the function after that I call it by using ngonit. then since js are interconnected. It all have to load at once. but it didn't work.abhey
Hey! I have used your code to implement and solve my bug. It solved it up to some extent but it is loading all the js files again in the DOM. As in if I have 3 js files(or 2 script tags) on the first refresh. Then if I route to another component. it is reloading the js but now the HTML DOM has 6 script tags. If you want me to share the codes I will.abhey
It is showing like this. Including only one js file. ON first visit to page <script scr = 'app.js'>></script>. Then on routing to another component now it has 2 same js in the fieldabhey

2 Answers

0
votes

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.

0
votes

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.