In Angular, a module is a mechanism to group components, directives, pipes, and services that are related, in such a way that can be combined with other modules to create an application. An Angular application can be compared to a puzzle where each piece (or each module) is needed to be able to see the full picture.
Another analogy to understand Angular modules is classes. In a class, we can define public or private methods. The public methods are the APIs that other parts of our code can use to interact with it while the private methods are implementation details that are hidden. In the same way, a module can export or hide components, directives, pipes and services. The exported elements are meant to be used by other modules, while the ones that are not exported (hidden) are just used inside the module itself and cannot be directly accessed by other modules of our application.
To be able to define modules we have to use the decorator NgModule.
In the example above, we have turned the class AppModule into an Angular module just by using the NgModule decorator. The NgModule decorator requires at least three properties: imports, declarations and bootstrap. The property imports expects an array of modules. Here's where we define the pieces of our puzzle (our application). The property declarations expects an array of components, directives and pipes that are part of the module. The bootstrap property is where we define the root component of our module.
Even though this property is also an array, 99% of the time we are going to define only one component. There are very special circumstances where more than one component may be required to bootstrap a module but we are not going to cover those edge cases here.
Here's how a basic module made up of just one component would look like.
app/app.component.ts
- import { Component } from '@angular/core';
- @Component({
- selector: 'app-root',
- template: '<h1>My Angular App</h1>'
- })
- export class AppComponent {}
app/app.module.ts
- import { NgModule } from '@angular/core';
- import { BrowserModule } from '@angular/platform-browser';
- import { AppComponent } from './app.component';
- @NgModule({
- imports: [BrowserModule],
- declarations: [AppComponent],
- bootstrap: [AppComponent]
- })
- export class AppModule { }
The file app.component.ts is just a "hello world" component, nothing interesting there. On the other hand, the file app.module.ts is following the structure that we've seen before for defining a module but in this case, we are defining the modules and components that we are going to be using. The first thing that we notice is that our module is importing the BrowserModule as an explicit dependency. The BrowserModule is a built-in module that exports basic directives, pipes and services. Unlike previous versions of Angular, we have to explicitly import those dependencies to be able to use directives like *ngFor or *ngIf in our templates.
Given that the root (and only) component of our module is the AppComponent, we have to list it in the bootstrap array. Because in the declarations property we are supposed to define all the components or pipes that make up our application, we have to define the AppComponent again there too. Before moving on, there's an important clarification to make. There are two types of modules, root modules and feature modules.
In the same way, in a module, we have one root component and many possible secondary components. In an application, we only have one root module and zero or many feature modules. To be able to bootstrap our application, Angular needs to know which one is the root module. An easy way to identify a root module is by looking at the imports property of its NgModule decorator. If the module is importing the BrowserModule then it's a root module, if instead is importing the CommonModule then it is a feature module. As developers, we need to take care of importing the BrowserModule in the root module and instead, import the CommonModule in any other module we create for the same application. Failing to do so might result in problems when working with lazy loaded modules as we are going to see in following sections. By convention, the root module should always be named AppModule.
Bootstrapping an Application
To bootstrap our module based application, we need to inform Angular which one is our root module to perform the compilation in the browser. This compilation in the browser is also known as "Just in Time" (JIT) compilation.
main.ts
- import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
- import { AppModule } from './app/app.module';
-
- platformBrowserDynamic().bootstrapModule(AppModule);
It is also possible to perform the compilation as a build step of our workflow. This method is called "Ahead of Time" (AOT) compilation and will require a slightly different bootstrap process that we are going to discuss in another section.
Adding Components, Pipes and Services to a Module
In the previous section, we learned how to create a module with just one component but we know that is hardly the case. Our modules are usually made up of multiple components, services, directives and pipes. In this chapter we are going to extend the example we had before with a custom component, pipe and service.
Let's start by defining a new component that we are going to use to show credit card information.
credit-card.component.ts
- import { Component, OnInit } from '@angular/core';
- import { CreditCardService } from './credit-card.service';
-
- @Component({
- selector: 'app-credit-card',
- template: `
- <p>Your credit card is: {{ creditCardNumber | creditCardMask }}</p>
- `
- })
- export class CreditCardComponent implements OnInit {
- creditCardNumber: string;
-
- constructor(private creditCardService: CreditCardService) {}
-
- ngOnInit() {
- this.creditCardNumber = this.creditCardService.getCreditCard();
- }
- }
This component is relying on the CreditCardService to get the credit card number, and on the pipe creditCardMask to mask the number except the last 4 digits that are going to be visible.
credit-card.service.ts
- import { Injectable } from '@angular/core';
- @Injectable()
- export class CreditCardService {
- getCreditCard(): string {
- return '2131313133123174098';
- }
- }
credit-card-mask.pipe.ts
- import { Pipe, PipeTransform } from '@angular/core';
- @Pipe({
- name: 'creditCardMask'
- })
- export class CreditCardMaskPipe implements PipeTransform {
- transform(plainCreditCard: string): string {
- const visibleDigits = 4;
- let maskedSection = plainCreditCard.slice(0, -visibleDigits);
- let visibleSection = plainCreditCard.slice(-visibleDigits);
- return maskedSection.replace(/./g, '*') + visibleSection;
- }
- }
With everything in place, we can now use the CreditCardComponent in our root component.
app.component.ts
- import { Component } from "@angular/core";
- @Component({
- selector: 'app-root',
- template: `
- <h1>My Angular App</h1>
- <app-credit-card></app-credit-card>
- `
- })
- export class AppComponent {}
Of course, to be able to use this new component, pipe and service, we need to update our module, otherwise Angular is not going to be able to compile our application.
app.module.ts
- import { NgModule } from '@angular/core';
- import { BrowserModule } from '@angular/platform-browser';
-
- import { AppComponent } from './app.component';
-
- import { CreditCardMaskPipe } from './credit-card-mask.pipe';
- import { CreditCardService } from './credit-card.service';
- import { CreditCardComponent } from './credit-card.component';
-
- @NgModule({
- imports: [BrowserModule],
- providers: [CreditCardService],
- declarations: [
- AppComponent,
- CreditCardMaskPipe,
- CreditCardComponent
- ],
- bootstrap: [AppComponent]
- })
- export class AppModule { }
Notice that we have added the component CreditCardComponent and the pipe CreditCardMaskPipe to the declarations property, along with the root component of the module AppComponent. In the other hand, our custom service is configured with the dependency injection system with the providersproperty.
Be aware that this method of defining a service in the providers property should only be used in the root module. Doing this in a feature module is going to cause unintended consequences when working with lazy loaded modules.
Creating a Feature Module
When our root module starts growing, it starts to be evident that some elements (components, directives, etc.) are related in a way that almost feel like they belong to a library that can be "plugged in".
In our previous example, we started to see that. Our root module has a component, a pipe and a service that its only purpose is to deal with credit cards. What if we extract these three elements to their own feature module and then we import it into our root module?
We are going to do just that. The first step is to create two folders to differentiate the elements that belong to the root module from the elements that belong to the feature module.
Notice how each folder has its own module file: app.module.ts and credit-card.module.ts. Let's focus on the latter first.
credit-card/credit-card.module.ts
- import { NgModule } from '@angular/core';
- import { CommonModule } from '@angular/common';
-
- import { CreditCardMaskPipe } from './credit-card-mask.pipe';
- import { CreditCardService } from './credit-card.service';
- import { CreditCardComponent } from './credit-card.component';
-
- @NgModule({
- imports: [CommonModule],
- declarations: [
- CreditCardMaskPipe,
- CreditCardComponent
- ],
- providers: [CreditCardService],
- exports: [CreditCardComponent]
- })
- export class CreditCardModule {}
Our feature CreditCardModule it's pretty similar to the root AppModule with a few important differences:
- We are not importing the BrowserModule but the CommonModule. If we see the documentation of the BrowserModule here, we can see that it's re-exporting the CommonModule with a lot of other services that helps with rendering an Angular application in the browser. These services are coupling our root module with a particular platform (the browser), but we want our feature modules to be platform independent. That's why we only import the CommonModule there, which only exports common directives and pipes. When it comes to components, pipes and directives, every module should import its own dependencies disregarding if the same dependencies were imported in the root module or in any other feature module. In short, even when having multiple feature modules, each one of them needs to import the CommonModule.
- We are using a new property called exports. Every element defined in the declarations array is private by default. We should only export whatever the other modules in our application need to perform its job. In our case, we only need to make the CreditCardComponent visible because it's being used in the template of the AppComponent.
app/app.component.ts
- ...
- @Component({
- ...
- template: `
- ...
- <app-credit-card></app-credit-card>
- `
- })
- export class AppComponent {}
We are keeping the CreditCardMaskPipe private because it's only being used inside the CreditCardModule and no other module should use it directly. We can now import this feature module into our simplified root module.
app/app.module.ts
- import { NgModule } from '@angular/core';
- import { BrowserModule } from '@angular/platform-browser';
-
- import { CreditCardModule } from '../credit-card/credit-card.module';
- import { AppComponent } from './app.component';
-
- @NgModule({
- imports: [
- BrowserModule,
- CreditCardModule
- ],
- declarations: [AppComponent],
- bootstrap: [AppComponent]
- })
- export class AppModule { }
At this point we are done and our application behaves as expected.
Services and Lazy Loaded Modules
Here's the tricky part of Angular modules. While components, pipes and directives are scoped to its module unless explicitly exported, services are globally available unless the module is lazy loaded. It's hard to understand that at first so let's try to see what's happening with the CreditCardService in our example. Notice first that the service is not in the exports array but in the providers array. With this configuration, our service is going to be available everywhere, even in the AppComponent which lives in another module. So, even when using modules, there's no way to have a "private" service unless... the module is being lazy loaded.
When a module is lazy loaded, Angular is going to create a child injector (which is a child of the root injector from the root module) and will create an instance of our service there. Imagine for a moment that our CreditCardModule is configured to be lazy loaded. With our current configuration, when the application is bootstrapped and our root module is loaded in memory, an instance of the CreditCardService (a singleton) is going to be added to the root injector. But, when the CreditCardModule is lazy loaded sometime in the future, a child injector will be created for that module with a new instance of the CreditCardService. At this point we have a hierarchical injector with two instances of the same service, which is not usually what we want.
Think for example of a service that does the authentication. We want to have only one singleton in the entire application, disregarding if our modules are being loaded at bootstrap or lazy loaded. So, in order to have our feature module's service only added to the root injector, we need to use a different approach.
credit-card/credit-card.module.ts
- import { NgModule, ModuleWithProviders } from '@angular/core';
-
-
- @NgModule({
- imports: [CommonModule],
- declarations: [
- CreditCardMaskPipe,
- CreditCardComponent
- ],
- exports: [CreditCardComponent]
- })
- export class CreditCardModule {
- static forRoot(): ModuleWithProviders {
- return {
- ngModule: CreditCardModule,
- providers: [CreditCardService]
- }
- }
- }
Different than before, we are not putting our service directly in the property providers of the NgModuledecorator. This time we are defining a static method called forRoot where we define the module and the service we want to export. With this new syntax, our root module is slightly different.
app/app.module.ts
-
-
- @NgModule({
- imports: [
- BrowserModule,
- CreditCardModule.forRoot()
- ],
- declarations: [AppComponent],
- bootstrap: [AppComponent]
- })
- export class AppModule { }
Can you spot the difference? We are not importing the CreditCardModule directly, instead what we are importing is the object returned from the forRoot method, which includes the CreditCardService. Although this syntax is a little more convoluted than the original, it will guarantee us that only one instance of the CreditCardService is added to the root module. When the CreditCardModule is loaded (even lazy loaded), no new instance of that service is going to be added to the child injector. As a rule of thumb, always use the forRoot syntax when exporting services from feature modules, unless you have a very special need that requires multiple instances at different levels of the dependency injection tree.