Search code examples
angularangular17angular-standalone-components

After upgrade to angular 17 and convert as a standalone loader interceptor & it's component not working


Project is build In Angular Verison:17 We have created loading.component.ts and loader.service.ts and bind in app.component.html and in progress.interceptor.ts file as you can see In code. Loader was working well In Angular version 14 but after upgraded to Angular 17 it was stopped working. And Angular Version 17 upgraded with standalone component and we did updated code as per 17. Loader should work on each router navigation and API call.

Also getting Error "Cannot read properties of undefined (reading 'apply')" after every API call.

loading.component.ts

      import { Component, OnInit, OnDestroy } from '@angular/core';
        import { LoaderService } from '../../service';
        import { Subscription } from 'rxjs';
        import { LoaderState } from '../../models/loader.model'
        import { CommonModule } from '@angular/common';
        @Component({
        selector: 'loading',
        templateUrl: './loading.component.html',
        styleUrls: ['./loading.component.css'],
        standalone: true,
        imports: [CommonModule],
        providers:[LoaderService]
        })
        export class LoadingComponent implements OnInit, OnDestroy {
        show = false;
        subscription: Subscription;
        constructor(private loaderSvc: LoaderService) { }

        ngOnInit() {
        this.subscription = this.loaderSvc.loaderState.subscribe((state: LoaderState) => {
        this.show = state.show; //setTimeout(() => this.show = state.show);
        })
        }

        ngOnDestroy() {
        if (this.subscription) {
        this.subscription.unsubscribe();
        }
        }
        }

loading.component.html

<div *ngIf="show">
 <div class="loader-overlay" style="position: relative; top: 45vh;">
    <div class="spinner">
      <div class="bounce1"></div>
      <div class="bounce2"></div>
      <div class="bounce3"></div>
    </div>
  </div>

app.component.html

<loading></loading> 
<router-outlet></router-outlet>

app.component.ts

import { AfterViewInit, Component, OnInit, Inject, PLATFORM_ID } from '@angular/core';
import { Router, ActivatedRoute, NavigationEnd, RouterModule } from '@angular/router';
import { LoadingComponent } from './shared/components';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
standalone: true,
imports: [RouterModule, NgIdleKeepaliveModule, LoadingComponent],
providers: [{ provide: WINDOW, useValue: {} }]

constructor(public loaderSVC: LoaderService){}

ngOnInit() {
this.router.events.subscribe((evt) => {
this.loaderSVC.startLoading();
// console.log("start")
if (!(evt instanceof NavigationEnd)) {
return;
}
setTimeout(() => {
this.loaderSVC.stopLoading();
// console.log("stop")
});
})

loader.service.ts

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { LoaderState } from '../models/loader.model';

@Injectable({
providedIn: 'root'
})
export class LoaderService {
private loaderSubject = new Subject<LoaderState>();
loaderState = this.loaderSubject.asObservable();
constructor() { }

startLoading() {  
this.loaderSubject.next(<LoaderState>{show: true});
}

stopLoading() {
this.loaderSubject.next(<LoaderState>{show: false});
}
}

progress.interceptor.ts

import {Injectable} from '@angular/core';
import {HttpRequest, HttpHandler, HttpEvent, HttpInterceptor} from '@angular/common/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/finally';
import { LoaderService } from '../service';

@Injectable()
export class ProgressInterceptor implements HttpInterceptor {
constructor(private loaderSvc: LoaderService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (request.params.has('ignoreLoadingBar')) {
return next.handle(request.clone({ headers: request.headers.delete('ignoreLoadingBar') }));
}
this.loaderSvc.startLoading();
return next.handle(request).finally(() => this.loaderSvc.stopLoading());
}
}

main.ts

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import './icons';
import { environment } from './environments/environment';
import { AppComponent } from '@app/app.component';
import { appConfig } from '@app/app.config.server';
import { bootstrapApplication } from '@angular/platform-browser';

if (environment.production) {
enableProdMode();
}

bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err));

app.config.server.ts

import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './routes/routes';
import { provideClientHydration, withHttpTransferCacheOptions } from '@angular/platform-browser';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';
import { provideAnimations } from '@angular/platform-browser/animations';
import { CookieService } from 'ngx-cookie-service';
import { provideNgxMask } from 'ngx-mask';
import { provideToastr } from 'ngx-toastr';
import { ProgressInterceptor, BasicAuthInterceptor } from './shared/helpers';
import { StorageService } from './shared/service';

export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), provideClientHydration(withHttpTransferCacheOptions({
includePostRequests: true
})), provideNgxMask(), {
provide: HTTP_INTERCEPTORS,
useClass: ProgressInterceptor,
multi: true
}, {
provide: HTTP_INTERCEPTORS,
useClass: BasicAuthInterceptor,
multi: true
}, provideAnimations(),
CookieService, StorageService, provideToastr({ positionClass: 'toast-top-right' }),
importProvidersFrom(ReactiveFormsModule, HttpClientModule),]
};

Solution

  • The loading.component.ts has the LoaderService in the providers array, this means a separate instance has been created that is only for this component. So other emits are not received.

    ...
    @Component({
        selector: 'loading',
        templateUrl: './loading.component.html',
        styleUrls: ['./loading.component.css'],
        standalone: true,
        imports: [CommonModule],
        // providers:[LoaderService] // <- changed here!
    })
    export class LoadingComponent implements OnInit, OnDestroy {
        ...
    

    You need to convert progress.interceptor.ts to a interceptorFn

    import { inject } from '@angular/core';
    import { HttpInterceptorFn } from '@angular/common/http';
    import { LoaderService } from '../service';
    
    export const demoInterceptor: HttpInterceptorFn = (req, next) => {
      const loaderSvc = inject(LoaderService);
      const authToken = 'YOUR_AUTH_TOKEN_HERE';
    
      // Pass the cloned request with the updated header to the next handler
      if (req.params.has('ignoreLoadingBar')) {
          // Clone the request and add the authorization header
          const authReq = req.clone({ 
               headers: request.headers.delete('ignoreLoadingBar') 
          });
          return next(authReq);
      }
      this.loaderSvc.startLoading();
      return next(request).finally(() => this.loaderSvc.stopLoading());
    };
    

    Finally in the app.config.server.ts we need to use provideHttpClient(withInterceptors([demoInterceptor])) to configure the interceptors

    import { ApplicationConfig, importProvidersFrom } from '@angular/core';
    import { provideRouter } from '@angular/router';
    
    import { routes } from './routes/routes';
    import { provideClientHydration, withHttpTransferCacheOptions } from '@angular/platform-browser';
    import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
    import { ReactiveFormsModule } from '@angular/forms';
    import { provideAnimations } from '@angular/platform-browser/animations';
    import { CookieService } from 'ngx-cookie-service';
    import { provideNgxMask } from 'ngx-mask';
    import { provideToastr } from 'ngx-toastr';
    import { ProgressInterceptor, BasicAuthInterceptor } from './shared/helpers';
    import { StorageService } from './shared/service';
    
    export const appConfig: ApplicationConfig = {
        providers: [
            provideRouter(routes), 
            provideClientHydration(withHttpTransferCacheOptions({
                includePostRequests: true
            })), 
            provideNgxMask(), 
            provideHttpClient(withInterceptors([ProgressInterceptor, 
                 BasicAuthInterceptor])),
            provideAnimations(),
            CookieService, 
            StorageService, 
            provideToastr({ positionClass: 'toast-top-right' }),
            importProvidersFrom(ReactiveFormsModule, HttpClientModule),
        ]
    };