What is NgModule? (English)
What is NgModule?
Welcome to Angular Fundamental Series π
The story behind this blog is that I saw the Chrome Dev Tools π οΈerror in my coworker’s screen. The error is
Error: Template parse errors:
'mat-tab' is not a known element:
1. If 'mat-tab' is an Angular component, then verify that it is part of this module.
2. If 'mat-tab' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.
“Did you forget to import MatTableModule into the feature module that you are developing?” I said.
After she checked, and she found that she didn’t import it. She then fixed it, and it works fine.
I keep this problem in my mind, and ask to my ex-coworker who often use Angular. “Do you really understand what NgModule is?”
He replied “I don’t really understand what NgModule is. I just know how to make it work.” π±
Thus, I started this blog to clarify NgModule.
Let’s start.
NgModule - Unit of Compilation & Distribution
NgModule is JavaScript Class enhanced behaviors with decorator namely
@NgModule.
@NgModule receives metadata object.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
// @NgModule decorator with metadata object (declarations, imports, ...)
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
What does NgModule do? NgModule receives metadata for
- Tell Angular Compiler how to compile Components, Templates, Directives, Pipes.
- Define Components, Directives, Pipes to be public via metadata that is exports in order to let other modules access and exploit it.
- Add Services or Providers for Dependency Injection in Component.
Let’s see case 1,2 via the below example.
@NgModule({
imports: [
CommonModule,
FlexLayoutModule,
BrowserAnimationsModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatProgressSpinnerModule,
],
declarations: [
CompanyCardComponent,
TechToIconPipe,
CompanyListComponent
],
exports: [
CompanyListComponent,
]
})
export class CompanyModule { }
There are interesting metadatas - imports, declarations, exports.
However, the order will be exports, imports, and declarations respectively.
exports
Define Components, Directives, Pipes to be exported. After other modules import this module, it will understand this compilation scope.
For example, ModuleA import ModuleB, then everything being exported in ModuleB will be in compilation scope of ModuleA
Therefore, ModuleA can used exported Components, Directives, and Pipes in ModuleB.
The below example shows CompanyListComponent is exported in CompanyModule
@NgModule({
...,
exports: [
CompanyListComponent,
// XXXPipe,
// XXXDirective
]
})
export class CompanyModule { }
Then, AppModule import CompanyModule.
@NgModule({
declarations: [
AppComponent
]
imports: [
...,
CompanyModule,
...
],
...
})
export class AppModule { }
AppComponent inside declarations understands CompanyListComponent. and it is able to use CompanyListComponent in its template.
Here is the example of AppComponent Template (app.component.html) uses <angular-th-company-list></angular-th-company-list>
that is selector in CompanyListComponent.
...
<angular-th-company-list [companies]="companies$ | async" [loaded]="loaded"></angular-th-company-list>
...
app.component.html’s full code
imports
imports other modules, then all Components, Pipes, Directives in that module will in the scope at Compile-time.
Providers registered in that Module can be used also at Run-time (Dependency Injection).
@NgModule({
imports: [
CommonModule,
FlexLayoutModule,
BrowserAnimationsModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatProgressSpinnerModule,
// XXXModule
],
declarations: [
CompanyCardComponent, // <-- Here
...
],
...
})
export class CompanyModule { }
From above example, CompanyCardComponent (company-card.component.html) uses MatCard Component <mat-card></mat-card>
and Directive fxLayoutAlign<div fxLayoutAlign="center center">
<mat-card fxLayout="column" *ngIf="company">
...
<div fxLayoutAlign="center center" style="height: 300px">
...
</div>
...
</mat-card>
company-card.component.html’s full code
To use mat-card (Component) and fxLayoutAlign (Directive), we have to tell Angular Compiler that we are going to use MatCardComponent, FxLayoutAlignDirective. In MatCardModule and FxLayoutModule have already exported it. Thus, we just imports MatCardModule and FlexLayoutModule to use it.
How’s about CommonModule? What does it actually do?
CommonModule is important.
CommonModule exports *ngIf, *ngFor, [ngClass], AsyncPipe (| async), CurrencyPipe (| currency)
that why we can use all of it.
Other components such as MatButton, BrowserAnimationsModule, MatIcon, MatProgressBar have the same concept.
In order to use Angular form, FormsModule or ReactiveFormsModule has to be imported also.
declarations
Define all Component, Directive, Pipe to tell Angular Compiler that we are going to use it. In this module scope only
@NgModule({
...
declarations: [
CompanyCardComponent,
TechToIconPipe,
CompanyListComponent,
// XXXDirective
],
...
})
export class CompanyModule { }
From the above example,
Components (CompanyCardComponent, CompanyListComponent) and Pipes (TechToIconPipe) know each other because they are in the same compilation scope. Hence, they can use the others interchangably.
CompanyCardComponent uses TechToIconPipe in the template.
...
<img *ngIf="tech | techToIcon as icon; else techText"/>
...
company-card.component.html’s full code
CompanyListComponent loops to create <angular-th-company-card></angular-th-company-card>
via *ngFor
in the template.
...
<angular-th-company-card *ngFor="let company of companies" [company]="company"></angular-th-company-card>
...
company-list.component.html’s full code
providers
After Angular Version 6.0, we can create Singleton Service by defining providedIn: 'root'
in metadata of @Injectable()
to tell Angular register Service into Application Root. This process make compiler be able to Tree-shaking unused services.
@Injectable({
providedIn: 'root'
})
export class CompanyService {
constructor(private http: HttpClient) { }
getCompanies() {
return this.http.get<Company[]>('/assets/data/companies.json').pipe(
shareReplay(1));
}
}
Before Version 6.0, we registers services at module or component level.
@NgModule({
...
providers: [
CompanyService,
// {
// provide: HTTP_INTERCEPTORS, useClass: HttpInterceptorService, multi: true
// },
],
...
})
forRoot, forChild, forFeature, forXXX, xxx is ModulewithProviders
Everyone may notice forRoot, forChild, forFeature, and so on. What is it? π€
@NgModule({
imports: [RouterModule.forRoot(...)],
exports: [RouterModule]
})
export class AppRoutingModule {}
@NgModule({
imports: [RouterModule.forChild(...)],
exports: [RouterModule]
})
export class CompanyRoutingModule { }
@NgModule({
imports: [
StoreModule.forFeature(...),
TranslateModule.forChild(...),
EffectsModule.forFeature(...)
],
})
export class ExamplesModule { }
@NgModule({
imports: [
AngularFireModule.initializeApp(...),
StoreModule.forRoot(...),
StoreRouterConnectingModule.forRoot(),
EffectsModule.forRoot(...),
StoreDevtoolsModule.instrument(...),
TranslateModule.forRoot(...)
],
})
export class AppModule { }
It seems like to configure something. Right? Let’s check the example via AngularFireModule π₯.
@NgModule({
imports: [
AngularFireModule.initializeApp({
apiKey: 'perjerzKey',
authDomain: 'perjerz.app',
projectId: 'perjerzId',
databaseURL: 'https://perjerz.firebaseio.com',
storageBucket: 'perjerz.appspot.com',
messagingSenderId: '1212312121',
appId: '1150',
}),
...
],
})
export class AppModule { }
Yes, it was used to configure. β We can configure value in tokens and services to use Dependency Injection in component. Component will be able to use that values.
Let’s see the example via Setup AngularFire. Angular π °οΈ App want to talk to Firebase π₯. Therefore, we have to configure where is the our Firebase.
The code below is AngularFireModule code from @angular/fire. It passes parameters to config providers - FirebaseOptionsToken and FirebaseNameOrConfigToken.
const FirebaseAppProvider = {
provide: FirebaseApp,
useFactory: _firebaseAppFactory,
deps: [
FirebaseOptionsToken,
[new Optional(), FirebaseNameOrConfigToken]
]
};
@NgModule({
providers: [ FirebaseAppProvider ],
})
export class AngularFireModule {
static initializeApp(options: FirebaseOptions, nameOrConfig?: string | FirebaseAppConfig) { // Pass options, nameOrConfig values here <----
return {
ngModule: AngularFireModule,
providers: [
{ provide: FirebaseOptionsToken, useValue: options },
{ provide: FirebaseNameOrConfigToken, useValue: nameOrConfig }
]
}
}
}
AngularFireModule’s full code\
When we use Firebase services such as AngularFireDatabase, AngularFireAuth, we can use it directly without specifing apiKey, authDomain, and databaseURL every times. It has already resolved values from tokens that we configure (register) in the module.
@Injectable()
export class AppGuard implements CanActivate {
constructor(
private db: AngularFireDatabase,
private auth: AngularFireAuth,
private router: Router,
) { }
canActivate(): Observable<boolean | UrlTree> {
return this.auth.authState.pipe(switchMap(user => {
return this.db.object(`...`).valueChanges().pipe(...)
}
}
}
In addition, newcomers are able to read configuration at the module easily.
Read more about Dependency Injection at angular.io
entryComponents
Define components to tell Angular Compiler that we are going to use these components explicitly for creating component factories, dynamic loading at Run-time (Imperatively). Please don’t tree Shake these components out.
For components that we use in the template <angular-th-company-card></angular-th-company-card>
, Angular compiler understand how it be used because of referenceing. Angular compiler is then able to inline instantiation. (Statically, Declaratively)
However Dynamic Component Loader (Load Component at Runtime) needs entryComponents.
The clear example is MatDialog (Material Dialog). We have to explicitly define MatDialog component in order to open the dialog.
@NgModule({
imports: [
// ...
MatDialogModule
],
declarations: [
AppComponent,
ExampleDialogComponent
],
entryComponents: [
ExampleDialogComponent // Must add this Component, however for Ivy, it is not neccessary anymore
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
Another example is loading Component in Routes (RouterModule). It uses component factory also because of dynamic loading at runtime. However, we don’t have to define components in entryComponents because RouterModule do it for us during compilation.
const routes: Routes = [
{
path: 'a',
component: ComponentA
},
{
path: 'b',
component: ComponentB
}
];
@NgModule({
imports: [
RouterModule.forRoot(routes)
],
exports: [RouterModule],
})
export class AppRoutingModule {}
However, we don’t need entryComponents anymore in Ivy. Check below link
refactor(core): deprecate entryComponents #33205
The another interesting story is when we declare and export ComponentA, ComponentB, and Component C in ModuleA, then we import ModuleA into AppModule. However, we don’t use ComponentA, ComponentB, ComponentC in templates or controllers. The Angular Compiler will not include these components in the bundle.
For example, we use CustomMatModule that declares and exports MatButton and MatCard. Even though we import this CustomMatModule, we don’t need to pay bundle size’s cost if we don’t reference or use them in the template or controllers.
However, my best practice is that I do always import what I actually needs. It will be difficult to know what dependencies we are using when project is complicated if we import all modules.
@NgModule({
imports: [
MatButtonModule,
MatCardModule,
...
],
exports: [
MatButtonModule,
MatCardModule,
...
], // Even though these are exported, we don't have to pay if we not using it
declarations: []
})
export class CustomMatModule { }
bootstrap
Define Component to start the App (Bootstrap) - Root Component or AppComponent Bootstrap Component will be automatically added into entryComponents.
Here is the example in AppModule.
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
schemas
Define Schema to allow compilation scope in NgModule. There are two values - NO_ERRORS_SCHEMA and CUSTOM_ELEMENTS_SCHEMA.
NO_ERRORS_SCHEMA - Tell Angular Compiler to allow all elements and properties
For Shallow Testing, we want to test Angular Template by ignoring dependencies of components (Don’t care template errors about what dependencies they need)
We can configure module to make Angular components behaving as simple DOMs.
The below example is to prepare ConversationsCmp for shallow testing.
We just want to test that it renders texts correctly. However, the parent of texts is <mat-card></mat-card>
or MatCardComponent. We don’t really care it. We just want to check there is right text inside.
When we create TestBed, we have to configure the module with schemas NO_ERRORS_SCHEMA
to ignore the error from not importing MatCardModule
. Angular Compiler will be clear about the scope of MatCardComponent
and the test is passed.
<mat-card *ngFor="let c of conversations | async" [routerLink]="[c.id]">
<h3>
<a [routerLink]="[c.id]">{{c.title}}</a>
</h3>
<p>
<span class="light">{{c.user.name}} [{{c.user.email}}]</span>
</p>
</mat-card>
@Component({templateUrl: 'conversations.html'})
export class ConversationsCmp {
folder: Observable<string>;
conversations: Observable<Conversation[]>;
constructor(route: ActivatedRoute) {
this.folder = route.params.pluck<string>('folder');
this.conversations = route.data.pluck<Conversation[]>('conversations');
}
}
describe('ConversationsCmp', () => {
let params: BehaviorSubject<string>;
let data: BehaviorSubject<any>;
beforeEach(async(() => {
params = of({
folder: 'inbox'
});
data = of({
conversations: [
{ id: 1, title: 'On the Genealogy of Morals by Nietzsche', user: {name: 'Kate', email: 'katez@example.com'} },
{ id: 2, title: 'Ethics by Spinoza', user: {name: 'Corin', email: 'corin@example.com'} }
]
});
TestBed.configureTestingModule({
declarations: [ConversationsCmp],
providers: [
{ provide: ActivatedRoute, useValue: {params, data} }
],
// Tell compiler to not show error for unknown elements and attributes
schemas: [NO_ERRORS_SCHEMA]
});
TestBed.compileComponents();
}));
it('updates the list of conversations', () => {
const f = TestBed.createComponent(ConversationsCmp);
f.detectChanges();
expect(f.debugElement.nativeElement).toHaveText('inbox');
expect(f.debugElement.nativeElement).toHaveText('On the Genealogy of Morals');
expect(f.debugElement.nativeElement).toHaveText('Ethics');
params.next({
folder: 'drafts'
});
data.next({
conversations: [
{ id: 3, title: 'Fear and Trembling by Kierkegaard', user: {name: 'Someone Else', email: 'someonelse@example.com'} }
]
});
f.detectChanges();
expect(f.debugElement.nativeElement).toHaveText('drafts');
expect(f.debugElement.nativeElement).toHaveText('Fear and Trembling');
});
});
CUSTOM_ELEMENTS_SCHEMA - Tell Angular Compiler to allow Non-Angular elements and properties with dash case. Dash case is convention of custom elements
Angular always understands that Custom HTML Tag is Angular Component. When it encounter weird elements that out of scopes, the compiler then throw the error. To use custom elements, we have to enable Angular Compiler compiler to understand custom elements.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA] // <-- Here!
imports: [BrowserModule, ReactiveFormsModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
Read more about Custom Element in Angular
Summary of NgModule
- Tell Angular Compiler how to compile Components, Templates, Directives, Pipes
- Define Components, Directives, Pipes to be public via metadata that is exports in order to let other modules access and exploit it.
- Add Services or Providers for Dependency Injection in Component.
Before ending this blog, I have a quiz β‘ for you.
Angular App π °οΈ has been splited into feature modules already.
CEO π want telephone’s input π in Angular App to be masked from 0999999999 to 099-999-999 For every telephone’s input in the App
We then search for the libraries in Google, and we found ngx-mask. π¨π»
We have already installed npm install --save ngx-mask
.
ngx-mask Github has installation guide by importing Module like below.
import { NgxMaskModule } from 'ngx-mask'
export const options: Partial<IConfig> | (() => Partial<IConfig>);
@NgModule({
(...)
imports: [
NgxMaskModule.forRoot(options)
]
(...)
})
My question is where should import NgxMaskModule
, and how?
- import NgxMaskModule.forRoot(options) at AppModule
- import NgxMaskModule.forRoot(options) at AppModule and all Feature Module
- import NgxMaskModule.forRoot(options) at AppModule and import NgxMaskModule at Feature Module
- import NgxMaskModule at AppModule and all Featured Module
- import NgxMaskModule at AppModule and import NgxMaskModule.forRoot(options) all Feature Module
- import NgxMaskModule at AppModule
And developers will be able to use below codes without error.
<input type="text" mask="000-000-000">
<span>{{phone | mask: '000-000-0000'}}</span>
Please share π this blog for other Angular members.
See you next article π. Bye. π
This article uses Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).