Skip to content

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 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.

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.

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:

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}

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. Without lazy loading the routes might look like 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),
},

Now instead of loading the HomeComponent directly, we dynamically load the HomeModule. Once this is loaded, it will use the routing information from the HomeModule:

RouterModule.forChild([
{
path: '',
component: HomeComponent,
},
]),

and display the HomeComponent. Now, everything in the HomeModule will load according to whatever PreloadingStrategy we are using. By default, Angular will not preload modules, so the HomeModule will only be loaded when we try to access it. However, we can also use alternate strategies, like letting the main application load first, and then start preloading other modules in the background. This can help speed up startup times, but also won’t cause delays when we try to navigate to a route that hasn’t been loaded yet.

Aside from the ability to lazy load, it is also just generally nicer to have concerns split up in the application rather than having one giant AppModule file that does everything.

SCAMs and Modules for Everything!

What we have just done has historically been a very popular approach to modules. Each feature has a module, and we declare/provide/import whatever we need for that feature in that one module file.

However, there is some awkwardness to this approach. If we are bundling up a bunch of features into a module, then it becomes important to consider what should be bundled with what. If we want to use anything from one module, then we need to import the entire module.

Consider this: we have our fancy RandomColor directive we declared inside of our home module:

@NgModule({
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
component: HomeComponent,
},
]),
],
declarations: [HomeComponent, WelcomeComponent, ReversePipe, RandomColor],
})
export class HomeModule {}

This means that only things within the home feature will be able to use this directive. What happens if we also want to use this on the settings page? You would be sensible for thinking that you would just add it to the declarations for the settings module as well, e.g:

@NgModule({
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
component: SettingsComponent,
},
]),
],
declarations: [SettingsComponent, RandomColor],
})
export class SettingsModule {}

But you can not do this. It will give you the following error:

error NG6007: The Directive 'RandomColor' is declared by more than one NgModule.

A component/pipe/directive can only be declared in one module. If we want to use this directive in multiple modules, then it needs to be part of a separate module that we import into multiple modules. Unlike a declarable (i.e. a component/directive/pipe), we can import the same module into multiple different modules. If this sounds unnecessarily complex you are not the first person to think that way — it’s part of what makes standalone components such an appealing option. The barrier to understanding is way lower. Once you do wrap your head around modules they aren’t so bad, but getting there can be difficult.

So, if we want to use our directive in multiple different modules, a (historically) common approach is to create a SharedModule like this:

SharedModule

@NgModule({
declarations: [RandomColor, ReversePipe],
exports: [RandomColor, ReversePipe]
})

We can add whatever we want to share with the entire application into this module, and then we can import the SharedModule into any other module that needs to make use of something in it:

HomeModule

@NgModule({
imports: [
SharedModule,
CommonModule,
RouterModule.forChild([
{
path: '',
component: HomeComponent,
},
]),
],
declarations: [HomeComponent, WelcomeComponent],
})
export class HomeModule {}

SettingsModule

@NgModule({
imports: [
SharedModule,
CommonModule,
RouterModule.forChild([
{
path: '',
component: SettingsComponent,
},
]),
],
declarations: [SettingsComponent],
})
export class SettingsModule {}

No errors now and we can access the directive and pipe inside of both features! However, this is still somewhat awkward. We can keep adding things to this SharedModule but now any component that wants to use anything from the SharedModule needs to import everything from the SharedModule.

You might try to break the SharedModule up into multiple logically separated modules but getting this right can be hard. This is where the approach of SCAMs or Single Component Angular Modules comes in. The idea is that for every component/directive/pipe you create, you also create a module specifically for just that one component/directive/pipe.

For our directive, we would modify it to look like this:

import { Directive, HostBinding, NgModule } from '@angular/core';
@Directive({
selector: '[randomColor]',
})
export class RandomColor {
@HostBinding('style.backgroundColor') color = `#${Math.floor(
Math.random() * 16777215
).toString(16)}`;
}
@NgModule({
declarations: [RandomColor],
exports: [RandomColor],
})
export class RandomColorModule {}

It declares itself in the module, and importantly it also exports itself. This means that any module that imports this module will be able to use the directive (because it is being exported from this module). This is the same basic idea as a SharedModule except each module just has one thing in it. Then, we can just import whatever we need into each module (and it is fine to do this in multiple different modules):

@NgModule({
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
component: SettingsComponent,
},
]),
RandomColorModule,
],
declarations: [SettingsComponent],
})
export class SettingsModule {}

This is a nice approach to use generally because you no longer have to think about how to bundle related declarables together, and you can just use them wherever you need them.

This architecture is basically approximating part of the benefit of standalone components.

Recap

For each of these questions, assume that standalone components are not being used except for where they are explicitly mentioned. Also assume that module refers to an @NgModule.

    If we want to make a component available to use within a module we can…

    Which of the following contains an exhaustive list of all declarables (i.e. they can be added to the declarations array of a module)?

    What best describes what happens when we import one module into another module?

    Which is the best description of the SCAM approach?

    Why would we export something from a module?