Angular Modules (NgModule)

Angular Modules help organize an application into cohesive blocks of functionality.

An Angular Module is a class adorned with the @NgModule decorator function. @NgModule takes a metadata object that tells Angular how to compile and run module code. It identifies the module's own components, directives and pipes, making some of them public so external components can use them. It may add service providers to the application dependency injectors. And there are more options covered here.

This page explains how to create NgModule classes and how to load them, either immediately when the application launches or later, as needed, via the Router.

Table of Contents

Live examples

This page explains Angular Modules through a progression of improvements to a sample with a "Tour of Heroes" theme. Here's an index to live examples at key moments in the evolution of that sample:

  • A minimal NgModule app
  • The first contact module
  • The revised contact module
  • Just before adding SharedModule
  • The final version

Frequently Asked Questions (FAQs)

This page covers Angular Module concepts in a tutorial fashion.

The companion Angular Module FAQs cookbook offers ready answers to specific design and implementation questions. Read this page first before hopping over to those FAQs.

Angular Modularity

Modules are a great way to organize the application and extend it with capabilities from external libraries.

Many Angular libraries are modules (e.g, FormsModule, HttpModule, RouterModule). Many third party libraries are available as Angular modules (e.g., Material Design, Ionic, AngularFire2).

Angular modules consolidate components, directives and pipes into cohesive blocks of functionality, each focused on a feature area, application business domain, workflow, or common collection of utilities.

Modules can also add services to the application. Such services might be internally-developed such as the application logger. They can come from outside sources such as the Angular router and Http client.

Modules can be loaded eagerly when the application starts. They can also be lazy loaded asynchronously by the router.

An Angular module is a class decorated with @NgModule metadata. The metadata:

  • declare which components, directives and pipes belong to the module.
  • make some of those classes public so that other component templates can use them.
  • import other modules with the components, directives and pipes needed by the components in this module.
  • provide services at the application level that any application component can use.

Every Angular app has at least one module class, the root module. We bootstrap that module to launch the application.

The root module is all we need in a simple application with a few components. As the app grows, we refactor the root module into feature modules that represent collections of related functionality. We then import these modules into the root module.

We'll see how later in the page. Let's start with the root module.

AppModule - the application root module

Every Angular app has a root module class. By convention it's a class called AppModule in a file named app.module.ts.

This AppModule is about as minimal as it gets:

app/app.module.ts (minimal)

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

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

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

The @NgModule decorator defines the metadata for the module. We'll take an intuitive approach to understanding the metadata and fill in details as we go.

This metadata imports a single helper module, BrowserModule, the module every browser app must import.

BrowserModule registers critical application service providers. It also includes common directives like NgIf and NgFor which become immediately visible and usable in any of this modules component templates.

The declarations list identifies the application's only component, the root component, the top of this app's rather bare component tree.

The example AppComponent simply displays a data-bound title:

app/app.component.ts (minimal)

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: '<h1>{{title}}</h1>',
})
export class AppComponent {
  title = 'Minimal NgModule';
}

Lastly, the @NgModule.bootstrap property identifies this AppComponent as the bootstrap component. When Angular launches the app, it places the HTML rendering of AppComponent in the DOM, inside the <my-app> element tags of the index.html

Bootstrapping in main.ts

We launch the application by bootstrapping the AppModule in the main.ts file.

Angular offers a variety of bootstrapping options, targeting multiple platforms. In this page we consider two options, both targeting the browser.

Dynamic bootstrapping with the Just-in-time (JiT) compiler

In the first, dynamic option, the Angular compiler compiles the application in the browser and then launches the app.

app/main.ts (dynamic)

// The browser platform with a compiler
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

// The app module
import { AppModule } from './app.module';

// Compile and launch the module
platformBrowserDynamic().bootstrapModule(AppModule);

The samples in this page demonstrate the dynamic bootstrapping approach.

Try the live example.

Static bootstrapping with the Ahead-Of-time (AoT) compiler

Consider the static alternative which can produce a much smaller application that launches faster, especially on mobile devices and high latency networks.

In the static option, the Angular compiler runs ahead-of-time as part of the build process, producing a collection of class factories in their own files. Among them is the AppModuleNgFactory.

The syntax for bootstrapping the pre-compiled AppModuleNgFactory is similar to the dynamic version that bootstraps the AppModule class.

app/main.ts (static)

// The browser platform without a compiler
import { platformBrowser } from '@angular/platform-browser';

// The app module factory produced by the static offline compiler
import { AppModuleNgFactory } from './app.module.ngfactory';

// Launch with the app module factory.
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);

Because the entire application was pre-compiled, we don't ship the Angular Compiler to the browser and we don't compile in the browser.

The application code downloaded to the browser is much smaller than the dynamic equivalent and it is ready to execute immediately. The performance boost can be significant.

Both the JiT and AoT compilers generate an AppModuleNgFactory class from the same AppModule source code. The JiT compiler creates that factory class on the fly, in memory, in the browser. The AoT compiler outputs the factory to a physical file that we're importing here in the static version of main.ts.

In general, the AppModule should neither know nor care how it is bootstrapped.

Although the AppModule evolves as the app grows, the bootstrap code in main.ts doesn't change. This is the last time we'll look at main.ts.

Declare directives and components

The app evolves. The first addition is a HighlightDirective, an attribute directive that sets the background color of the attached element.

app/highlight.directive.ts

import { Directive, ElementRef, Renderer } from '@angular/core';

@Directive({ selector: '[highlight]' })
/** Highlight the attached element in gold */
export class HighlightDirective {
  constructor(renderer: Renderer, el: ElementRef) {
    renderer.setElementStyle(el.nativeElement, 'backgroundColor', 'gold');
    console.log(
      `* AppRoot highlight called for ${el.nativeElement.tagName}`);
  }
}

We update the AppComponent template to attach the directive to the title:

template: '<h1 highlight>{{title}}</h1>'

If we ran the app now, Angular would not recognize the highlight attribute and would ignore it. We must declare the directive in AppModule.

Import the HighlightDirective class and add it to the module's declarations like this:

declarations: [
  AppComponent,
  HighlightDirective,
],

Add a component

We decide to refactor the title into its own TitleComponent. The component's template binds to the component's title and subtitle properties like this:

app/title.component.html

<h1 highlight>{{title}} {{subtitle}}</h1>

app/title.component.ts

import { Component, Input } from '@angular/core';

@Component({
  moduleId: module.id,
  selector: 'app-title',
  templateUrl: 'title.component.html',
})
export class TitleComponent {
  @Input() subtitle = '';
  title = 'Angular Modules';
}

We rewrite the AppComponent to display the new TitleComponent in the <app-title> element, using an input binding to set the subtitle.

app/app.component.ts (v1)

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: '<app-title [subtitle]="subtitle"></app-title>'
})
export class AppComponent {
  subtitle = '(v1)';
}

Angular won't recognize the <app-title> tag until we declare it in AppModule. Import the TitleComponent class and add it to the module's declarations:

  declarations: [
    AppComponent,
    HighlightDirective,
    TitleComponent,
  ],

Service Providers

Modules are a great way to provide services for all of the module's components.

The Dependency Injection page describes the Angular hierarchical dependency injection system and how to configure that system with providers at different levels of the application's component tree.

A module can add providers to the application's root dependency injector, making those services available everywhere in the application.

Many applications capture information about the currently logged-in user and make that information accessible through a user service. This sample application has a dummy implementation of such a UserService.

app/user.service.ts

import { Injectable } from '@angular/core';

@Injectable()
/** Dummy version of an authenticated user service */
export class UserService {
  userName = 'Sherlock Holmes';
}

The sample application should display a welcome message to the logged in user just below the application title. Update the TitleComponent template to show the welcome message below the application title.

app/title.component.html

<h1 highlight>{{title}} {{subtitle}}</h1>
<p *ngIf="user">
  <i>Welcome, {{user}}</i>
<p>

Update the TitleComponent class with a constructor that injects the UserService and sets the component's user property from the service.

app/title.component.ts

import { Component, Input } from '@angular/core';
import { UserService } from './user.service';

@Component({
  moduleId: module.id,
  selector: 'app-title',
  templateUrl: 'title.component.html',
})
export class TitleComponent {
  @Input() subtitle = '';
  title = 'Angular Modules';
  user = '';

  constructor(userService: UserService) {
    this.user = userService.userName;
  }
}

We've defined and used the service. Now we provide it for all components to use by adding it to a providers property in the AppModule metadata:

app/app.module.ts (providers)

providers: [ UserService ],

Import supporting modules

The app shouldn't welcome a user if there is no user.

Notice in the revised TitleComponent that an *ngIf directive guards the message. There is no message if there is no user.

app/title.component.html (ngIf)

<p *ngIf="user">
  <i>Welcome, {{user}}</i>
<p>

Although AppModule doesn't declare NgIf, the application still compiles and runs. How can that be? The Angular compiler should either ignore or complain about unrecognized HTML.

Angular does recognize NgIf because we imported it earlier. The initial version of AppModule imports BrowserModule.

app/app.module.ts (imports)

imports: [ BrowserModule ],

Importing BrowserModule made all of its public components, directives and pipes visible to the component templates in AppModule. They are ready to use without further ado.

More accurately, NgIf is declared in CommonModule from @angular/common.

CommonModule contributes many of the common directives that applications need including ngIf and ngFor.

BrowserModule imports CommonModule and re-exports it. The net effect is that an importer of BrowserModule gets CommonModule directives automatically.

Many familiar Angular directives do not belong toCommonModule. For example, NgModel and RouterLink belong to Angular's FormsModule and RouterModule respectively. We must import those modules before we can use their directives.

To illustrate this point, we extend the sample app with ContactComponent, a form component that imports form support from the Angular FormsModule.

Add the ContactComponent

Angular Forms are a great way to manage user data entry.

The ContactComponent presents a "contact editor", implemented with Angular Forms in the template-driven form style.

Angular Form Styles

We write Angular form components in either the template-driven form style or the reactive form style.

This sample is about to import the FormsModule from @angular/forms because the ContactComponent is written in the template-driven style. Modules with components written in the reactive style, should import the ReactiveFormsModule instead.

The ContactComponent selector matches an element named <app-contact>. Add an element with that name to the AppComponent template just below the <app-title>:

app/app.component.ts (template)

template: `
  <app-title [subtitle]="subtitle"></app-title>
  <app-contact></app-contact>
`

The ContactComponent has a lot going on. Form components are often complex anyway and this one has its own ContactService, its own custom pipe called Awesome, and an alternative version of the HighlightDirective.

To make it manageable, we place all contact-related material in an app/contact folder and break the component into three constituent HTML, TypeScript, and css files:

app/contact/contact.component.html
<h2>Contact of {{userName}}</h2>
<div *ngIf="msg" class="msg">{{msg}}</div>

<form *ngIf="contacts" (ngSubmit)="onSubmit()" #contactForm="ngForm">
  <h3 highlight>{{ contact.name | awesome }}</h3>
  <div class="form-group">
    <label for="name">Name</label>
    <input type="text" class="form-control" required
      [(ngModel)]="contact.name"
        name="name"  #name="ngModel" >
    <div [hidden]="name.valid" class="alert alert-danger">
      Name is required
    </div>
  </div>
  <br>
  <button type="submit" class="btn btn-default" [disabled]="!contactForm.form.valid">Save</button>
  <button type="button" class="btn" (click)="next()" [disabled]="!contactForm.form.valid">Next Contact</button>
  <button type="button" class="btn" (click)="newContact()">New Contact</button>
</form>
app/contact/contact.component.ts
import { Component, OnInit }      from '@angular/core';

import { Contact, ContactService } from './contact.service';
import { UserService }    from '../user.service';

@Component({
  moduleId: module.id,
  selector: 'app-contact',
  templateUrl: 'contact.component.html',
  styleUrls: ['contact.component.css']
})
export class ContactComponent implements OnInit {
  contact:  Contact;
  contacts: Contact[];

  msg = 'Loading contacts ...';
  userName = '';

  constructor(private contactService: ContactService, userService: UserService) {
    this.userName = userService.userName;
  }

  ngOnInit() {
    this.contactService.getContacts().then(contacts => {
      this.msg = '';
      this.contacts = contacts;
      this.contact = contacts[0];
    });
  }

  next() {
    let ix = 1 + this.contacts.indexOf(this.contact);
    if (ix >= this.contacts.length) { ix = 0; }
    this.contact = this.contacts[ix];
  }

  onSubmit() {
    // POST-DEMO TODO: do something like save it
    this.displayMessage('Saved ' + this.contact.name);
  }

  newContact() {
    this.displayMessage('New contact');
    this.contact = {id: 42, name: ''};
    this.contacts.push(this.contact);
  }

  /** Display a message briefly, then remove it. */
  displayMessage(msg: string) {
    this.msg = msg;
    setTimeout(() => this.msg = '', 1500);
  }
}
app/contact/contact.component.css
.ng-valid[required] {
  border-left: 5px solid #42A948; /* green */
}

.ng-invalid {
  border-left: 5px solid #a94442; /* red */
}

.alert {
  padding: 15px;
  margin: 8px 0;
  border: 1px solid transparent;
  border-radius: 4px;
}
.alert-danger {
  color: #a94442;
  background-color: #f2dede;
  border-color: #ebccd1;
}

.msg {
  color: blue;
  background-color: whitesmoke;
  border: 1px solid transparent;
  border-radius: 4px;
  margin-bottom: 20px;
}
app/contact/contact.service.ts
import { Injectable } from '@angular/core';

export class Contact {
  constructor(public id: number, public name: string) { }
}

const CONTACTS: Contact[] = [
  new Contact(21, 'Sam Spade'),
  new Contact(22, 'Nick Danger'),
  new Contact(23, 'Nancy Drew')
];

const FETCH_LATENCY = 500;

@Injectable()
export class ContactService {

  getContacts() {
    return new Promise<Contact[]>(resolve => {
      setTimeout(() => { resolve(CONTACTS); }, FETCH_LATENCY);
    });
  }

  getContact(id: number | string) {
    return this.getContacts()
      .then(heroes => heroes.find(hero => hero.id === +id));
  }
}
app/contact/awesome.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'awesome' })
/** Precede the input string with the word "Awesome " */
export class AwesomePipe implements PipeTransform {
  transform(phrase: string) {
    return phrase ? 'Awesome ' + phrase : '';
  }
}
app/contact/highlight.directive.ts
import { Directive, ElementRef, Renderer } from '@angular/core';

@Directive({ selector: '[highlight], input' })
/** Highlight the attached element or an InputElement in blue */
export class HighlightDirective {
  constructor(renderer: Renderer, el: ElementRef) {
    renderer.setElementStyle(el.nativeElement, 'backgroundColor', 'powderblue');
    console.log(
      `* Contact highlight called for ${el.nativeElement.tagName}`);
  }
}

Focus on the component template. Notice the two-way data binding [(ngModel)] in the middle of the template. ngModel is the selector for the NgModel directive.

Although NgModel is an Angular directive, the Angular Compiler won't recognize it because (a) AppModule doesn't declare it and (b) it wasn't imported via BrowserModule.

Less obviously, even if Angular somehow recognized ngModel, this ContactComponent would not behave like an Angular form because form features such as validation are not yet available.

Import the FormsModule

Add the FormsModule to the AppModule metadata's imports list.

imports: [ BrowserModule, FormsModule ],

Now [(ngModel)] binding will work and the user input will be validated by Angular Forms, once we declare our new component, pipe and directive.

Do not add NgModel — or the FORMS_DIRECTIVES — to the AppModule metadata's declarations!

These directives belong to the FormsModule. Components, directives and pipes belong to one module — and one module only.

Never re-declare classes that belong to another module.

Declare the contact component, directive and pipe

The application fails to compile until we declare the contact component, directive and pipe. Update the declarations in the AppModule accordingly:

app/app.module.ts (declarations)

  declarations: [
    AppComponent,
    HighlightDirective,
    TitleComponent,

    AwesomePipe,
    ContactComponent,
    ContactHighlightDirective
  ],

There are two directives with the same name, both called HighlightDirective.

We work around it by creating an alias for the second, contact version using the as JavaScript import keyword:

import {
  HighlightDirective as ContactHighlightDirective
} from './contact/highlight.directive';

This solves the immediate problem of referencing both directive types in the same file but leaves another problem unresoved as we discuss below.

Provide the ContactService

The ContactComponent displays contacts retrieved by the ContactService which Angular injects into its constructor.

We have to provide that service somewhere. The ContactComponent could provide it. But then it would be scoped to this component only. We want to share this service with other contact-related components that we will surely add later.

In this app we chose to add ContactService to the AppModule metadata's providers list:

app/app.module.ts (providers)

providers: [ ContactService, UserService ],

Now ContactService (like UserService) can be injected into any component in the application.

Application-scoped Providers

The ContactService provider is application-scoped because Angular registers a module's providers with the application's root injector.

Architecturally, the ContactService belongs to the Contact business domain. Classes in other domains don't need the ContactService and shouldn't inject it.

We might expect Angular to offer a module-scoping mechanism to enforce this design. It doesn't. Angular module instances, unlike components, do not have their own injectors so they can't have their own provider scopes.

This omission is intentional. Angular modules are designed primarily to extend an application, to enrich the entire app with the module's capabilities.

Service scoping is rarely a problem in practice. Non-contact components can't inject the ContactService by accident. To inject ContactService, you must first import its type. Only Contact components should import the ContactService type.

See the FAQ that pursues this issue and its mitigations in greater detail.

Run the app

Everything is now in place to run the application with its contact editor.

The app file structure looks like this:

app
app.component.ts
app.module.ts
highlight.directive.ts
main.ts
title.component.(html|ts)
user.service.ts
contact
awesome.pipe.ts
contact.component.(css|html|ts)
contact.service.ts
highlight.directive.ts

Try the example:

Resolve directive conflicts

We ran into trouble above when we declared the contact's HighlightDirective because we already had a HighlightDirective class at the application level.

That both directives have the same name smells of trouble.

A look at their selectors reveals that they both highlight the attached element with a different color.

app/highlight.directive.ts
import { Directive, ElementRef, Renderer } from '@angular/core';

@Directive({ selector: '[highlight]' })
/** Highlight the attached element in gold */
export class HighlightDirective {
  constructor(renderer: Renderer, el: ElementRef) {
    renderer.setElementStyle(el.nativeElement, 'backgroundColor', 'gold');
    console.log(
      `* AppRoot highlight called for ${el.nativeElement.tagName}`);
  }
}
app/contact/highlight.directive.ts
import { Directive, ElementRef, Renderer } from '@angular/core';

@Directive({ selector: '[highlight], input' })
/** Highlight the attached element or an InputElement in blue */
export class HighlightDirective {
  constructor(renderer: Renderer, el: ElementRef) {
    renderer.setElementStyle(el.nativeElement, 'backgroundColor', 'powderblue');
    console.log(
      `* Contact highlight called for ${el.nativeElement.tagName}`);
  }
}

Will Angular use only one of them? No. Both directives are declared in this module so both directives are active.

When the two directives compete to color the same element, the directive declared later wins because its DOM changes overwrite the first. In this case, the contact's HighlightDirective colors the application title text blue when it should stay gold.

The real problem is that there are two different classes trying to do the same thing.

It's OK to import the same directive class multiple times. Angular removes duplicate classes and only registers one of them.

But these are actually two different classes, defined in different files, that happen to have the same name.

They're not duplicates from Angular's perspective. Angular keeps both directives and they take turns modifying the same HTML element.

At least the app still compiles. If we define two different component classes with the same selector specifying the same element tag, the compiler reports an error. It can't insert two components in the same DOM location.

What a mess!

We can eliminate component and directive conflicts by creating feature modules that insulate the declarations in one module from the declarations in another.

Feature Modules

This application isn't big yet. But it's already suffering structural problems.

  • The root AppModule grows larger with each new application class and shows no signs of stopping.

  • We have conflicting directives. The HighlightDirective in contact is re-coloring the work done by the HighlightDirective declared in AppModule. And it's coloring the application title text when it should only color the ContactComponent.

  • The app lacks clear boundaries between contact functionality and other application features. That lack of clarity makes it harder to assign development responsibilities to different teams.

We mitigate these problems with feature modules.

Feature Module

A feature module is a class adorned by the @NgModule decorator and its metadata, just like a root module. Feature module metadata have the same properties as the metadata for a root module.

The root module and the feature module share the same execution context. They share the same dependency injector which means the services in one module are available to all.

There are two significant technical differences:

  1. We boot the root module to launch the app; we import a feature module to extend the app.

  2. A feature module can expose or hide its implementation from other modules.

Otherwise, a feature module is distinguished primarily by its intent.

A feature module delivers a cohesive set of functionality focused on an application business domain, a user workflow, a facility (forms, http, routing), or a collection of related utilities.

While we can do everything within the root module, feature modules help us partition the app into areas of specific interest and purpose.

A feature module collaborates with the root module and with other modules through the services it provides and the components, directives, and pipes that it chooses to share.

In the next section, we carve the contact functionality out of the root module and into a dedicated feature module.

Make Contact a feature module

It's easy to refactor the contact material into a contact feature module.

  1. Create the ContactModule in the app/contact folder.
  2. Move the contact material from AppModule to ContactModule.
  3. Replace the imported BrowserModule with CommonModule.
  4. Import the ContactModule into the AppModule.

AppModule is the only existing class that changes. But we do add one new file.

Add the ContactModule

Here's the new ContactModule

app/contact/contact.module.ts

import { NgModule }           from '@angular/core';
import { CommonModule }       from '@angular/common';
import { FormsModule }        from '@angular/forms';

import { AwesomePipe }        from './awesome.pipe';

import
       { ContactComponent }   from './contact.component';
import { ContactService }     from './contact.service';
import { HighlightDirective } from './highlight.directive';

@NgModule({
  imports:      [ CommonModule, FormsModule ],
  declarations: [ ContactComponent, HighlightDirective, AwesomePipe ],
  exports:      [ ContactComponent ],
  providers:    [ ContactService ]
})
export class ContactModule { }

We copy from AppModule the contact-related import statements and the @NgModule properties that concern the contact and paste them in ContactModule.

We import the FormsModule because the contact component needs it.

Modules do not inherit access to the components, directives or pipes that are declared in other modules. What AppModule imports is irrelevant to ContactModule and vice versa. Before ContactComponent can bind with [(ngModel)], its ContactModule must import FormsModule.

We also replaced BrowserModule by CommonModule for reasons explained in an FAQ.

We declare the contact component, directive, and pipe in the module declarations.

We export the ContactComponent so other modules that import the ContactModule can include it in their component templates.

All other declared contact classes are private by default. The AwesomePipe and HighlightDirective are hidden from the rest of the application. The HighlightDirective can no longer color the AppComponent title text.

Refactor the AppModule

Return to the AppModule and remove everything specific to the contact feature set.

Delete the contact import statements. Delete the contact declarations and contact providers. Remove the FormsModule from the imports list (AppComponent doesn't need it). Leave only the classes required at the application root level.

Then import the ContactModule so the app can continue to display the exported ContactComponent.

Here's the refactored version of the AppModule side-by-side with the previous version.

app/app.module.ts (v2)
import { NgModule }           from '@angular/core';
import { BrowserModule }      from '@angular/platform-browser';

/* App Root */
import
       { AppComponent }       from './app.component';
import { HighlightDirective } from './highlight.directive';
import { TitleComponent }     from './title.component';
import { UserService }        from './user.service';

/* Contact Imports */
import
       { ContactModule }      from './contact/contact.module';

@NgModule({
  imports:      [ BrowserModule, ContactModule ],
  declarations: [ AppComponent, HighlightDirective, TitleComponent ],
  providers:    [ UserService ],
  bootstrap:    [ AppComponent ],
})
export class AppModule { }
app/app.module.ts (v1)
import { NgModule }           from '@angular/core';
import { BrowserModule }      from '@angular/platform-browser';

/* App Root */
import
       { AppComponent }       from './app.component';
import { HighlightDirective } from './highlight.directive';
import { TitleComponent }     from './title.component';
import { UserService }        from './user.service';

/* Contact Imports */
import
       { ContactComponent }   from './contact/contact.component';
import { ContactService }     from './contact/contact.service';
import { AwesomePipe }        from './contact/awesome.pipe';

import {
  HighlightDirective as ContactHighlightDirective
} from './contact/highlight.directive';

import { FormsModule }        from '@angular/forms';

@NgModule({
  imports: [ BrowserModule,  FormsModule ],
  declarations: [
    AppComponent, HighlightDirective, TitleComponent,
    AwesomePipe, ContactComponent, ContactHighlightDirective
  ],
  providers: [ ContactService, UserService ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Improvements

There's a lot to like in the revised AppModule

  • It does not change as the Contact domain grows.
  • It only changes when we add new modules.
  • It's simpler:
    • Fewer import statements
    • No FormsModule import
    • No contact-specific declarations
    • No ContactService provider
    • No HighlightDirective conflict

Try this ContactModule version of the sample.

Try the live example.

Lazy loading modules with the Router

The Heroic Staffing Agency sample app has evolved. It has two more modules, one for managing the heroes-on-staff and another for matching crises to the heroes. Both modules are in the early stages of development. Their specifics aren't important to the story and we won't discuss every line of code.

Examine and download the complete source for this version from the

live example.

Some facets of the current application merit discussion.

  • The app has three feature modules: Contact, Hero, and Crisis.
  • The Angular router helps users navigate among these modules.
  • The ContactComponent is the default destination when the app starts.
  • The ContactModule continues to be "eagerly" loaded when the application starts.
  • HeroModule and the CrisisModule are lazy loaded.

Let's start at the top with the new AppComponent template: a title, three links, and a <router-outlet>.

app/app.component.ts (v3 - Template)

template: `
  <app-title [subtitle]="subtitle"></app-title>
  <nav>
    <a routerLink="contact" routerLinkActive="active">Contact</a>
    <a routerLink="crisis"  routerLinkActive="active">Crisis Center</a>
    <a routerLink="heroes"  routerLinkActive="active">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`

The <app-contact> element is gone; we're routing to the Contact page now.

The AppModule has changed modestly:

app/app.module.ts (v3)

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

/* App Root */
import { AppComponent }       from './app.component.3';
import { HighlightDirective } from './highlight.directive';
import { TitleComponent }     from './title.component';
import { UserService }        from './user.service';

/* Feature Modules */
import { ContactModule }      from './contact/contact.module.3';
import { routing }            from './app.routing.3';

@NgModule({
  imports:      [
    BrowserModule,
    ContactModule,
    routing
  ],
  providers:    [ UserService ],
  declarations: [ AppComponent, HighlightDirective, TitleComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

Some file names bear a .3 extension indicating a difference with prior or future versions. We'll explain differences that matter in due course.

The module still imports ContactModule so that its routes and components are mounted when the app starts.

The module does not import HeroModule or CrisisModule. They'll be fetched and mounted asynchronously when the user navigates to one of their routes.

The significant change from version 2 is the addition of a routing object to the imports. The routing object, which provides a configured Router service, is defined in the app.routing.ts file.

App routing

app/app.routing.ts

import { ModuleWithProviders }  from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

export const routes: Routes = [
  { path: '', redirectTo: 'contact', pathMatch: 'full'},
  { path: 'crisis', loadChildren: 'app/crisis/crisis.module#CrisisModule' },
  { path: 'heroes', loadChildren: 'app/hero/hero.module#HeroModule' }
];

export const routing: ModuleWithProviders = RouterModule.forRoot(routes);

The router is the subject of its own page so we'll skip lightly over the details and concentrate on the intersection of Angular modules and routing.

This file defines three routes.

The first redirects the empty URL (e.g., http://host.com/) to another route whose path is contact (e.g., http://host.com/contact).

The contact route isn't defined here. It's defined in the Contact feature's own routing file, contact.routing.ts. It's standard practice for feature modules with routing components to define their own routes. We'll get to that file in a moment.

The remaining two routes use lazy loading syntax to tell the router where to find the modules:

{ path: 'crisis', loadChildren: 'app/crisis/crisis.module#CrisisModule' },
{ path: 'heroes', loadChildren: 'app/hero/hero.module#HeroModule' }

A lazy loaded module location is a string, not a type. In this app, the string identifies both the module file and the module class, the latter separated from the former by a #.

RouterModule.forRoot

The last line calls the forRoot static class method of the RouterModule, passing in the configuration.

export const routing: ModuleWithProviders = RouterModule.forRoot(routes);

The returned routing object is a ModuleWithProviders containing both the RouterModule directives and the Dependency Injection providers that produce a configured Router.

This routing object is intended for the app root module only.

Never call RouterModule.forRoot in a feature module.

Back in the root AppModule, we add this routing object to its imports list, and the app is ready to navigate.

app/app.module.ts (imports)

imports:      [
  BrowserModule,
  ContactModule,
  routing
],

Routing to a feature module

The app/contact folder holds a new file, contact.routing.ts. It defines the contact route we mentioned a bit earlier and also creates a routing object like so:

app/contact/contact.routing.ts (routing)

export const routing: ModuleWithProviders = RouterModule.forChild([
  { path: 'contact', component: ContactComponent}
]);

This time we pass the route list to the forChild method of the RouterModule. It produces a different kind of object intended for feature modules.

Always call RouterModule.forChild in a feature module.

forRoot and forChild are conventional names for methods that deliver different import values to root and feature modules. Angular doesn't recognize them but Angular developers do.

Follow this convention if you write a similar module that has both shared declarables and services.

ContactModule has changed in two small but important details

app/contact/contact.module.3.ts
@NgModule({
  imports:      [ CommonModule, FormsModule, routing ],
  declarations: [ ContactComponent, HighlightDirective, AwesomePipe ],
  providers:    [ ContactService ]
})
export class ContactModule { }
app/contact/contact.module.2.ts
@NgModule({
  imports:      [ CommonModule, FormsModule ],
  declarations: [ ContactComponent, HighlightDirective, AwesomePipe ],
  exports:      [ ContactComponent ],
  providers:    [ ContactService ]
})
export class ContactModule { }
  1. It imports the routing object from contact.routing.ts
  2. It no longer exports ContactComponent

Now that we navigate to ContactComponent with the router there's no reason to make it public. Nor does it need a selector. No template will ever again reference this ContactComponent. It's gone from the AppComponent template.

Lazy loaded routing to a module

The lazy loaded HeroModule and CrisisModule follow the same principles as any feature module. They don't look different from the eagerly loaded ContactModule.

The HeroModule is a bit more complex than the CrisisModule which makes it a more interesting and useful example. Here's its file structure:

hero
hero-detail.component.ts
hero-list.component.ts
hero.component.ts
hero.module.ts
hero.routing.ts
hero.service.ts
highlight.directive.ts

This is the child routing scenario familiar to readers of the Router page. The HeroComponent is the feature's top component and routing host. Its template has a <router-outlet> that displays either a list of heroes (HeroList) or an editor of a selected hero (HeroDetail). Both components delegate to the HeroService to fetch and save data.

There's yet another HighlightDirective that colors elements in yet a different shade. We should do something about the repetition and inconsistencies. We endure for now.

The HeroModule is a feature module like any other.

app/hero/hero.module.ts (class)

@NgModule({
  imports: [ CommonModule, FormsModule, routing ],
  declarations: [
    HeroComponent, HeroDetailComponent, HeroListComponent,
    HighlightDirective
  ]
})
export class HeroModule { }

It imports the FormsModule because the HeroDetailComponent template binds with [(ngModel)]. It imports a routing object from hero.routing.ts just as ContactModule and CrisisModule do.

The CrisisModule is much the same. There's nothing more to say that's new.

Try the live example.

Shared modules

The app is shaping up. One thing we don't like is carrying three different versions of the HighlightDirective. And there's a bunch of other stuff cluttering the app folder level that could be tucked away.

Let's add a SharedModule to hold the common components, directives, and pipes and share them with the modules that need them.

  • create an app/shared folder
  • move the AwesomePipe and HighlightDirective from app/contact to app/shared.
  • delete the HighlightDirective classes from app/ and app/hero
  • create a SharedModule class to own the shared material
  • update other feature modules to import SharedModule

Most of this is familiar blocking and tackling. Here is the SharedModule

app/app/shared/shared.module.ts

import { NgModule }            from '@angular/core';
import { CommonModule }        from '@angular/common';
import { FormsModule }         from '@angular/forms';

import { AwesomePipe }         from './awesome.pipe';
import { HighlightDirective }  from './highlight.directive';

@NgModule({
  imports:      [ CommonModule ],
  declarations: [ AwesomePipe, HighlightDirective ],
  exports:      [ AwesomePipe, HighlightDirective,
                  CommonModule, FormsModule ]
})
export class SharedModule { }

Some highlights

  • It imports the CommonModule because its component needs common directives.
  • It declares and exports the utility pipe, directive, and component classes as expected.
  • It re-exports the CommonModule and FormsModule

Re-exporting other modules

While reviewing our application, we noticed that many components requiring SharedModule directives also use NgIf and NgFor from CommonModule and bind to component properties with [(ngModel)], a directive in the FormsModule. Modules that declare these components would have to import CommonModule, FormsModule and SharedModule.

We can reduce the repetition by having SharedModule re-export CommonModule and FormsModule so that importers of SharedModule get CommonModule and FormsModule for free.

As it happens, the components declared by SharedModule itself don't bind with [(ngModel)]. Technically, there is no need for SharedModule to import FormsModule.

SharedModule can still export FormsModule without listing it among its imports.

Why TitleComponent isn't shared

SharedModule exists to make commonly used components, directives and pipes available for use in the templates of components in many other modules.

The TitleComponent is used only once by the AppComponent. There's no point in sharing it.

Why UserService isn't shared

While many components share the same service instances, they rely on Angular dependency injection to do this kind of sharing, not the module system.

Several components of our sample inject the UserService. There should be only one instance of the UserService in the entire application and only one provider of it.

UserService is an application-wide singleton. We don't want each module to have its own separate instance. Yet there is a real danger of that happening if the SharedModule provides the UserService.

Do not specify app-wide singleton providers in a shared module. A lazy loaded module that imports that shared module will make its own copy of the service.

The Core module

At the moment, our root folder is cluttered with the UserService and the TitleComponent that only appears in the root AppComponent. We did not include them in the SharedModule for reasons just explained.

Instead, we'll gather them in a single CoreModule that we import once when the app starts and never import anywhere else.

Steps:

  • create an app/core folder
  • move the UserService and TitleComponent from app/ to app/core
  • create a CoreModule class to own the core material
  • update the AppRoot module to import CoreModule

Again, most of this is familiar blocking and tackling. The interesting part is the CoreModule

app/app/core/core.module.ts

import {
  ModuleWithProviders, NgModule,
  Optional, SkipSelf }       from '@angular/core';

import { CommonModule }      from '@angular/common';

import { TitleComponent }    from './title.component';
import { UserService }       from './user.service';
@NgModule({
  imports:      [ CommonModule ],
  declarations: [ TitleComponent ],
  exports:      [ TitleComponent ],
  providers:    [ UserService ]
})
export class CoreModule {
}

We're importing some extra symbols from the Angular core library that we're not using yet. They'll become relevant later in this page.

The @NgModule metadata should be familiar. We declare the TitleComponent because this module owns it and we export it because AppComponent (which is in AppModule) displays the title in its template. TitleComponent needs the Angular NgIf directive that we import from CommonModule.

CoreModule provides the UserService. Angular registers that provider with the app root injector, making a singleton instance of the UserService available to any component that needs it, whether that component is eagerly or lazily loaded.

Why bother?

This scenario is clearly contrived. The app is too small to worry about a single service file and a tiny, one-time component.

A TitleComponent sitting in the root folder isn't bothering anyone. The root AppModule can register the UserService itself, as it does currently, even if we decide to relocate the UserService file to the app/core folder.

Real world apps have more to worry about. They can have several single-use components (e.g., spinners, message toasts, and modal dialogs) that appear only in the AppComponent template. We don't import them elsewhere so they're not shared in that sense. Yet they're too big and messy to leave loose in the root folder.

Apps often have many singleton services like this sample's UserService. Each must be registered exactly once, in the app root injector, when the application starts.

While many Components inject such services in their constructors — and therefore require JavaScript import statements to import their symbols — no other component or module should define or re-create the services themselves. Their providers are not shared.

We recommend collecting such single-use classes and hiding their gory details inside a CoreModule. A simplified root AppModule imports CoreModule in its capacity as orchestrator of the application as a whole.

Cleanup

Having refactored to a CoreModule and a SharedModule, it's time to cleanup the other modules.

A trimmer AppModule

Here is the updated AppModule paired with version 3 for comparison:

app/app.module.ts (v4)
import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';

/* App Root */
import { AppComponent }   from './app.component';

/* Feature Modules */
import { ContactModule }  from './contact/contact.module';
import { CoreModule }     from './core/core.module';
import { routing }        from './app.routing';

@NgModule({
  imports: [
    BrowserModule,
    ContactModule,
    CoreModule,
    routing
  ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }
app/app.module.ts (v3)
import { NgModule }           from '@angular/core';
import { BrowserModule }      from '@angular/platform-browser';

/* App Root */
import { AppComponent }       from './app.component.3';
import { HighlightDirective } from './highlight.directive';
import { TitleComponent }     from './title.component';
import { UserService }        from './user.service';

/* Feature Modules */
import { ContactModule }      from './contact/contact.module.3';
import { routing }            from './app.routing.3';

@NgModule({
  imports:      [
    BrowserModule,
    ContactModule,
    routing
  ],
  providers:    [ UserService ],
  declarations: [ AppComponent, HighlightDirective, TitleComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

Notice that AppModule is ...

  • a little smaller because many app/root classes have moved to other modules.
  • stable because we'll add future components and providers to other modules, not this one.
  • delegating to imported modules rather than doing work.
  • focused on its main task, orchestrating the app as a whole.

A trimmer ContactModule

Here is the new ContactModule paired with the prior version:

app/contact/contact.module.ts (v4)
import { NgModule }           from '@angular/core';
import { SharedModule }       from '../shared/shared.module';

import { ContactComponent }   from './contact.component';
import { ContactService }     from './contact.service';
import { routing }            from './contact.routing';

@NgModule({
  imports:      [ SharedModule, routing ],
  declarations: [ ContactComponent ],
  providers:    [ ContactService ]
})
export class ContactModule { }
app/contact/contact.module.ts (v3)
import { NgModule }           from '@angular/core';
import { CommonModule }       from '@angular/common';
import { FormsModule }        from '@angular/forms';

import { AwesomePipe }        from './awesome.pipe';

import { ContactComponent }   from './contact.component.3';
import { ContactService }     from './contact.service';
import { HighlightDirective } from './highlight.directive';

import { routing }            from './contact.routing.3';

@NgModule({
  imports:      [ CommonModule, FormsModule, routing ],
  declarations: [ ContactComponent, HighlightDirective, AwesomePipe ],
  providers:    [ ContactService ]
})
export class ContactModule { }

Notice that

  • The AwesomePipe and HighlightDirective are gone.
  • The imports include SharedModule instead of CommonModule and FormsModule
  • This new version is leaner and cleaner.

Configure core services with CoreModule.forRoot

A module that adds providers to the application can offer a facility for configuring those providers as well.

By convention, the forRoot static method both provides and configures services at the same time. It takes a service configuration object and returns a ModuleWithProviders which is a simple object with two properties:

  • ngModule - the CoreModule class
  • providers - the configured providers

The root AppModule imports the CoreModule and adds the providers to the AppModule providers.

More precisely, Angular accumulates all imported providers before appending the items listed in @NgModule.providers. This sequence ensures that whatever we add explicitly to the AppModule providers takes precedence over the providers of imported modules.

Let's add a CoreModule.forRoot method that configures the core UserService.

We've extended the core UserService with an optional, injected UserServiceConfig. If a UserServiceConfig exists, the UserService sets the user name from that config.

app/core/user.service.ts (constructor)

constructor(@Optional() config: UserServiceConfig) {
  if (config) { this._userName = config.userName; }
}

Here's CoreModule.forRoot that takes a UserServiceConfig object:

app/core/core.module.ts (forRoot)

static forRoot(config: UserServiceConfig): ModuleWithProviders {
  return {
    ngModule: CoreModule,
    providers: [
      {provide: UserServiceConfig, useValue: config }
    ]
  };
}

Lastly, we call it within the imports list of the AppModule.

app//app.module.ts (imports)

  imports: [
    BrowserModule,
    ContactModule,
    CoreModule.forRoot({userName: 'Miss Marple'}),
    routing
  ],

The app displays "Miss Marple" as the user instead of the default "Sherlock Holmes".

Call forRoot only in the root application module, AppModule. Calling it in any other module, particularly in a lazy loaded module, is contrary to the intent and is likely to produce a runtime error.

Remember to import the result; don't add it to any other @NgModule list.

Prevent reimport of the CoreModule

Only the root AppModule should import the CoreModule. Bad things happen if a lazy loaded module imports it.

We could hope that no developer makes that mistake. Or we can guard against it and fail fast by adding the following CoreModule constructor.

constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
  if (parentModule) {
    throw new Error(
      'CoreModule is already loaded. Import it in the AppModule only');
  }
}

The constructor tells Angular to inject the CoreModule into itself. That seems dangerously circular.

The injection would be circular if Angular looked for CoreModule in the current injector. The @SkipSelf decorator means "look for CoreModule in an ancestor injector, above me in the injector hierarchy."

If the constructor executes as intended in the AppModule, there is no ancestor injector that could provide an instance of CoreModule. The injector should give up.

By default the injector throws an error when it can't find a requested provider. The @Optional decorator means not finding the service is OK. The injector returns null, the parentModule parameter is null, and the constructor concludes uneventfully.

It's a different story if we improperly import CoreModule into a lazy loaded module such as HeroModule (try it).

Angular creates a lazy loaded module with its own injector, a child of the root injector. @SkipSelf causes Angular to look for a CoreModule in the parent injector which this time is the root injector. Of course it finds the instance imported by the root AppModule. Now parentModule exists and the constructor throws the error.

Conclusion

You made it! You can examine and download the complete source for this final version from the live example.

Frequently Asked Questions

Now that you understand Angular Modules, you may be interested in the companion Angular Module FAQs cookbook with its ready answers to specific design and implementation questions.

doc_Angular
2016-10-06 09:46:12
Comments
Leave a Comment

Please login to continue.