Angular - Creating a Feature Module

Creating a Feature Module

When our root module start 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.
.
├── app
│ ├── app.component.ts
│ └── app.module.ts
├── credit-card
│ ├── credit-card-mask.pipe.ts
│ ├── credit-card.component.ts
│ ├── credit-card.module.ts
│ └── credit-card.service.ts
├── index.html
└── main.ts
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 modules 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';
/* ...other imports... */
@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 NgModule decorator. 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
/* ...imports... */
@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.

Shared Modules

A shared module is the perfect place to declare components in order to make them reusable: this way, you won’t re-import the same components in every module, you’ll just import the shared module.


That’s awesome! But there’s a problem. 😅 Not with shared modules themselves, they’re fine! There’s a problem with lazy modules!
Lazy modules have a strange behavior: since they have their own injector, if they import a module which provides some services, they’ll create their own instances of those services. Gasp! Does that mean that due to lazy modules’ behavior, we cannot provide services in our shared modules?!
Well, you might think that we already declare our global services in CoreModule, and it’s true, but what if the components declared in the shared module needed some services? What if the module that imports the shared module (perhaps a lazy module?) needs a service from it? We can still work it through. 👍 👍 👍
Angular gives us a special interface we can use to attach services to modules, it’s called ModuleWithProviders, here it is:


What’s interesting is that we can import an object with this particular shape instead of a normal module! Our mission will be the following:
  • In our AppModule, we’re going to import the shared module with the providers attached to it
  • In all other modules, we’ll be importing the module without any provider, since they’ll be already provided in AppModule
So, how do we proceed? It’s simple: we don’t provide our services in the SharedModule metadata; instead, we define a static method inside the module, which returns the SharedModule istelf AND the array of providers!


Awesome! Now, here’s what to do: in the AppModule you can import SharedModule.forRoot(), while in all the other modules you can import SharedModule. This way the services will be provided only at AppModule level (and accessible everywhere) while the other modules still have access to the components.
SHARE

esant technology

1 comments :

  1. I definitely enjoying every little bit of it. It is a great website and nice share. I want to thank you. Good job! You guys do a great blog, and have some great contents. Keep up the good work. https://catcherrors.com/repos/facebook/react

    ReplyDelete