Using Angular NgModules for Reusable Code and More

NgModules are a core concept in Angular that are part of every application and help to wire up some important details for the compiler and application runtime. They’re especially useful for organizing code into features, lazy loading routes, and creating reusable libraries.

In this guide, we’re going to cover the primary uses of NgModules with some examples to show you how to use them in your Angular projects! This guide assumes you have a working knowledge of Angular.

JavaScript Modules Aren’t NgModules

Let’s clear the air first about what JavaScript modules are (sometimes called ES6 modules). They’re a language construct that makes it easier to organize your code.

--ADVERTISEMENT--

At their most basic, Javascript modules are JavaScript files that contain either the import or export keywords, and which cause the objects defined inside of that file to be private unless you export it. I encourage you to review the link above for a deeper understanding, but essentially this is a way to organize your code and easily share it, without relying on the dreaded global scope.

When you create an Angular application with TypeScript, any time you use import or export in your source, it’s treated as a JavaScript module. TypeScript is able to handle the module loading for you.

Note: to help keep things clear in this article, I’ll always refer to JavaScript modules and NgModules by their full names.

The Basic NgModule, the AppModule

Let’s start by looking at a basic NgModule that exists in every Angular application, the AppModule (which is generated by default in any new Angular application). It looks something like you see here:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Angular uses decorators to define metadata it needs to know about during compile time. To define an NgModue, you simply add the @NgModule() decorator above a class. The class may not always be empty, but often it is. However, you’ll need to define an object with some properties for the NgModule to do anything.

When the application bootstraps, it needs to be given an NgModule to instantiate. If you look in the main file of your application (also typically called main.ts), you’ll see platformBrowserDynamic().bootstrapModule(AppModule), which is how the application registers and initiates the AppModule (which can be named anything, but is almost always named this).

The Properties of NgModule

The NgModule API documentation page outlines the properties that you can pass when defining an NgModule, but we’ll cover them here as well. They’re all optional, but you’ll need to define values for at least one of them for the NgModule to do anything.

providers

The providers is an array that contains the list of any providers (injectable services) that are available for this NgModule. Providers have a scope, and if they’re listed in a lazy-loaded NgModule, they’re not available outside of that NgModule.

declarations

The declarations array should contain a list of any directives, components, or pipes that this NgModule defines. This makes it possible for the compiler to find these items and ensure they’re bundled properly. If this is the root NgModule, then declarations are available for all NgModules. Otherwise, they’re only visible to the same NgModule.

imports

If your NgModule depends on any other objects from another NgModule, you’ll have to add it to the imports array. This ensures that the compiler and dependency injection system know about the imported items.

exports

Using the exports array, you can define which directives, components, and pipes are available for any NgModule that imports this NgModule. For example, in a UI library you’d export all of the components that compose the library.

entryComponents

Any component that needs to be loaded at runtime has to be added to the list of entryComponents. Essentially, this will create the component factory and store it for when it needs to be loaded dynamically. You can learn more about how to dynamically load components from the documentation.

bootstrap

You can define any number of components to bootstrap when the app is first loaded. Usually you only need to bootstrap the main root component (usually called the AppComponent), but if you had more than one root component, each would be declared here. By adding a component to the bootstrap array, it’s also added to the list of entryComponents and precompiled.

schemas

Schemas are a way to define how Angular compiles templates, and if it will throw an error when it finds elements that aren’t standard HTML or known components. By default, Angular throws an error when it finds an element in a template that it doesn’t know, but you can change this behavior by setting the schema to either NO_ERRORS_SCHEMA (to allow all elements and properties) or CUSTOM_ELEMENTS_SCHEMA (to allow any elements or properties with a - in their name).

id

This property allows you to give an NgModule a unique ID, which you can use to retrieve a module factory reference. This is a rare use case currently.

NgModule Examples

To illustrate the way NgModule is used with Angular, let’s look at a set of examples that show you how to handle various use cases easily.

Feature NgModules

The most basic use case for NgModules besides the AppModule is for Feature NgModules (usually called feature modules, but trying to keep the terms consistent). They help separate individual parts of your application, and are highly recommended. In most ways, they’re the same as the main App NgModule. Let’s take a look at a basic Feature NgModule:

@NgModule({
  declarations: [
    ForumComponent,
    ForumsComponent,
    ThreadComponent,
    ThreadsComponent
  ],
  imports: [
    CommonModule,
    FormsModule,
  ],
  exports: [
    ForumsComponent
  ]
  providers: [
    ForumsService
  ]
})
export class ForumsModule { }

This simple Feature NgModule defines four components, one provider, and imports two modules that are required by the components and service. Together, these comprise the necessary pieces for the forums section of an application.

The items in providers are available to any NgModule that imports the ForumsModule to be injected, but it’s important to understand that each NgModule will get its own instance of that service. This is different from providers listed in the root NgModule, from which you’ll always get the same instance (unless its reprovided). This is where it’s important to understand dependency injection, particularly hierarchical dependency injection. It’s easy to think you’ll get the same instance of a service and change properties on it, but never see the changes elsewhere in the application.

As we learned earlier, the items in declarations are not actually available to be used in other NgModules, because they’re private to this NgModule. To fix this, you can optionally export those declarations you wish to consume in other NgModules, like in this snippet where it exports just the ForumsComponent. Now, in any other Feature NgModules, you could put <app-forums></app-forums> (or whatever the selector for the component is) to display the ForumsComponent in a template.

Another key difference is that ForumsModule imports the CommonModule instead of the BrowserModule. The BrowserModule should only be imported at the root NgModule, but the CommonModule contains the core Angular directives and pipes (such as NgFor and the Date pipe). If your Feature NgModule doesn’t use any of those features, it wouldn’t actually need the CommonModule.

Now, when you want to consume the ForumsModule in your project, you need to import it into your AppModule like you see here:

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    ForumsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

This NgModule is then imported into the main AppModule to load it properly, which includes the items in the ForumsModule providers array and any exported items for consumption in your application.

When you use the Angular CLI, you can easily generate Feature NgModules by running the generator for a new NgModule:

ng generate module path/to/module/feature

You can organize your Feature NgModules any way you see fit, but the general recommendation is to group similar things that are used on the same view. I try to make a small number of Feature NgModules to hold the commonly shared things, and then focus more on NgModules for each major feature of the application.

Lazy Loading NgModules with Routes

Sometimes you want to load code only when the user needs it, and with Angular this is currently possible by using the router and Feature NgModules together. The router has the ability to lazy load NgModules when a user requests a specific route. See this primer on routing with Angular if you’re new to routing.

The best way to start is to create a Feature NgModule for the unique parts of a route. You might even want to group more than one route, if they’re almost always used together. For example, if you have a customer account page with several subpages for managing the account details, more than likely you’d declare them as part of the same NgModule.

There’s no difference in the way you define the NgModule itself, except you’ll need to define some routes with RouterModule.forChild(). You should have one route that has an empty path, which will act like the root route for this Feature NgModule, and all other routes hang from it:

@NgModule({
  declarations: [
    ForumComponent,
    ForumsComponent,
  ],
  imports: [
    CommonModule,
    FormsModule,
    RouterModule.forChild([
      {path: '', component: ForumsComponent},
      {path: ':forum_id', component: ForumComponent}
    ])
  ],
  providers: [
    ForumsService
  ]
})
export class ForumsModule { }

There is an important change in behavior that isn’t obvious related to the way the providers are registered with the application. Since this is a lazy loaded NgModule, providers are not available to the rest of the application. This is an important distinction, and should be considered when planning your application architecture. Understanding how Angular dependency injection works is very important here.

To load the lazy route, the main AppModule defines the path that goes to this Feature NgModule. To do this, you’ll have to update your root router config for a new route. This example shows how to define a lazy loaded route, by giving it a path and loadChildren properties:

const routes: Routes = [
  {
    path: 'forums',
    loadChildren: 'app/forums/forums.module#ForumsModule'
  },
  {
    path: '',
    component: HomeComponent
  }
];

The syntax of the loadChildren property is a string that has the path to the NgModule file (without the file extension), a # symbol, and then the name of the NgModule class: loadChildren: 'path/to/module#ModuleName. Angular uses this to know where to load the file at runtime, and to know the name of NgModule.

The path to the lazy loaded route is defined at the root level of routes, so the lazy loaded NgModule doesn’t even know specifically what the path for its route will be. This makes them more reusable, and makes it possible for the application to know when to lazy load that NgModule. Think of the lazy loaded NgModule defining all routes as relative paths, and the full path is provided by combining the root route and lazy loaded routes.

For example, if you visit the / route in this application, it will load the HomeComponent and the ForumsModule will not be loaded. However, once a user clicks a link to view the forums, it will notice that the /forums path requires the ForumsModule to be loaded, downloads it, and registers the defined routes from it.

Routing NgModules

A common pattern for Angular is to use a separate NgModule to host all of your routes. It’s done for separation of concerns, and is entirely optional. The Angular CLI has support for automatically generating a Routing NgModule when you create a new module by passing the --routing flag:

ng generate module path/to/module/feature --routing

What happens is that you create a standalone NgModule that defines your routes, and then your Feature NgModule imports it. Here’s what a routing NgModule could look like:

const routes: Routes = [
  { path: '', component: ForumsComponent }
];

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

Then you just import it to your ForumsModule like you see here:

@NgModule({
  declarations: [
    ForumComponent,
    ForumsComponent,
  ],
  imports: [
    CommonModule,
    FormsModule,
    ForumsRoutingModule,
  ],
  providers: [
    ForumsService
  ]
})
export class ForumsModule { }

This is largely preference, but it’s a common pattern you should consider. Essentially, it’s another way NgModules are used for code separation.

Singleton services

We’ve seen a couple of concerns about providers where you couldn’t be guaranteed that you’d get the same instance of a service across NgModules unless you only provided it in the root NgModule. There’s a way to define your NgModule so that it can declare providers only for the root NgModule, but not redeclare them for all other NgModules.

In fact, the Angular router is a good example of this. When you define a route in your root NgModule, you use RouterModule.forRoot(routes), but inside of Feature NgModules you use RouterModule.forChild(routes). This pattern is common for any reusable library that needs a single instance of a service (singleton). We can do the same with any NgModule by adding two static methods to our NgModule like you see here:

@NgModule({
  declarations: [
    ForumComponent,
    ForumsComponent,
    ThreadComponent,
    ThreadsComponent
  ],
  imports: [
    CommonModule,
    FormsModule,
  ],
  exports: [
    ForumsComponent
  ]
})
export class ForumsModule {
  static forRoot(): ModuleWithProviders {
    return {
      ngModule: ForumsModule,
      providers: [ForumsService]
    };
  }

  static forChild(): ModuleWithProviders {
    return {
      ngModule: ForumsModule,
      providers: []
    };
  }
}

Then in our AppModule you’d define the import with the forRoot() method, which will return the NgModule with providers. In any other NgModule that imports ForumsModule, you’d use the forChild() method so you don’t declare the provider again (thus creating a new instance):

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    ForumsModule.forRoot()
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

NgModules for grouping NgModules

You can combine a number of other NgModules into a single one, to make it easier to import and reuse. For example, in the Clarity project I work on, we have a number of NgModules that only export other NgModules. For instance, this is the main ClarityModule which actually reexports the other individual NgModules that contain each of the components:

@NgModule({
  exports: [
    ClrEmphasisModule, ClrDataModule, ClrIconModule, ClrModalModule, ClrLoadingModule, ClrIfExpandModule, ClrConditionalModule, ClrFocusTrapModule, ClrButtonModule, ClrCodeModule, ClrFormsModule, ClrLayoutModule, ClrPopoverModule, ClrWizardModule
  ]
})
export class ClarityModule { }

This makes it easy to import many NgModules at once, but it does make it harder for the compiler to know which NgModules are used or not for tree shaking optimizations.

Summary

We’ve gone through a whirlwind tour of NgModules in Angular, and covered the key use cases. The Angular documentation about NgModules is quite in-depth as well, and if you get stuck I suggest reviewing the FAQ.

Sponsors