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

Popular posts from this blog

A complete SignalR with ASP Net Core example with WSS, Authentication, Nginx

Verify dotnet SDK and runtime version installed

SDK-Style project and project.assets.json