You just created a beautiful Angular component. As a good developer, you want to share it with others. In Angular, you achieve this by moving it to a separate NgModule. The next step is to make your Angular module customisable. Dependency injection is the key to success! However, be careful with lazy loading.
In this article, I want to show you some tips and tricks for configuration in Angular. Let's make your modules awesome!
Imagine a simple FooModule
, with a FooComponent
:
import { NgModule } from '@angular/core';
import { FooComponent } from './foo.component';
@NgModule({
declarations: [FooComponent],
exports: [FooComponent],
})
export class FooModule {}
The reason we added the FooComponent
to the exports
array, is that other modules that import this module, can now use the FooComponent as well.
Now imagine that the FooComponent
shows a label with a prefix:
import { Component, Input } from '@angular/core';
@Component({
selector: 'foo-component',
template: '<h1>{{ prefix }}: {{ label }}</h1>'
})
export class FooComponent {
@Input() label: string;
prefix = 'Foo';
}
Of course this is a very simplified example, but you get the idea. This component can now be used as:
<foo-component [label]="'Some label'"></foo-component>
Let's add some configuration.
The second step is to make the the prefix
configurable.
Of course we could just add an input property binding with
@Input
, like we did forlabel
. However, imagine that we want to configure the prefix globally for every FooComponent.
A good way to achieve this, is with a configuration class and dependency injection:
import { Component, Input } from '@angular/core';
import { FooConfig } from './foo.config';
@Component({
selector: 'foo',
template: '<h1>{{ prefix }}: {{ label }}</h1>'
})
export class FooComponent {
@Input() label: string;
prefix: string;
constructor(config: FooConfig) {
this.prefix = config.prefix;
}
}
This FooConfig
can be a configuration class, e.g.:
export class FooConfig {
prefix = 'Foo';
}
If you want to use a TypeScript interface instead, you have to use an
InjectionToken
. This is because TypeScript interfaces do not exist at runtime.
Let's provide a default config in our FooModule, so that anyone can use use this module right away:
import { NgModule } from '@angular/core';
import { FooComponent } from './foo.component';
import { FooConfig } from './foo.config';
@NgModule({
declarations: [FooComponent],
exports: [FooComponent],
providers: [FooConfig]
})
export class FooModule {}
Now, anyone importing this FooModule will use the default config. It is also possible to provide a custom configuration (if preferred):
import { NgModule } from '@angular/core';
import { FooConfig, FooModule } from '@foo/lib';
@NgModule({
imports: [FooModule],
providers: [{
provide: FooConfig,
useValue: {
prefix: 'Custom prefix'
}
}]
})
export class AppModule {}
Any FooComponent
in this Angular will now use the custom prefix.
Imagine an app with the following routing structure.
- AppModule (root module)
- DashboardModule (route "/dashboard")
- UsersModule (route "/users")
Now, when both DashboardModule
and UsersModule
need to render FooComponents, they should also both import the FooModule:
@NgModule({imports: [FooModule]}) // (simplified)
export class DashboardModule {}
@NgModule({imports: [FooModule]}) // (simplified)
export class UsersModule {}
However, we still want to have a global configuration, so we keep providing the FooConfig
in the root module:
@NgModule({
imports: [], // (simplified)
providers: [{
provide: FooConfig,
useValue: {
prefix: 'Custom prefix'
}
}]
})
export class AppModule {}
This works fine, because the injector of AppModule
is the top-most injector for every other module.
Now, imagine that the UsersModule
of the previous example app is loaded lazily:
const routes: ROUTES = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full'},
// lazy loading:
{ path: 'users', loadChildren: 'app/users/users.module#UsersModule' }
];
@NgModule({
imports: [
RouterModule.forRoot(routes),
DashboardModule // contains the dashboard route
]
providers: [{
provide: FooConfig,
useValue: {
prefix: 'Custom prefix'
}
}]
})
export class AppModule {}
We will now have an issue: the UsersModule - which is loaded lazily - will not inherit the custom config from the root module, but the default config from the FooModule.
What happened?
It seems that a module which is lazily loaded, will be the top-most injector at bootstrap time.
To solve this, we have to:
- Import FooModule WITH a default config in root module (which we can optionally override)
- Import FooModule WITHOUT a default config in child modules (in order to use its FooComponent)
Instead of making two different modules, Angular provides a ModuleWithProviders
interface for this:
import { NgModule, ModuleWithProviders } from '@angular/core';
import { FooComponent } from './foo.component';
import { FooConfig } from './foo.config';
@NgModule({
declarations: [FooComponent],
exports: [FooComponent],
providers: [], // NOTE: no default config here anymore!
})
export class FooModule {}
export const fooModuleWithConfig: ModuleWithProviders = {
ngModule: FooModule,
providers: [FooConfig]
};
export const fooModuleWithoutConfig: ModuleWithProviders = {
ngModule: FooModule
};
Now we can use the fooModuleWithConfig
in our root module, and fooModuleWithoutConfig
in other modules:
@NgModule({imports: [fooModuleWithConfig]}) // (simplified)
export class AppModule {}
@NgModule({imports: [fooModuleWithoutConfig]}) // (simplified)
export class UsersModule {}
This will solve our issue. And overriding the default config is still possible.
In order to make the intended use more visible, it is recommended to use the static methods forRoot()
and forChild()
instead:
import { NgModule, ModuleWithProviders } from '@angular/core';
import { FooComponent } from './foo.component';
import { FooConfig } from './foo.config';
@NgModule({
declarations: [FooComponent],
exports: [FooComponent],
providers: [], // NOTE: no default config here anymore!
})
export class FooModule {
static forRoot(config: FooConfig): ModuleWithProviders {
return {
ngModule: FooModule,
providers: [{ provide: FooConfig, useValue: config }]
};
}
static forChild(): ModuleWithProviders {
return {
ngModule: FooModule
};
}
}
The benefit is that we can now even require a configuration object as argument for the forRoot()
method (like above), or merge it with a default one.
In the root module:
@NgModule({
imports: [FooModule.forRoot({ prefix: 'custom' )]
})
export class AppModule {}
In any (lazy-loaded) child module:
@NgModule({
imports: [FooModule.forChild()]
})
export class UsersModule {}
Note: As .forChild()
in this case does not return any providers at all, you can also just use the "plain" FooModule in child modules:
@NgModule({
imports: [FooModule]
})
export class UsersModule {}
- Lazy loading problem: StackBlitz
- Lazy loading solution (with
forRoot()
): StackBlitz