How does NgModule work in Angular
How does NgModule work in Angular
The first time I saw the NgModule decorator and its arguments I was completely lost.
I couldn’t understand what was the meaning of imports, declarations, exports, providers or bootstrap and I had a hard time finding clear explanations.
So today I will go through each attributes and provide an explanation together with an example to understand what is the role of each NgModule argument.
1. How does a NgModule declaration look like?
2. Imports
3. Exports
4. Declarations
5. Providers
The full source code is available on my GitHub https://github.com/Kimserey/ng-samples.
1. How does a NgModule declaration look like?
Angular utilises the typescript decorators to define NgModule
.
It accepts imports
, declarations
, exports
and providers
.
A typical declaration would look like that:
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SquareComponent } from './square.component';
import { SquareService } from './square.service';
@NgModule({
imports: [
CommonModule
],
declarations: [
SquareComponent
],
exports: [
SquareComponent
],
providers: [
SquareService
]
})
export class SquareModule { }
Here we are importing the CommonModule
from @angular/common
to give access to common directives like ngIf
or ngFor
.
Then we are declaring the SquareComponent
which is a component defined by ourselves.
Then we are exporting it, so that other modules who import SquareModule
can use the SquareComponent
.
Lastly we are providing a service, SquareService
which we have defined ourselves and make it avaible for dependency injection.
2. Imports
Imports
in NgModule is an array of module. Use it to import functionalities which other modules export.
3. Exports
Exports
is an array of functionalities which the module exports.
A module can also re-export an entire loaded module which then export all the functionality of that module.
For example here in (1) we imported CommonModule
. Now let say we had a SharedModule
with common functionalities like so:
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { HighlightDirective } from './highlight.directive';
@NgModule({
imports: [
CommonModule
],
declarations: [
HighlightDirective
],
exports: [
HighlightDirective,
CommonModule
]
})
export class SharedModule { }
Notice that we are exporting the CommonModule
. By doing that, whoever is importing SharedModule
will not need to import CommonModule
as it will be imported thanks to SharedModule
.
So we can change (1) to the following:
@NgModule({
imports: [
SharedModule
],
declarations: [
SquareComponent
],
exports: [
SquareComponent
],
providers: [
SquareService
]
})
export class SquareModule { }
This technique can be used to import & export multiple modules which are repeatedly imported in different modules.
It is also possible to re-export specific components, directives or pipes from imported module. This could be useful if you do not wish to re-export the whole functionalities of a module.
4. Declarations
Declarations
is an array of either components, directives or pipes.
They are classes defined in the module. They need to be declared
to be usable within the module itself.
For example here we have defined SquareComponent
in SquareModule
and HighlightDirective
in SharedModule
.
Do not declare modules or services in the declarations.
5. Providers
Providers
are used to provide services or connstants via dependecy injection.
For dependency injection, more can be found in the official documentation.
In this example, we have defined a service which can be injected.
import { Injectable } from '@angular/core';
@Injectable
export class SquareService {
computeSurface = (side: number) => side * side;
}
Which we then register in the providers.
providers: [
SquareService
]
This is a shortform the following registration:
{ provide: SquareService, useClass: SquareService }
Providers can be of the following types:
export declare type Provider =
TypeProvider
| ValueProvider
| ClassProvider
| ExistingProvider
| FactoryProvider
| any[];
Provider alias
Instead of using the class, it is possible to provide a different alias to a provider.
{ provide: Service, useClass: Service2 }
This could be useful when we have a new service which will be replacing an old one but still have component referencing the old one.
If we want to be able to inject Service2 but need the same instance when injecting Service:
[
Service2,
{ provide: Service, useExisting: service2 }
]
Value providers
We can also directly provide values.
This can be achieved using useValue
instead of useClass
which creates a ValueProvider
.
class Settings {
apiUrl: string;
}
let settings = {
apiUrl = "test";
}
We then register it the same way as other providers:
{ provide: Settings, useValue: settings }
Factory providers
It is also possible to register a factory to provide services.
This can be used to provide computed values in the service constructor.
For example we could define the following factory:
let squareServiceFactory = (logger: Logger) => {
return new SquareService(logger);
}
Then we can create a FactoryProvider
:
export let squareServiceProvider =
{ provide: SquareService,
useFactory: squareServiceFactory,
deps: [ Logger ] }
Notice the deps
(dependencies) which is a list of provided dependencies.
Lastly we can register squareServiceProvider
in our providers:
providers: [ squareServiceProvider ]
InjectionToken
So far we have used classes to provide a service.
For ValueProvider
, we provided the class to retrieve the value but in event where we don’t have a class, for example if we have an interface, what can we do?
Interfaces are typescript invention. They help the compiler but when turned to JS, they are removed.
Therefore since the classes are used to figure out what service to provide, we need a way to register those values without classes.
That is what the InjectionToken
is for.
import { InjectionToken } from '@angular/core';
export interface AppConfig {
Test: string;
}
export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');
Here we have an interface with a Test
member. We can define a InjectionToken
which we can then use to register the service.
providers: [{ provide: APP_CONFIG, useValue: { Test: 'Hello world' } }]
Lastly we ca then inject the value using the InjectionToken
using the @Inject()
decorator like so:
constructor(@Inject(APP_CONFIG) config: AppConfig) {
this.title = config.title;
}
Note: app.config
is a description used for the string representation of the injection token.
We can see it in the source code of Angular:
toString(): string { return `InjectionToken ${this._desc}`; }
Conclusion
Today we saw what NgModule are composed of. We seen what were the purpose of the imports, exports, declarations and providers.
We have also seen multiple way to provide services and values using the different type of provider.
Hope you enjoy this post as much as I enjoyed writing it. If you have any questions, leave it here or hit me on Twitter https://twitter.com/Kimserey_Lam.
See you next time!
Comments
Post a Comment