The purpose of said functions are to initialize the module, e.g. providing services the module or application might need, and offering a way to provide a configuration for the module, that its components and services can use.
In such a forRoot()
function, we might commonly see a snippet of code along the following lines:
export interface ModuleConfig {
debug: boolean;
text: string;
}
export const MODULE_CONFIG = new InjectionToken('Module Config');
export const DEFAULT_CONFIG: ModuleConfig = { debug: false, text: 'Eager Module' };
@NgModule({
declarations: [HomeComponent],
imports: [CommonModule, ConfigurableRoutingModule],
})
export class ConfigurableModule {
static forRoot(config: Partial): ModuleWithProviders {
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
return {
ngModule: ConfigurableModule,
providers: [{ provide: MODULE_CONFIG, useValue: mergedConfig }],
};
}
}
forRoot()
function of the module. It is merely a static function that returns a representation of the Angular module with additional providers. We provide the module’s config via the injection token MODULE_CONFIG
. In this case this config is provided in the application’s root injector, thus making it accessible to the whole Application. Of course it is possible to provide the config in the AppModule directly resulting in the same behavior, however, it degrades the usability of our module, as the user needs to be aware that it is their responsibility to do so. Therefore, we prefer the above approach resulting in a clear API for the user. Lazy Loaded Modules
As an Angular veteran you are probably familiar with lazy loading your modules. It usually looks like something like this:
const routes: Routes = [
{
path: 'lazy',
loadChildren: () =>
import('../app/lazy-loaded/lazy-loaded.module').then(
(m) => m.LazyLoadedModule,
),
},
];
ModuleWithProviders
as our forRoot()
function does. Now imagine the situation that you do want to provide a config with the module, as we usually would want to do. One way of course is providing the config in the application root as outlined above. For the sake of a cleaner API, let’s try to make this module configurable.
One thought about lazily loaded modules: a module that is loaded during the runtime when the user navigates to a certain route, cannot possibly provide a service or configuration to the root level of the application. If that were the case, other parts of the app would not function properly unless the user navigated to the route first.
That means a config we provide for this module, will only be relevant to that module. This also comes with the added benefit, that this encapsulation will allow us to reuse the module on two different routes with different configurations. Configuring Lazy Loaded Modules
We want to suggest a way to configure our lazy loaded module in a similar way as shown in the introduction to this article.
In order to make this possible we need to give our module a static method:
@NgModule({
// [...]
})
export class LazyLoadedConfigurableModule {
static config = DEFAULT_CONFIG;
static configure(
config: Partial = DEFAULT_CONFIG,
): LazyLoadedConfigurableModule {
this.config = { ...DEFAULT_CONFIG, ...config };
return this;
}
}
forRoot()
example, we provide a static method to receive and
prepare our module’s config. Important to note are the two differences
- the static method returns
this
(the module class) - we store the config in a static class variable
const routes: Routes = [
{
path: 'lazy',
loadChildren: () =>
import('../app/lazy-loaded-configurable/lazy-loaded-configurable.module').then(
(m) => m.LazyLoadedConfigurableModule.configure({ text: 'Lazy Loaded' }),
),
},
];
There is now a missing piece to make this all work. While we do provide the config and could potentially access it via the static class member, we would like to get our config into the Angular DI. Leveraging a factory provider, we can do so as follows:
function ConfigFactory() {
return LazyLoadedConfigurableModule.config;
}
@NgModule({
declarations: [HomeComponent],
imports: [CommonModule, LazyLoadedConfigurableRoutingModule],
providers: [
{
provide: MODULE_CONFIG,
useFactory: ConfigFactory,
},
],
})
export class LazyLoadedConfigurableModule {
static config = DEFAULT_CONFIG;
static configure(
config: Partial = DEFAULT_CONFIG,
): LazyLoadedConfigurableModule {
this.config = { ...DEFAULT_CONFIG, ...config };
return this;
}
}
This will provide our config in Angular’s DI on module level and thus making our lazy loaded module configurable. We also maintain a clean developer experience and avoid the implicit knowledge necessary to provide a config in the app root for a module that might never be loaded in the first place.
Conclusion
Above we explored configuring a lazily loaded Angular module. If you are already familiar with making modules configurable, the solution introduced in this article should be easy to implement. In case you are seeing how to provide your modules with a forRoot
method for the first time now, consider adding it to your tool-belt!
GitHub Repository
You can find a sample implementation of the above concept right in this GitHub Repository:
https://github.com/thinktecture/blog-configure-lazy-modules-demo