
Introduction to NgModules – Components, Dependency Injection, and Testing
Reading Time: 6 minutesA big change in Angular is the introduction of NgModule
s - it affects how every Angular app is bootstrapped and organized. I'll explain to you how NgModule
s works below, but first:
NgModules
NgModule
is a way to organize your dependencies for 1. the compiler and 2. dependency injection.
I'm going to quickly explain why we need NgModule
s and how to work with them. In five minutes, you'll understand NgModule
s enough to put it in your own apps.
The context here is to think about the two roles of the compiler and dependency injection in Angular. Briefly, Angular needs to know what components define valid tags and where dependencies are coming from.
NgModule
vs. JavaScript Modules
You might be asking, why do we need a new module system at all? Why can't we just use ES6/TypeScript modules?
The reason is, whereas using import
will load code modules into JavaScript, the NgModule
system is a way of organizing dependencies within the Angular framework. Specifically around what tags are compiled and what dependencies should be injected.
The Compiler and Components
For the compiler, when you have an Angular template that has custom tags you have to tell the compiler what tags are valid (and what functionality should be attached to them).
E.g. if we have this component:
@Component({
selector: 'hello-world',
template: `<div>Hello world</div>`
})
class HelloWorld {
}
We want the compiler to know that the following HTML should use our hello-world
component (and that hello-world
isn't some random invalid tag):
<div>
<hello-world></hello-world>
</div>
In Angular 1, the hello-world
selector would have been registered globally which is convenient until your app grows and you start having naming conflicts. For instance, it's not hard to imagine two open-source projects that might use the same selector.
If you've been using Angular 2 since the earlier versions, you may remember that previous versions required that you specify a directives
option in your @Component
annotation. This was good in that it was less "magic" and removed the surface area for conflicts. The problem was it's a bit onerous to specify all directives necessary for all components.
Instead, using NgModule
s we can tell Angular what components are dependencies at a "module" level. More on this in a second.
Dependency Injection and Providers
Recall that Dependency Injection (DI) is an organized way to make dependencies available across our app. It's an improvement over simply import
ing code because we have a standardized way to share singletons, create factories, and override dependencies at testing time.
In earlier versions of Angular 2 we had to specify all things-that-would-be-injected (with providers) as an argument to the bootstrap
function.
Note on terminology: a provider provides (creates, instantiates, etc.) the injectable (the thing you want). In Angular when you want to access an injectable you inject a dependency into a function and Angular's dependency injection framework will locate it and provide it to you.
Now with NgModule
each provider is specified as part of a module.
NgModules 101
So now that we understand why we need NgModule
s how do we actually use it? Here's the simplest case:
// app.ts
@NgModule({
imports: [ BrowserModule ],
declarations: [ HelloWorld ],
bootstrap: [ HelloWorld ]
})
class HelloWorldAppModule {}
platformBrowserDynamic().bootstrapModule(HelloWorldAppModule);
In this case we're defining a class HelloWorldAppModule
- this is going to be the entry point of our application. Starting with RC5, instead of bootstrapping our app with a component, we bootstrap a module with bootstrapModule
, as you see here.
NgModule
s can import other modules as dependencies. We're going to be running this app in our browser and so we import BrowserModule
.
We want to use the HelloWorld
component in this app. Here's a key thing to keep in mind: Every component must be declared in some NgModule
. Here we put HelloWorld
into the declarations
of this NgModule
.
We say the HelloWorld
component belongs to the HelloWorldAppModule
- every component can belong to only one NgModule
.
You'll often group multiple components together into one NgModule
, much like you might use a namespace in a language like Java.
If you want to bootstrap this module (that is, use this module as the entry point for an application), then you provide a bootstrap
key which specifies the component that will be used as the entry-point component for this module.
So in this case we're going to bootstrap
the HelloWorld
component as the root component. However, the bootstrap
key is optional if you're creating a module that doesn't need to be the entry-point of an application.
Component Visibility
In order to use any component, the current NgModule
has to know about it. For instance, say we wanted to use a user-greeting
component in our hello-world
component like this:
<!-- hello-world template -->
<div>
<user-greeting></user-greeting>
world
</div>
For any component to use another component it must be accessible via the NgModule
system. There are two ways to make this happen:
- Either the
user-greeting
component is part of the sameNgModule
(e.g.HelloWorldAppModule
) or - The
HelloWorldAppModule
imports
the module that theUserGreeting
component is in.
Let's say we want to go the second route. Here's the implementation of our UserGreeting
component along with the UserGreetingModule
:
@Component({
selector: 'user-greeting',
template: `<span>hello</span>`
})
class UserGreeting {
}
@NgModule({
declarations: [ UserGreeting ],
exports: [ UserGreeting ]
})
export class UserGreetingModule {}
Notice here that we added a new key: exports
. Think of exports
as the list of public components for this NgModule
. The implication here is that you can easily have private components by simply not listing them in exports
.
If you forget to put your component in both declarations
and exports
(and then try to use it in another module via imports
) it won't work. In order to use a component in another module via imports
you must put your component in both places.
Now we can use this in our HelloWorld
component by importing it into the HelloWorldAppModule
like so:
// updated HelloWorldAppModule
@NgModule({
declarations: [ HelloWorld ],
imports: [ BrowserModule, UserGreetingModule ], // <-- added
bootstrap: [ HelloWorld ],
})
class HelloWorldAppModule {}
Specifying Providers
Specifying providers of injectable things is done by adding them to the providers
key of a NgModule
.
For instance, say we have this simple service:
export class ApiService {
get(): void {
console.log('Getting resource...');
}
}
and we want to be able to inject it on a component like this:
class ApiDataComponent {
constructor(private apiService: ApiService) {
}
getData(): void {
this.apiService.get();
}
}
To do this with NgModule
is easy: we pass ApiService
to the providers
key of the module:
@NgModule({
declarations: [ ApiDataComponent ],
providers: [ ApiService ] // <-- here
})
class ApiAppModule {}
Passing the constant ApiService
here is the shorthand version of using provide
like this:
@NgModule({
declarations: [ ApiDataComponent ],
providers: [
provide(ApiService, { useClass: ApiService })
]
})
class ApiAppModule {}
We're telling Angular that when the ApiService
is to be injected, create and maintain a singleton instance of that class and pass it in the injection. There are lots of different ways to inject things, and we go over all of them in the book.
In order to use those providers from another module, you guessed it, you have to import
that module.
Because the ApiDataComponent
and ApiService
are in the same NgModule
the ApiDataComponent
is able to inject the ApiService
. If they were in different modules, then you would need to import the module containing ApiService
into the ApiAppModule
.
Testing
New in RC5 we have a TestBed
library from @angular/core/testing
. When you write your tests you have to configure an NgModule
there. Thankfully, TestBed
provides a configureTestingModule
option which takes an NgModule
configuration and applies to the components created in your test.
For instance, here's how we might configure our NgModule
to use the above samples:
describe('DemoFormSku Component', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HelloWorldAppModule,
UserGreetingModule,
ApiAppModule
]
});
});
// tests here...
});
When using TestBed.configureTestingModule
. You can also override your providers or put custom testing components in declarations etc. Again, we have complete examples of these in the sample code for the book.
NgModule
s Tips and Gotchas
The community conventions around NgModule
s are still being worked out. That said, my advice is to organize your NgModule
s like you organize your packages already. Group ideas together and then import the top-level modules into your app module at bootstrap time.
There are two "gotchas" I want you to keep in mind:
First, if you want to use a component in another module, make sure you add it to declarations
and exports
before you put it in imports
(this bit me several times).
Second, you can find the common directives like NgIf
, NgFor
, etc. by import { CommonModule } from '@angular/common'
. This happens by default when using BrowserModule
but if you're writing tests you may not import BrowserModule
and thus you'd get an error.
The advice generalizes to: make sure you understand the module dependency flow, especially when testing. Failure to import modules properly will result in failing tests (and the error messages aren't always clear).
Summary
NgModule
s is a great way to clearly specify which dependencies your components require. While it is slightly more ceremony up-front, the long-term effect is that we're able to organize dependencies explicitly and minimize conflicts - which is a requirement for larger apps.
Of course, there are a lot more details and configuration options for NgModule
s and so I'd encourage you to checkout the official documentation.
We also have over a dozen examples of using NgModule
in the sample code of ng-book 2. If you want become an expert at NgModule
and all of Angular, checkout ng-book 2