NgModules, Routing, and Standalone Components
Now we are getting into the trickier side of things. Angular’s concept of modules, which are classes decorated with the @NgModule
decorator (not to be confused with standard ES6 modules that we import
and export
). NgModules can be hard to wrap your head around, so if you don’t already have experience with them, don’t expect to understand everything right away. You will learn as you run into walls and errors, and eventually, figure out the peculiarities of modules. When people say Angular has a steep learning curve, modules are probably a significant part of this. I hope this lesson will help to give you a head start, and also give you something to come back and reference.
We’ve already been exposed to this concept a little bit through the root module that we have been working with in our example app:
@NgModule({
declarations: [
AppComponent,
HomeComponent,
SettingsComponent,
WelcomeComponent,
RandomColor,
ReversePipe,
],
imports: [BrowserModule, AppRoutingModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Our experience with this so far has mostly just been as a place to add the components, directives, and pipes we have been creating. We also discussed how this root AppModule
is passed into the bootstrapModule
method in the main.ts
file:
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
Which is what kicks off our whole application. Since our root component AppComponent
is supplied to the bootstrap
property in the @NgModule
it is available for us to use in the index.html
file:
<body>
<app-root></app-root>
</body>
So far, we just have the one module in our application, and everything just goes inside of that one module. This is not what we will typically do. Typically, we would have a module for each feature in our application. We keep using the example of a home page and a settings page. In that example application, we would likely have three modules:
AppModule
HomeModule
SettingsModule
This allows us to create some modularity in our application. The AppModule
can include things that are needed for the application as a whole, the HomeModule
can just include things relevant to the home feature, and the SettingsModule
can just include things relevant to the settings feature.
This is useful from a code organisation perspective, but splitting our application up this way also allows us to lazy load parts of our application. We will discuss this in just a moment, but the general idea is that rather than loading all of the code for the entire application all at once, we could just load the code for the settings feature only when we try to access that feature.
In this lesson, we are going to discuss the anatomy of the @NgModule
itself (because we haven’t talked about all of its common properties yet), and we are also going to discuss the general role of Angular modules. This will tie into a discussion around standalone components which is a newer (still kind of experimental) feature that allows us to build our application without using Angular modules.
The Anatomy of @NgModule
We are not going to discuss every single feature of @NgModule
, just the core concepts we will frequently be using. A useful exercise to do as we discuss this will be to refactor our home feature to use its own @NgModule
instead of just dumping everything into the root module.
First, we can pull all of the components/pipes/directives we created out of the root AppModule
:
app.module.ts
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Now we can create a new module specifically for our home feature. We can create a separate file for this if we want to, e.g. home.module.ts
, but we are going to just define this module in the same file as the HomeComponent
itself:
import { CommonModule } from '@angular/common';
import { Component, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { RandomColor } from './ui/random-color.directive';
import { ReversePipe } from './ui/reverse.pipe';
import { WelcomeComponent } from './ui/welcome.component';
@Component({
selector: 'app-home',
template: `
<app-welcome
[name]="user.name"
(cookiesAccepted)="handleCookies()"
></app-welcome>
<p>I am the home component</p>
<p randomColor>{{ 'reverse me ' | reverse }}</p>
`,
})
export class HomeComponent {
user = {
name: 'Josh',
};
handleCookies() {
console.log('do something');
}
}
@NgModule({
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
component: HomeComponent,
},
]),
],
declarations: [HomeComponent, WelcomeComponent, ReversePipe, RandomColor],
})
export class HomeModule {}
As we have discussed before, an @NgModule
provides a compilation context for the things it contains. You can kind of imagine the @NgModule
above like a box that contains our HomeComponent
and everything else we are declaring or importing. Anything inside of this box is aware of the existence of all of the other things in the same box and can use them.
I’ve added the entire file here for context, but let’s focus on just the module:
@NgModule({
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
component: HomeComponent,
},
]),
],
declarations: [HomeComponent, WelcomeComponent, ReversePipe, RandomColor],
})
export class HomeModule {}
We are now declaring everything we need for our home feature in its own HomeModule
. The concept of the declarations
is the same - we declare any component/directive/pipe we want to be able to use within this module.
What is new here is imports
, or at least we haven’t talked about it yet. In the context of an @NgModule
an import is similar to a declaration, but instead of declaring individual components/pipes/directives we import an entire @NgModule
. Anything that the @NgModule
we are importing exports will be available within our HomeModule
.
To go back to our box analogy, importing a module into another module is like taking the contents of one box (BoxA
) and making them available to another box (BoxB
). But, we don’t just dump the entire contents of BoxA
into BoxB
, we only supply BoxB
with the items that were explicitly exported in BoxA
with the exports
property. We will see this in just a moment.
Both CommonModule
and RouterModule
are modules provided by Angular, and we are importing them into our own module. This means that we are getting access to the stuff inside of CommonModule
and RouterModule
, but only the things those modules export.
If you were to investigate what was inside CommonModule
(this is a great exercise if you want to dig into that) you would find that it exports a bunch of directives and pipes including ngIf
and ngFor
. It is by importing the CommonModule
into our HomeModule
that we will be able to use *ngFor
and *ngIf
in our templates. If we want to use [(ngModel)]
then we would need to import the FormsModule
from Angular.
We can import modules provided by Angular into our own modules, and we can also import our own modules into our own modules to share functionality throughout the app. We will look at a more concrete example of this in the next section.
For now, let’s switch gears to talking about routing and what the RouterModule
is doing here. We’ve actually already seen the RouterModule
, we used it in the app-routing.module.ts
file to supply our routes:
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
NOTE: Notice we both import
and export
the RouterModule
here. We import it because we need access to its functionality in our AppRoutingModule
. We don’t need to export it, but by doing so, we are automatically making RouterModule
available to any other module that imports our AppRoutingModule
. One feature the RouterModule
provides is the ability to use the <router-outlet>
in the template. Our root AppModule
makes use of the <router-outlet>
but it never imports the RouterModule
. However, our AppModule
imports AppRoutingModule
which exports RouterModule
- this means that AppModule
automatically has access to the RouterModule
. If AppRoutingModule
did not export RouterModule
then we would have to manually import it into our AppModule
. I would highly recommend referencing your own example project and tracing this explanation through the code, just reading it probably sounds like a bunch of nonsense.
By calling the .forRoot
static method on the RouterModule
we can configure the routes for our root module. However, notice that in our home feature module we are using .forChild
instead:
@NgModule({
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
component: HomeComponent,
},
]),
],
declarations: [HomeComponent, WelcomeComponent, ReversePipe, RandomColor],
})
export class HomeModule {}
We have our “main” routing set up in the root module, but we can define the routing for each individual feature within their own modules. If we activate the /home
route, from that point on the routing information above will be used. This feature currently just has a single page, so we just have a default path of ''
to display the HomeComponent
but we can (and will later) define more complex routing for features.
The last step of this story is to set up lazy loading. I mentioned before that this is a big benefit of structuring your application into feature modules like this. Currently, we are doing this:
app-routing.module.ts
const routes: Routes = [
{
path: 'home',
component: HomeComponent,
},
{
path: 'settings',
component: SettingsComponent,
},
{
path: '',
redirectTo: 'home',
pathMatch: 'full',
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
We just pass the component we want to display directly to the route. This means that everything is going to be loaded on app load, and we will need to declare everything in the single root AppModule
. Now that our home feature has its own module, we can load that module instead:
{
path: 'home',
loadChildren: () =>
import('./home/home.component').then((m) => m.HomeModule),
},