3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 6

NgModules, Routing, and Standalone Components

Why you need to understand modules and why you won't need them

STANDARD

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),
  },
STANDARD
Key

Thanks for checking out the preview of this lesson!

You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).