Introducing Angular Modules - Feature Modules
The @NgModule
is a new decorator that has recently been added in Angular 2. NgModule
defines an Angular Module, which (from the official docs) are defined as "Angular Modules help organize an application into cohesive blocks of functionality."
This post is the third in a series that introduce @NgModule
and its role in creating Angular 2 apps. In this post I'll discuss some motivations for creating feature modules.
Angular Modules Series
- Introducing NgModule and the Root Module
- Routing Module
- Feature Modules
- Shared/Core Modules
- Eager and Lazy Loading
I'm currently updating my Angular 2 First Look course on Pluralsight for this and other new topics. Be sure to check back later this Fall.
Features
Often our apps have discrete sets of related functionality. Customers, orders, administration, and login/logout are all examples of areas of our apps that, when linked together, make our app. Basically, we break down our app into smaller pieces. Let's refer to these pieces as features.
It makes sense that some of these features have functionality that is not intended to be exposed outside of the feature. We also may want to add a feature to the app as a whole unit at a later time. Or maybe the feature is there and we want to lazy load it when the user decides it is time to visit the feature.
These are all reasons why we might want to divide our app into feature areas. Many technologies have this concept in different forms. Angular 2 addresses this with @NgModule
.
Identifying Features
Let's revisit where we last left our app by looking at its root module. Notice that we have various components in our declarations
. This could easily grow to a long list in even a medium sized app. But also notice that many of these declarations
can be logically grouped together. These are candidates for features.
// app.module.ts
@NgModule({
imports: [BrowserModule, FormsModule, HttpModule],
declarations: [
// Root Feature
AppComponent,
NavComponent,
// Shared Widgets Feature
ButtonComponent,
DateComponent,
// Character Feature
CharactersListComponent,
CharactersDetailComponent,
// Vehicle Feature
VehicleListComponent,
VehicleSelectionDirective,
VehicleSortingPipe
],
providers: [
LoggerService,
VehicleService,
UserProfileService
],
bootstrap: [AppComponent],
})
export class AppModule { }
We identify features by first looking for sets of components, pipes, directives, and services that related to each other. Then if these are not used by other places, it becomes a stronger case for a feature module. Let's examine each.
Our root module makes sense for our AppComponent
and NavComponent
. These are needed everywhere in our app visually and for logic. They are also needed as soon as our app is loaded. If we had a lot of declarations
that fit this description of NavComponent
we might consider creating another feature too, for navigation. But with only one, we can leave it in the root module.
Shared Features
Our ButtonComponent
and DateComponent
are widgets that we will use in many places across our app. We want to make multiple instances of them and for these to be available everywhere. We could leave these in the root module, just like we did for the NavComponent
. However, these are slightly different in that we intend to use these many times over, with multiple instances appearing the a component. We very likely will have many other widgets, so this list will grow. These shared components are a logical place to create a SharedModule
as a feature module.
Let's create a new @NgModule
for this feature.
// app/shared/shared.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [CommonModule, FormsModule],
exports: [
CommonModule, FormsModule,
ButtonComponent, DateComponent
],
declarations: [ButtonComponent, DateComponent]
})
export class SharedModule { }
Notice the @SharedModule
declares the components and also exports them. Exporting them allows others to use those components, which we intend to do in our app's templates.
We modify the root module to no longer declare these shared components and instead we import the SharedModule
. This becomes very useful when our app grows to have 5, 10 or more shared components.
Our components in the root module can still use DateComponent
and ButtonComponent
in their templates.
// app/app.module.ts
@NgModule({
imports: [
BrowserModule, FormsModule, HttpModule,
SharedModule // Shared feature
],
declarations: [
// Root Feature
AppComponent,
NavComponent,
// Character Feature
CharactersListComponent,
CharactersDetailComponent,
// Vehicle Feature
VehicleListComponent,
VehicleSelectionDirective,
VehicleSortingPipe
],
providers: [
LoggerService,
VehicleService,
UserProfileService
],
bootstrap: [AppComponent],
})
export class AppModule { }
Characters Feature
We have another opportunity for creating a feature by grouping the characters. It is likely we will have more than two components plus some pipes or directives too. Sometimes in our apps we'll identify these features are areas of an app where we route to. This is a very discrete feature area that we'll make into a CharactersModule
.
We already had routing for our characters in our root module's app.routing.ts
. Let's move that to our new CharactersModule
. We'll create a folder for our feature module and a new file app/characters/characters.routing.ts
. We'll also remove these routes from the app.routing.ts
. Effectively, we're just moving the route setup for this feature area where it belongs.
// app/characters/characters.routing.ts
// ...
const routes: Routes = [
{ path: 'characters', component: CharactersListComponent },
{ path: 'character/:id', component: CharactersDetailComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class CharactersRoutingModule { }
export const routedComponents = [CharactersListComponent, CharactersDetailComponent];
Notice we use the forChild()
function here and not the forRoot()
. When we create feature modules, we always use forChild()
as the forRoot()
function is reserved for the app's root module.
Now that we moved routing, let's create the feature module in app/characters/characters.module.ts
. Notice it is pretty small as it imports the CharactersRoutingModule
routing module we just created and declares the routedComponents
. These are just a convention I like to use to grab a set of the routed components, instead of having to re-import each component, one by one.
// app/characters/characters.module.ts
// ...
@NgModule({
imports: [CharactersRoutingModule, SharedModule],
declarations: [routedComponents]
})
export class CharactersModule { }
Our character feature's component want to use the shared features including the ButtonComponent
. Modules do no inherit each other, so even though the CharactersModule
is going to be imported by the AppModule
(which imports the SharedModule
already), the CharactersModule
cannot access the shared features ... unless we import SharedModule
.
Shared features modules must be imported by any module expecting to use it's declarables.
Don't worry though, if we forget to import the SharedModule
Angular will throw an error telling us it does not recognize the ButtonComponent
's template (e.g. my-button
) when it sees it.
Now we refactor our app's root module to import the feature and we remove the individual declarations of its components.
// app.module.ts - revised
@NgModule({
imports: [
BrowserModule, FormsModule, HttpModule,
SharedModule, CharactersModule
],
declarations: [
// Root Feature
AppComponent,
NavComponent,
// Vehicle Feature - soon!
VehicleListComponent,
VehicleSelectionDirective,
VehicleSortingPipe
],
providers: [
LoggerService,
VehicleService,
UserProfileService
],
bootstrap: [AppComponent],
})
export class AppModule { }
Feature Module Providers
Our app needs have changed a little and we need a service to gather a list of characters and get a single character for the CharacterListComponent
and CharactersDetailComponent
. We have to decide which module to put the service and where to provide it. Let's think about how we should approach this.
We will only use our service by these two components. Therefore, it makes sense that we place our new CharactersService
in the CharactersModule
.
Now we have to decide where to provide the service. We can either provide it in a component or in its module. If we provide it in a component, we'll need to do this in each component that uses it (for our example). Our list and detail components are separated and not in a parent-child association. If we had a parent component (which we could create) such as CharacterLandingComponent
that had the other two as children, we could provide it there and the child components would be able to access it. This is due to how Angular's injectors work. Angular injectors are hierarchical and exist at each component level. Clearly, this is a lot to think about :)
There is an easier way and one with more possibilities.
If we provide the CharactersService
in the CharactersModule
, it is accessible to all components in this module. The caveat to be aware of is that there is no module level injector ... but there is one available at the root module.
When we provide a service in a feature module that is eagerly loaded by our app's root module, it is available for everyone to inject.
We'll provide the service in our eagerly module, thus making it available everywhere. We may not use it elsewhere, but it is available if needed.
// app/characters/characters.module.ts
// ...
@NgModule({
imports: [CharactersRoutingModule, SharedModule],
declarations: [routedComponents],
providers: [CharactersService]
})
export class CharactersModule { }
We'll touch on providers more later. There is a great section in the Angular docs about this topic here.
Next
We learned how to make 2 different kinds of feature modules. We created a SharedModule
which offers shared features that we'll expect to use in lots of places with multiple instances. This is ideal for widgets or pipes. We also created a feature module for characters, which we route to and eagerly load.
In the upcoming posts we'll explore how to reuse services across the app's modules and how to lazily load our modules.
Ultimate Angular 2 Workshop
If you want to learn more about Angular 2 and NgModule
, come join Dan Wahlin and I at the Ultimate Angular 2 Workshop on October 6 and 7, in Ft Lauderdale, Florida
This is an immersive two day Angular 2 hands-on workshop that is custom tailored to help you jump start your skills with Angular 2.