ABP Framework Angular UI has some built-in modules, each of which is a separate npm package. Every package has an exported module that can be loaded by the router. When loaded, these modules introduce several pages to the application via child routes.

To allow a degree of customization, we have an extensibility system, and this requires configuration. So, a question arises: How can we configure modules loaded by loadChildren? It may look like a requirement specific to our use case, but I believe the method described here may help other subjects.

The Setup

Here is a mere module that attaches a simple component to the path the router loads it.

import { Component, Inject, InjectionToken, NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";

export const FOO = new InjectionToken<string>("FOO");

@Component({
  selector: "app-foo",
  template: "{{ foo }} works!"
})
export class FooComponent {
  constructor(@Inject(FOO) public readonly foo: string) {}
}

@NgModule({
  imports: [
    RouterModule.forChild([
      {
        path: "",
        component: FooComponent
      }
    ])
  ],
  declarations: [FooComponent],
  providers: [
    {
      provide: FOO,
      useValue: "foo"
    }
  ]
})
export class FooModule {}

Let's load this module with the router.

import { Component, NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { RouterModule } from "@angular/router";
import { FooModule } from "./foo.module";

@Component({
  selector: "app-root",
  template: "<router-outlet></router-outlet>"
})
export class AppComponent {}

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {
        path: "",
        loadChildren: () => FooModule
      }
    ])
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

Do you wish lazy-loading? Well, then we need to do this:

// remove FooModule from imports

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {
        path: "",
        loadChildren: () => import('./foo.module').then(m => m.FooModule),
      }
    ])
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

Voila: "foo works!". Simple, isn't it? So far, there is nothing impressive but bear with me.

The Twist

Now, let's make a small change to introduce some configuration options.

// other imports are removed for brevity
import { ModuleWithProviders } from "@angular/core";

@NgModule({
  imports: [
    RouterModule.forChild([
      {
        path: "",
        component: FooComponent
      }
    ])
  ],
  declarations: [FooComponent]
})
export class FooModule {
  static withOptions(foo = "foo"): ModuleWithProviders<FooModule> {
    return {
      ngModule: FooModule,
      providers: [
        {
          provide: FOO,
          useValue: foo
        }
      ]
    };
  }
}

You have probably used or created a few modules with the forRoot pattern before. What we did is here similar: We used a static method called withOptions to make our module configurable. However, we cannot call this method in loadChildren. Eager or lazy, while loading modules with Angular router, we need to return an NgModuleFactory, not ModuleWithProviders. Let's fix this.

The Factory

Angular core package exports an abstract class called NgModuleFactory. We will extend it, and implement the abstract methods to convert a ModuleWithProviders to a module factory for the router.

import {
  Compiler,
  Injector,
  ModuleWithProviders,
  NgModuleFactory,
  NgModuleRef,
  StaticProvider,
  Type
} from "@angular/core";

export class ChildModuleFactory<T> extends NgModuleFactory<T> {
  get moduleType(): Type<T> {
    return this.moduleWithProviders.ngModule;
  }

  constructor(private moduleWithProviders: ModuleWithProviders<T>) {
    super();
  }

  create(parentInjector: Injector | null): NgModuleRef<T> {
    const injector = Injector.create({
      parent: parentInjector,
      providers: this.moduleWithProviders.providers as StaticProvider[]
    });

    const compiler = injector.get(Compiler);
    const factory = compiler.compileModuleSync(this.moduleType);

    return factory.create(injector);
  }
}

We pass a ModuleWithProviders to the ChildModuleFactory constructor. The following steps happen when the RouterConfigLoader calls the create method:

  • A new injector is created, using the parent injector and providers from the ModuleWithProviders in the process.
  • Compiler is retrieved via that injector.
  • compileModuleSync method accesses the moduleType property, which returns the ngModule from the ModuleWithProviders, and creates a factory with it.
  • Finally, the module is created using that factory.

The Solution

Now we will put this factory in use. We are going to add a new static method.

// other imports are removed for brevity
import { NgModuleFactory } from "@angular/core";
import { ChildModuleFactory } from "./child-module-factory";

@NgModule(/* module metadata is removed for brevity */)
export class FooModule {
  static withOptions(foo = "foo"): ModuleWithProviders<FooModule> {
    return {
      ngModule: FooModule,
      providers: [
        {
          provide: FOO,
          useValue: foo
        }
      ]
    };
  }

  static asChild(...params: FooOptions): NgModuleFactory<FooModule> {
    return new ChildModuleFactory(FooModule.withOptions(...params));
  }
}

type FooOptions = Parameters<typeof FooModule.withOptions>;

The asChild static method takes the ModuleWithProviders returned by the withOptions method, creates a new instance of ChildModuleFactory, and returns it. Guess what? We can call this method inside loadChildren.

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {
        path: "",
        loadChildren: () =>
          import("./foo.module").then(m => m.FooModule.asChild("bar"))
      }
    ])
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

What we see on the screen is now "bar works!". Nice! Besides, you can load the module eagerly and still configure it.

// other imports are removed for brevity
import { FooModule } from "./foo.module";

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {
        path: "",
        loadChildren: () => FooModule.asChild("bar"),
      }
    ])
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

Conclusion

Of course, you can create a wrapper module and pass options through it. Or, maybe you choose to provide tokens directly, and that's perfectly fine. I have described nothing special, but it's a clean and reusable way to achieve loading configurable Angular modules.

I have created a StackBlitz playground where you can see the ChildModuleFactory in action.

Thanks for reading. Have a beautiful day.