This chapter is all about the magic of dependency injection (DI) in Angular. Here, you'll learn some detailed information about the concept of DI in Angular. DI is the process that Angular uses to inject different dependencies into components, directives, and services. You'll work with several examples using services and providers to get some hands-on experience that you can utilize in your later Angular projects.
In this chapter, we're going to cover the following recipes:
For the recipes in this chapter, ensure you have Git and NodeJS installed on your machine. You also need to have the @angular/cli package installed, which you can do so using npm install -g @angular/cli from your Terminal. The code for this chapter can be found at https://github.com/PacktPublishing/Angular-Cookbook/tree/master/chapter03.
In this recipe, you'll learn how to create a basic DI token for a regular TypeScript class to be used as an Angular service. We have a service (UserService) in our application, which currently uses the Greeter class to create a user with a greet method. Since Angular is all about DI and services, we'll implement a way in which to use this regular TypeScript class, named Greeter, as an Angular service. We'll use InjectionToken to create a DI token and then the @Inject decorator to enable us to use the class in our service.
The project that we are going to work with resides in chapter03/start_here/ng-di-token, which is inside the cloned repository. Perform the following steps:
This should open the app in a new browser tab; you should see something similar to the following screenshot:
Now that we have the app running, we can move on to the steps for the recipe.
The app we have right now shows a greeting message to a random user that has been retrieved from our UserService. And UserService uses the Greeter class as it is. Instead of using it as a class, we'll use it as an Angular service using DI. We'll start by creating an InjectionToken for our Greeter class, which is a regular TypeScript class, and then we'll inject it into our services. Perform these steps to follow along:
import { InjectionToken } from '@angular/core';
import { User } from '../interfaces/user.interface';
export class Greeter implements User {
...
}
export const GREETER = new InjectionToken('Greeter', {
providedIn: 'root',
factory: () => Greeter
});
import { Inject, Injectable } from '@angular/core';
import { GREETER, Greeter } from '../classes/greeter.class';
@Injectable({
providedIn: 'root'
})
export class UserService {
...
}
Notice that we'll be using typeof Greeter instead of just Greeter because we need to use the constructor later on:
...
export class UserService {
...
constructor(@Inject(GREETER) public greeter: typeof Greeter) { }
...
}
...
export class UserService {
...
getUser() {
const user = this.users[Math.floor(Math.random() * this.users.length)]
return new this.greeter(user);
}
}
Now that we know the recipe, let's take a closer look at how it works.
Angular doesn't recognize regular TypeScript classes as injectables in services. However, we can create our own injection tokens and use the @Inject decorator to inject them whenever possible. Angular recognizes our token behind the scenes and finds its corresponding definition, which is usually in the form of a factory function. Notice that we're using providedIn: 'root' within the token definition. This means that there will be only one instance of the class in the entire application.
Optional dependencies in Angular are really powerful when you use or configure a dependency that may or may not exist or that has been provided within an Angular application. In this recipe, we'll learn how to use the @Optional decorator to configure optional dependencies in our components/services. We'll work with LoggerService and ensure our components do not break if it has not already been provided.
The project for this recipe resides in chapter03/start_here/ng-optional-dependencies. Perform the following steps:
This should open the app in a new browser tab. You should see something similar to the following screenshot:
Now that we have the app running, we can move on to the steps for the recipe.
We'll start with an app that has a LoggerService with providedIn: 'root' set to its injectable configuration. We'll see what happens when we don't provide this service anywhere. Then, we'll identify and fix the issues using the @Optional decorator. Follow these steps:
This will result in the logs being saved in localStorage via LoggerService. Open Chrome Dev Tools, navigate to Application, select Local Storage, and then click on localhost:4200. You will see the key log_log with log values, as follows:
import { Injectable } from '@angular/core';
import { Logger } from '../interfaces/logger';
@Injectable({
providedIn: 'root' ← Remove
})
export class LoggerService implements Logger {
...
}
This will result in Angular not being able to recognize it and throwing an error to VcLogsComponent:
import { Component, OnInit, Input, OnChanges, SimpleChanges, Optional } from '@angular/core';
...
export class VcLogsComponent implements OnInit {
...
constructor(@Optional() private loggerService: LoggerService) {
this.logger = this.loggerService;
}
...
}
Great! Now if you refresh the app and view the console, there shouldn't be any errors. However, if you change the version and hit the Submit button, you'll see that it throws the following error because the component is unable to retrieve LoggerService as a dependency:
...
export class VcLogsComponent implements OnInit {
...
constructor(@Optional() private loggerService: LoggerService) {
if (!this.loggerService) {
this.logger = console;
} else {
this.logger = this.loggerService;
}
}
...
Now, if you update the version and hit Submit, you should see the logs on the console, as follows:
Great! We've finished the recipe and everything looks great. Please refer to the next section to understand how it works.
The @Optional decorator is a special parameter from the @angular/core package, which allows you to mark a parameter for a dependency as optional. Behind the scenes, Angular will provide the value as null when the dependency doesn't exist or is not provided to the app.
In this recipe, you'll learn several tips on how to ensure your Angular service is being used as a singleton. This means that there will only be one instance of your service in the entire application. Here, we'll use a couple of techniques, including the providedIn: 'root' statement and making sure we only provide the service once in the entire app by using the @Optional() and @SkipSelf() decorators.
The project for this recipe resides in the chapter03/start_here/ng-singleton-service path. Perform the following steps:
This should open the app in a new browser tab. You should see something similar to the following screenshot:
Now that you have your app running, let's see move ahead and look at the steps of this recipe.
The problem with the app is that if you add or remove any notifications, the count on the bell icon in the header does not change. That's due to us having multiple instances of NotificationsService. Please refer to the following steps to ensure we only have a single instance of the service in the app:
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class NotificationsService {
...
}
Great! Now even if you refresh and try adding or removing notifications, you'll still see that the count in the header doesn't change. "But why is this, Ahsan?" Well, I'm glad you asked. That's because we're still providing the service in AppModule as well as in VersioningModule.
...
import { NotificationsButtonComponent } from './components/notifications-button/notifications-button.component';
import { NotificationsService } from './services/notifications.service'; ← Remove this
@NgModule({
declarations: [... ],
imports: [...],
providers: [
NotificationsService ← Remove this
],
bootstrap: [AppComponent]
})
export class AppModule { }
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { VersioningRoutingModule } from './versioning-routing.module';
import { VersioningComponent } from './versioning.component';
import { NotificationsManagerComponent } from './components/notifications-manager/notifications-manager.component';
import { NotificationsService } from '../services/notifications.service'; ← Remove this
@NgModule({
declarations: [VersioningComponent, NotificationsManagerComponent],
imports: [
CommonModule,
VersioningRoutingModule,
],
providers: [
NotificationsService ← Remove this
]
})
export class VersioningModule { }
Awesome! Now you should be able to see the count in the header change according to whether you add/remove notifications. However, what happens if someone still provides it in another lazily loaded module by mistake?
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { VersioningRoutingModule } from './versioning-routing.module';
import { VersioningComponent } from './versioning.component';
import { NotificationsManagerComponent } from './components/notifications-manager/notifications-manager.component';
import { NotificationsService } from '../services/notifications.service';
@NgModule({
declarations: [VersioningComponent, NotificationsManagerComponent],
imports: [
CommonModule,
VersioningRoutingModule,
],
providers: [
NotificationsService
]
})
export class VersioningModule { }
Boom! We don't have any errors on the console or during compile time. However, we do have the issue of the count not updating in the header. So, how do we alert the developers if they make such a mistake? Please refer to the next step.
import { Injectable, SkipSelf } from '@angular/core';
...
export class NotificationsService {
...
constructor(@SkipSelf() existingService: NotificationsService) {
if (existingService) {
throw Error ('The service has already been provided in the app. Avoid providing it again in child modules');
}
}
...
}
With the previous step now complete, you'll notice that we have a problem. That is we have failed to provide NotificationsService to our app at all. You should see this in the console:
The reason for this is that NotificationsService is now a dependency of NotificationsService itself. This can't work as it has not already been resolved by Angular. To fix this, we'll also use the @Optional() decorator in the next step.
import { Injectable, Optional, SkipSelf } from '@angular/core';
...
export class NotificationsService {
...
constructor(@Optional() @SkipSelf() existingService: NotificationsService) {
if (existingService) {
throw Error ('The service has already been provided in the app. Avoid providing it again in child modules');
}
}
...
}
We have now fixed the NotificationsService -> NotificationsService dependency issue. You should see the proper error for the NotificationsService being provided multiple times in the console, as follows:
...
import { NotificationsManagerComponent } from './components/notifications-manager/notifications-manager.component';
import { NotificationsService } from '../services/notifications.service'; ← Remove this
@NgModule({
declarations: [...],
imports: [...],
providers: [
NotificationsService ← Remove this
]
})
export class VersioningModule { }
Bam! We now have a singleton service using the providedIn strategy. In the next section, let's discuss how it works.
Whenever we try to inject a service somewhere, by default, it tries to find a service inside the associated module of where you're injecting the service. When we use providedIn: 'root' to declare a service, whenever the service is injected anywhere in the app, Angular knows that it simply has to find the service definition in the root module and not in the feature modules or anywhere else.
However, you have to make sure that the service is only provided once in the entire application. If you provide it in multiple modules, then even with providedIn: 'root', you'll have multiple instances of the service. To avoid providing a service in multiple modules or at multiple places in the app, we can use the @SkipSelf() decorator with the @Optional() decorator in the services' constructor to check whether the service has already been provided in the app.
In this recipe, you'll learn how to use ModuleWithProviders and the forRoot() statement to ensure your Angular service is being used as a singleton in the entire app. We'll start with an app that has multiple instances of NotificationsService, and we'll implement the necessary code to make sure we end up with a single instance of the app.
The project for this recipe resides in the chapter03/start_here/ng-singleton-service-forroot path. Perform the following steps:
This should open the app in a new browser tab. The app should appear as follows:
Now that we have the app running, in the next section, we can move on to the steps for the recipe.
In order to make sure we only have a singleton service in the app with the forRoot() method, you need to understand how ModuleWithProviders and the static forRoot() method are created and implemented. Perform the following steps:
ng g m services
import { ModuleWithProviders, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NotificationsService } from '../services/notifications.service';
@NgModule({
...
})
export class ServicesModule {
static forRoot(): ModuleWithProviders<ServicesModule> {
return {
ngModule: ServicesModule,
providers: [
NotificationsService
]
};
}
}
This is because it injects ServicesModule with the providers in AppModule, for instance, with the NotificationsService being provided as follows:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { NotificationsButtonComponent } from './components/notifications-button/notifications-button.component';
import { NotificationsService } from './services/notifications.service'; ← Remove this
import { ServicesModule } from './services/services.module';
@NgModule({
declarations: [
AppComponent,
NotificationsButtonComponent
],
imports: [
BrowserModule,
AppRoutingModule,
ServicesModule.forRoot()
],
providers: [
NotificationsService ← Remove this
],
bootstrap: [AppComponent]
})
export class AppModule { }
You'll notice that when adding/removing notifications, the count in the header still doesn't change. This is because we're still providing the NotificationsService in the versioning.module.ts file.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { VersioningRoutingModule } from './versioning-routing.module';
import { VersioningComponent } from './versioning.component';
import { NotificationsManagerComponent } from './components/notifications-manager/notifications-manager.component';
import { NotificationsService } from '../services/notifications.service'; ← Remove
@NgModule({
declarations: [VersioningComponent, NotificationsManagerComponent],
imports: [
CommonModule,
VersioningRoutingModule,
],
NotificationsService ← Remove
]
})
export class VersioningModule { }
All right, so far, you've done a great job. Now that we have finished the recipe, in the next section, let's discuss how it works.
ModuleWithProviders is a wrapper around NgModule, which is associated with the providers array that is used in NgModule. It allows you to declare NgModule with providers, so the module where it is being imported gets the providers as well. We created a forRoot() method in our ServicesModule class that returns ModuleWithProviders containing our provided NotificationsService. This allows us to provide NotificationsService only once in the entire app, which results in only one instance of the service in the app.
In this recipe, you'll learn how to provide two different services to the app using Aliased class providers. This is extremely helpful in complex applications where you need to narrow down the implementation of the base class for some components/modules. Additionally, aliasing is used in component/service unit tests to mock the dependent service's actual implementation so that we don't rely on it.
The project that we are going to work with resides in the chapter03/start_here/ng-aliased-class-providers path, which is inside the cloned repository. Perform the following steps:
This should open the app in a new browser tab.
Now that we have the app running, let's move to the next section to follow the steps for the recipe.
We have a shared component named BucketComponent, which is being used in both the admin and employee modules. BucketComponent uses BucketService behind the scenes to add/remove items from and to a bucket. For the employee, we'll restrict the the ability to remove an item by providing an aliased class provider and a different EmployeeBucketService. This is so that we can override the remove item functionality. Perform the following steps:
ng g service employee/services/employee-bucket
import { Injectable } from '@angular/core';
import { BucketService } from 'src/app/services/bucket.service';
@Injectable({
providedIn: 'root'
})
export class EmployeeBucketService extends BucketService {
constructor() {
super();
}
}
import { Injectable } from '@angular/core';
import { BucketService } from 'src/app/services/bucket.service';
@Injectable({
providedIn: 'root'
})
export class EmployeeBucketService extends BucketService {
constructor() {
super();
}
removeItem() {
alert('Employees can not delete items');
}
}
import { NgModule } from '@angular/core';
...
import { BucketService } from '../services/bucket.service';
import { EmployeeBucketService } from './services/employee-bucket.service';
@NgModule({
declarations: [...],
imports: [
...
],
providers: [{
provide: BucketService,
useClass: EmployeeBucketService
}]
})
export class EmployeeModule { }
If you now log in as an employee in the app and try to remove an item, you'll see an alert pop up, which says Employees cannot delete items.
When we inject a service into a component, Angular tries to find that component from the injected place by moving up the hierarchy of components and modules. Our BucketService is provided in 'root' using the providedIn: 'root' syntax. Therefore, it resides at the top of the hierarchy. However, since, in this recipe, we use an aliased class provider in EmployeeModule, when Angular searches for BucketService, it quickly finds it inside EmployeeModule and stops there before it even reaches 'root' to get the actual BucketService.
In this recipe, you'll learn how to use value providers in Angular to provide constants and config values to your app. We'll start with the same example from the previous recipe, that is, EmployeeModule and AdminModule using the shared component named BucketComponent. We will restrict the employee from deleting items from the bucket by using a value provider, so the employees won't even see the delete button.
The project that we are going to work with resides in the chapter03/start_here/ng-value-providers path, which is inside the cloned repository. Perform the following steps:
This should open the app in a new browser tab.
We have a shared component, named BucketComponent, that is being used in both the admin and employee modules. For the employee, we'll restrict the ability to remove an item by providing a value provider in EmployeeModule. This is so that we can hide the delete button based on its value.
import { InjectionToken } from '@angular/core';
export interface IAppConfig {
canDeleteItems: boolean;
}
export const APP_CONFIG = new InjectionToken<IAppConfig>('APP_CONFIG');
export const AppConfig: IAppConfig = {
canDeleteItems: true
}
Before we can actually use this AppConfig constant in our BucketComponent, we need to register it to the AppModule so that when we inject this in the BucketComponent, the value of the provider is resolved.
...
import { AppConfig, APP_CONFIG } from './constants/app-config';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
],
providers: [{
provide: APP_CONFIG,
useValue: AppConfig
}],
bootstrap: [AppComponent]
})
export class AppModule { }
Now the app knows about the AppConfig constants. The next step is to use this constant in BucketComponent.
import { Component, Inject, OnInit } from '@angular/core';
...
import { IAppConfig, APP_CONFIG } from '../../../constants/app-config';
...
export class BucketComponent implements OnInit {
...
constructor(private bucketService: BucketService, @Inject(APP_CONFIG) private config: IAppConfig) { }
...
}
Great! The constant has been injected. Now, if you refresh the app, you shouldn't get any errors. The next step is to use the canDeleteItems property from config in BucketComponent to show/hide the delete button.
...
export class BucketComponent implements OnInit {
$bucket: Observable<IFruit[]>;
selectedFruit: Fruit = '' as null;
fruits: string[] = Object.values(Fruit);
canDeleteItems: boolean;
constructor(private bucketService: BucketService, @Inject(APP_CONFIG) private config: IAppConfig) { }
ngOnInit(): void {
this.$bucket = this.bucketService.$bucket;
this.bucketService.loadItems();
this.canDeleteItems = this.config.canDeleteItems;
}
...
}
<div class="buckets" *ngIf="$bucket | async as bucket">
<h4>Bucket <i class="material-icons">shopping_cart </i></h4>
<div class="add-section">
...
</div>
<div class="fruits">
<ng-container *ngIf="bucket.length > 0; else bucketEmptyMessage">
<div class="fruits__item" *ngFor="let item of bucket;">
<div class="fruits__item__title">{{item.name}} </div>
<div *ngIf="canDeleteItems" class="fruits__ item__delete-icon" (click)="deleteFromBucket(item)">
<div class="material-icons">delete</div>
</div>
</div>
</ng-container>
</div>
</div>
<ng-template #bucketEmptyMessage>
...
</ng-template>
You can test whether everything works by setting the AppConfig constant's canDeleteItems property to false. Note that the delete button is now hidden for both the admin and employee. Once tested, set the value of canDeleteItems back to true again.
Now we have everything set up. Let's add a new constant so that we can hide the delete button for the employee only.
import { IAppConfig } from '../../constants/app-config';
export const EmployeeConfig: IAppConfig = {
canDeleteItems: false
}
...
import { EmployeeComponent } from './employee.component';
import { APP_CONFIG } from '../constants/app-config';
import { EmployeeConfig } from './constants/employee-config';
@NgModule({
declarations: [EmployeeComponent],
imports: [
...
],
providers: [{
provide: APP_CONFIG,
useValue: EmployeeConfig
}]
})
export class EmployeeModule { }
And we're done! The recipe is now complete. You can see that the delete button is visible to the admin but hidden for the employee. It's all thanks to the magic of value providers.
When we inject a token into a component, Angular tries to find the resolved value of the token from the injected place by moving up the hierarchy of components and modules. We provided EmployeeConfig as APP_CONFIG in EmployeeModule. When Angular tries to resolve its value for BucketComponent, it finds it early at EmployeeModule as EmployeeConfig. Therefore, Angular stops right there and doesn't reach AppComponent. Notice that the value for APP_CONFIG in AppComponent is the AppConfig constant.
44.200.39.110