Angular 4.3 Interceptors for Lazy Loaded Modules

Mukun picture Mukun · Oct 30, 2017 · Viewed 10.9k times · Source

What is the best practice to use core module service in lazy loaded feature module and feature child modules.
As per Angular style guide I have the following

 app
   -core
       - core.module.ts
       -logger.service.ts
       -token-interceptor.service.ts
       -authentication.service.ts
   -shared
       -shared.module.ts
   -base module (my feature base , lazy loaded with router-outlet)
     -base.module.ts
     -base.routing.module.ts
     -base
       -base.component.ts
     -admin (lazy loaded , child module of base module)
      -admin.module.ts
      -admin.routing.ts
      -admin-component.ts 
     -report(lazy loaded , child module of base module, sibling of admin)
      -report.module.ts
      -report.routing.ts
      -report-component.ts

If I add TokenInterceptorService as a provider in all feature module,then HTTP interceptor works. When I add it in App module,(but not in lazy loaded feature module) it does not intercept http request triggered in lazy loaded feature module.

What is the best practice, of using services/interceptor declared in core module.

app.module.ts

@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
  ],
  imports: [
    BrowserModule,BrowserAnimationsModule, CoreModule, AppRoutingModule, FormsModule
  ], providers: [{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptorService, multi: true }],
  bootstrap: [AppComponent]
})
export class AppModule { }

app-routing.module.ts

@NgModule({
    imports: [
        RouterModule.forRoot([
            { path: 'login', component: LoginComponent },
            { path: '', redirectTo: 'base', pathMatch: 'full' },                   
            {  path: 'base', loadChildren: 'app/base/base.module#BaseModule'  }            
        ])
    ],
    exports: [
        RouterModule
    ]
})
export class AppRoutingModule {
}

core.module.ts

@NgModule({
  imports: [
    CommonModule, HttpModule,  
  ],
  declarations: [],
  providers: [LoggerService, AuthenticationService]
})
export class CoreModule {
  constructor( @Optional() @SkipSelf() parentModule: CoreModule) {
    throwIfAlreadyLoaded(parentModule, 'CoreModule');
  }
}

token-interceptor.service.ts

import { Injectable } from '@angular/core';
    import { Router } from '@angular/router';
    import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse } from '@angular/common/http';
    import { Observable } from 'rxjs/Observable';
    import 'rxjs/add/operator/do';

    import { AuthenticationService } from './authentication.service';

    @Injectable()
    export class TokenInterceptorService implements HttpInterceptor {


      constructor(public auth: AuthenticationService, private router: Router) { }

      intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        console.log('Adding authorization header')

        request = request.clone({
          setHeaders: { authorization: this.auth.getToken() }
        });
        console.log('Added authorization header')
        return next.handle(request).do(event => { }, (err: HttpErrorResponse) => {
          console.log("Error ===>", err);
          if (err.error instanceof Error) {
            // A client-side or network error occurred. Handle it accordingly.
            console.log('An error occurred:', err.error.message);
          } else if (err.status == 401) {

            console.log('Status 401  unautorized');
            this.router.navigate(['/login']);
          } else {
            // The backend returned an unsuccessful response code.
            // The response body may contain clues as to what went wrong,
            console.log(`Backend returned code ${err.status}, body was: ${err.error}`);
          }

          return Observable.throw(new Error('Your custom error'));
        });;
      }

    }

base.module.ts

 @NgModule({
      imports: [
        CommonModule, BaseRoutingModule
      ],
      declarations: [BaseComponent],
      providers: [],
    })
    export class BaseModule {


      constructor( @Optional() @SkipSelf() parentModule: BaseModule) {

        if (parentModule) {
          throw new Error(
            'BaseModule is already loaded. Import it in the AppModule only');
        }
      }
    }

base.routing.module.ts

@NgModule({
    imports: [
        RouterModule.forChild([
            {
                path: '', component: BaseComponent,
                children: [

                    { path: '', redirectTo: 'admin', pathMatch: 'full' },
                    { path: 'admin', loadChildren: 'app/base/admin/admin.module#AdminModule' },                         
                ]
            }])
    ],
    exports: [
        RouterModule
    ]
})
export class BaseRoutingModule {
}

admin.module.ts

@NgModule({
  imports: [
    CommonModule, FormsModule, HttpClientModule, AdminRoutingModule,BusyModule
  ],
  declarations: [UserListComponent, UserComponent, MenuListComponent, MenuComponent, CodeListComponent, CodeComponent],
  providers: [CodeService, UserService, MenuService,{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptorService, multi: true }]
})
export class AdminModule { }

admin.routing.module.ts

@NgModule({
    imports: [
        RouterModule.forChild([

            {
                path: '',
                children: [
                    { path: '', redirectTo:'code-list', pathMatch: 'full'  },
                    { path: 'code-list', component: CodeListComponent },
                    { path: 'code/:id', component: CodeComponent },

                    { path: 'user-list', component: UserListComponent },
                    { path: 'user/:id', component: UserComponent },

                    { path: 'menu-list', component: MenuListComponent },
                    { path: 'menu/:id', component: MenuComponent },                    
                ]
            }
        ])
    ],
    exports: [
        RouterModule
    ]
})
export class AdminRoutingModule {
}

Answer

Danil Gudz picture Danil Gudz · Oct 30, 2017

You don't have to create a provider for your interceptor. You should export your CoreModule with forRoot():

@NgModule({
  imports: [
    CommonModule,
    HttpClientModule,
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: HttpLoaderFactory,
        deps: [HttpClient]
      }
    }),
    RouterModule.forRoot(
      [],
      {enableTracing: true}
    ),
  ],
  declarations: [],
  providers: [DatePipe]
})
export class CoreModule {
  constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
    if (parentModule) {
      throw new Error(
        'CoreModule is already loaded. Import it in the AppModule only');
    }
  }

  static forRoot(): ModuleWithProviders {
    return {
      ngModule: CoreModule,
      providers: [
        {provide: 'Window', useValue: window},
        {provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, multi: true},
        SORT_TYPES_PROVIDER,
        ApiService,
        AnimationService,
        BillingService,
        UserService,
        ...
      ]
    };
  }
}

Then import it in your AppModule and forget about CoreModule imports at all. This is only one place it has to be explicitly used. All your lazy loaded modules will take your services etc by DI.

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    SharedModule,
    CoreModule.forRoot(),
    FeaturesModule,
    PublicModule,
    RouterModule.forRoot([])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {
}

Also you don't need to create a separate routing module for each module. Just export what RouterModule.forChild returns and use it in imports of your module that needs it.

export const publicRouting: ModuleWithProviders = RouterModule.forChild([
  {
    path: 'login',
    pathMatch: 'full',
    component: SignInComponent,
    data: {
      breadcrumb: 'LGN_TL'
    },
    canActivate: [AuthenticatedGuard]
  },
  {
    path: '',
    component: GlobalComponent,
    loadChildren: '../protected/protected.module#ProtectedModule',
    canLoad: [AuthCanLoadGuard]
  },
  {path: '**', component: PageNotFoundComponent}
]);

UPD. suggestion for routing not a styleguide. Use RoutingModule as before (https://angular.io/guide/styleguide#angular-ngmodule-names)