NgModules, Routing, and Standalone Components
NOTE: This lesson is dedicated to explaining how to build Angular applications if you are using NgModules instead of standalone components. We will not be using them in this course, but it is still worthwhile to read through this lesson even if you don’t intend to use them at all. It will provide you with additional context, and there is a high chance you will run into Angular code that uses them at some point.
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
at the top of
files). 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 discussed in our example app:
@NgModule({
declarations: [
AppComponent,
HomeComponent,
SettingsComponent,
WelcomeComponent,
RandomColor,
ReversePipe,
],
imports: [BrowserModule, AppRoutingModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
By having all of these components, directives, and pipes declared within the
same @NgModule
, they will all be aware of each others existence and can use
each other.
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.
NOTE: We still get lazy loading with standalone components without needing a module
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.
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.
NOTE: The imports
array of an @NgModule
is generally the same in concept
as the imports
array in standalone components.
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.