Angular 8 - Services and Dependency Injection


Advertisements

As learned earlier, Services provides specific functionality in an Angular application. In a given Angular application, there may be one or more services can be used. Similarly, an Angular component may depend on one or more services.

Also, Angular services may depend on another services to work properly. Dependency resolution is one of the complex and time consuming activity in developing any application. To reduce the complexity, Angular provides Dependency Injection pattern as one of the core concept.

Let us learn, how to use Dependency Injection in Angular application in this chapter.

Create Angular service

An Angular service is plain Typescript class having one or more methods (functionality) along with @Injectable decorator. It enables the normal Typescript class to be used as service in Angular application.

import { Injectable } from '@angular/core'; @Injectable() 
export class DebugService { 
   constructor() { } 
}

Here, @Injectable decorator converts a plain Typescript class into Angular service.

Register Angular service

To use Dependency Injection, every service needs to be registered into the system. Angular provides multiple option to register a service. They are as follows −

  • ModuleInjector @ root level
  • ModuleInjector @ platform level
  • ElementInjector using providers meta data
  • ElementInjector using viewProviders meta data
  • NullInjector

ModuleInjector @ root

ModuleInjector enforces the service to used only inside a specific module. ProvidedInmeta data available in @Injectable has to be used to specify the module in which the service can be used.

The value should refer to the one of the registered Angular Module (decorated with @NgModule). root is a special option which refers the root module of the application. The sample code is as follows −

import { Injectable } from '@angular/core'; @Injectable({ 
   providedIn: 'root', 
})
export class DebugService { 
   constructor() { } 
}

ModuleInjector @ platform

Platform Injector is one level higher than ModuleInject and it is only in advanced and rare situation. Every Angular application starts by executing PreformBrowserDynamic().bootstrap method (see main.js), which is responsible for bootstrapping root module of Angular application.

PreformBrowserDynamic() method creates an injector configured by PlatformModule. We can configure platform level services using platformBrowser() method provided by PlatformModule.

NullInjector

NullInjector is one level higher than platform level ModuleInjector and is in the top level of the hierarchy. We could not able to register any service in the NullInjector. It resolves when the required service is not found anywhere in the hierarchy and simply throws an error.

ElementInjector using providers

ElementInjector enforces the service to be used only inside some particular components. providers and ViewProviders meta data available in @Component decorator is used to specify the list of services to be visible for the particular component. The sample code to use providers is as follows −

ExpenseEntryListComponent

// import statement 
import { DebugService } from '../debug.service'; 
// component decorator 
@Component({ 
   selector: 'app-expense-entry-list', 
   templateUrl: './expense-entry-list.component.html', 
   styleUrls: ['./expense-entry-list.component.css'], 
   providers: [DebugService] })

Here, DebugService will be available only inside the ExpenseEntryListComponent and its view. To make DebugService in other component, simply use providers decorator in necessary component.

ElementInjector using viewProviders

viewProviders is similar to provider except it does not allow the service to be used inside the component’s content created using ng-content directive.

ExpenseEntryListComponent

// import statement 
import { DebugService } from '../debug.service'; 
// component decorator 
@Component({ 
   selector: 'app-expense-entry-list', 
   templateUrl: './expense-entry-list.component.html', 
   styleUrls: ['./expense-entry-list.component.css'], viewProviders: [DebugService] 
})

Parent component can use a child component either through its view or content. Example of a parent component with child and content view is mentioned below −

Parent component view / template

<div> 
   child template in view 
   <child></child> 
</div> 
<ng-content></ng-content>

child component view / template

<div> 
   child template in view 
</div> 

Parent component usage in a template (another component)

<parent> 
   <!-- child template in content -->
   <child></child>
</parent> 

Here,

  • child component is used in two place. One inside the parent’s view. Another inside parent content.
  • Services will be available in child component, which is placed inside parent’s view.
  • Services will not be available in child component, which is placed inside parent’s content.

Resolve Angular service

Let us see how a component can resolve a service using the below flow diagram.

Resolve Angular

Here,

  • First, component tries to find the service registered using viewProviders meta data.
  • If not found, component tries to find the service registered using providers meta data.
  • If not found, Component tries to find the service registered using ModuleInjector
  • If not found, component tries to find the service registered using PlatformInjector
  • If not found, component tries to find the service registered using NullInjector, which always throws error.

The hierarchy of the Injector along with work flow of the resolving the service is as follows −

Angular service

Resolution Modifier

As we learn in the previous chapter, the resolution of the service starts from component and stops either when a service is found or NUllInjector is reached. This is the default resolution and it can be changed using Resolution Modifier. They are as follows −

Self()

Self() start and stops the search for the service in its current ElementInjector itself.

import { Self } from '@angular/core'; 
constructor(@Self() public debugService: DebugService) {}

SkipSelf()

SkipSelf() is just opposite to Self(). It skips the current ElementInjector and starts the search for service from its parent ElementInjector.

import { SkipSelf } from '@angular/core'; 
constructor(@SkipSelf() public debugService: DebugService) {}

Host()

Host() stop the search for the service in its host ElementInjector. Even if service available up in the higher level, it stops at host.

import { Host } from '@angular/core'; 
constructor(@Host() public debugService: DebugService) {}

Optional()

Optional() does not throws the error when the search for the service fails.

import { Optional } from '@angular/core'; 
constructor(@Optional() private debugService?: DebugService) { 
   if (this.debugService) { 
      this.debugService.info("Debugger initialized"); 
   } 
}

Dependency Injector Providers

Dependency Injector providers serves two purpose. First, it helps in setting a token for the service to be registered. The token will be used to refer and call the service. Second, it helps in creating the service from the given configuration.

As learned earlier, the simplest provider is as follows −

providers: [ DebugService ]

Here, DebugService is both token as well as the class, with which the service object has to be created. The actual form of the provider is as follows −

providers: [ { provides: DebugService, useClass: DebugService }]

Here, provides is the token and useClass is the class reference to create the service object.

Angular provides some more providers and they are as follows −

Aliased class providers

The purpose of the providers is to reuse the existing service.

providers: [ DebugService, 
   { provides: AnotherDebugService, userClass: DebugService }]

Here, only one instance of DebugService service will be created.

Value providers

The purpose of the Value providers is to supply the value itself instead of asking the DI to create an instance of the service object. It may use existing object as well. The only restriction is that the object should be in the shape of referenced service.

export class MyCustomService { 
   name = "My Custom Service" 
} 
[{ provide: MyService, useValue: { name: 'instance of MyCustomService' }]

Here, DI provider just return the instance set in useValue option instead of creating a new service object.

Non-class dependency providers

It enables string, function or object to be used in Angular DI.

Let us see a simple example.

// Create the injectable token 
import { InjectionToken } from '@angular/core'; 
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config'); 
// Create value 
export const MY_CONFIG: AppConfig = { 
   title: 'Dependency Injection' 
}; 
// congfigure providers 
providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }] 
// inject the service 
constructor(@Inject(APP_CONFIG) config: AppConfig) {

Factory providers

Factory Providers enables complex service creation. It delegates the creation of the object to an external function. Factory providers has option to set the dependency for factory object as well.

{ provide: MyService, useFactory: myServiceFactory, deps: [DebugService] };

Here, myServiceFactory returns the instance of MyService.

Angular Service usage

Now, we know how to create and register Angular Service. Let us see how to use the Angular Service inside a component. Using an Angular service is as simple as setting the type of parameters of the constructor as the token of the service providers.

export class ExpenseEntryListComponent implements OnInit {
   title = 'Expense List'; 
   constructor(private debugService : DebugService) {} 
   ngOnInit() { 
      this.debugService.info("Angular Application starts"); 
   } 
}

Here,

  • ExpenseEntryListComponent constructor set a parameter of type DebugService.

  • Angular Dependency Injector (DI) will try to find any service registered in the application with type DebugService. If found, it will set an instance of DebugService to ExpenseEntryListComponent component. If not found, it will throw an error.

Add a debug service

Let us add a simple Debug service, which will help us to print the debugging information during application development.

Open command prompt and go to project root folder.

cd /go/to/expense-manager

Start the application.

ng serve

Run the below command to generate an Angular service, DebugService.

ng g service debug

This will create two Typescript files (debug service & its test) as specified below −

CREATE src/app/debug.service.spec.ts (328 bytes) 
CREATE src/app/debug.service.ts (134 bytes)

Let us analyse the content of the DebugService service.

import { Injectable } from '@angular/core'; @Injectable({ 
   providedIn: 'root' 
}) 
export class DebugService { 
   constructor() { } 
}

Here,

  • @Injectable decorator is attached to DebugService class, which enables the DebugService to be used in Angular component of the application.

  • providerIn option and its value, root enables the DebugService to be used in all component of the application.

Let us add a method, Info, which will print the message into the browser console.

info(message : String) : void { 
   console.log(message); 
}

Let us initialise the service in the ExpenseEntryListComponent and use it to print message.

import { Component, OnInit } from '@angular/core'; import { ExpenseEntry } from '../expense-entry'; import { DebugService } from '../debug.service'; @Component({ 
   selector: 'app-expense-entry-list', 
   templateUrl: './expense-entry-list.component.html', styleUrls: ['./expense-entry-list.component.css'] 
}) 
export class ExpenseEntryListComponent implements OnInit { 
   title: string; 
   expenseEntries: ExpenseEntry[]; 
   constructor(private debugService: DebugService) { } 
   ngOnInit() { 
      this.debugService.info("Expense Entry List 
      component initialized"); 
      this.title = "Expense Entry List"; 
      this.expenseEntries = this.getExpenseEntries(); 
   } 
   // other coding 
}

Here,

  • DebugService is initialised using constructor parameters. Setting an argument (debugService) of type DebugService will trigger the dependency injection to create a new DebugService object and set it into the ExpenseEntryListComponent component.

  • Calling the info method of DebugService in the ngOnInit method prints the message in the browser console.

The result can be viewed using developer tools and it looks similar as shown below −

Debug service

Let us extend the application to understand the scope of the service.

Let us a create a DebugComponent by using below mentioned command.

ng generate component debug
CREATE src/app/debug/debug.component.html (20 bytes) CREATE src/app/debug/debug.component.spec.ts (621 bytes) 
CREATE src/app/debug/debug.component.ts (265 bytes) CREATE src/app/debug/debug.component.css (0 bytes) UPDATE src/app/app.module.ts (392 bytes)

Let us remove the DebugService in the root module.

// src/app/debug.service.ts
import { Injectable } from '@angular/core'; @Injectable() 
export class DebugService { 
   constructor() { 
   }
   info(message : String) : void {     
      console.log(message); 
   } 
}

Register the DebugService under ExpenseEntryListComponent component.

// src/app/expense-entry-list/expense-entry-list.component.ts @Component({ 
   selector: 'app-expense-entry-list', 
   templateUrl: './expense-entry-list.component.html', 
   styleUrls: ['./expense-entry-list.component.css'] 
   providers: [DebugService] 
})

Here, we have used providers meta data (ElementInjector) to register the service.

Open DebugComponent (src/app/debug/debug.component.ts) and import DebugService and set an instance in the constructor of the component.

import { Component, OnInit } from '@angular/core'; import { DebugService } from '../debug.service'; 
@Component({ 
   selector: 'app-debug', 
   templateUrl: './debug.component.html', 
   styleUrls: ['./debug.component.css'] 
}) 
export class DebugComponent implements OnInit { 
   constructor(private debugService: DebugService) { } 
   ngOnInit() { 
      this.debugService.info("Debug component gets service from Parent"); 
   } 
}

Here, we have not registered DebugService. So, DebugService will not be available if used as parent component. When used inside a parent component, the service may available from parent, if the parent has access to the service.

Open ExpenseEntryListComponent template (src/app/expense-entry-list/expense-entry-list.component.html) and include a content section as shown below:

// existing content 
<app-debug></app-debug>
<ng-content></ng-content>

Here, we have included a content section and DebugComponent section.

Let us include the debug component as a content inside the ExpenseEntryListComponent component in the AppComponent template. Open AppComponent template and change app-expense-entry-list as below −

// navigation code
<app-expense-entry-list>
<app-debug></app-debug>
</app-expense-entry-list>

Here, we have included the DebugComponent as content.

Let us check the application and it will show DebugService template at the end of the page as shown below −

Debug

Also, we could able to see two debug information from debug component in the console. This indicate that the debug component gets the service from its parent component.

Let us change how the service is injected in the ExpenseEntryListComponent and how it affects the scope of the service. Change providers injector to viewProviders injection. viewProviders does not inject the service into the content child and so, it should fail.

viewProviders: [DebugService]

Check the application and you will see that the one of the debug component (used as content child) throws error as shown below −

Application

Let us remove the debug component in the templates and restore the application.

Open ExpenseEntryListComponent template (src/app/expense-entry-list/expense-entry-list.component.html) and remove below content

 
<app-debug></app-debug>
<ng-content></ng-content>

Open AppComponent template and change app-expense-entry-list as below −

// navigation code
<app-expense-entry-list>
</app-expense-entry-list>

Change the viewProviders setting to providers in ExpenseEntryListComponent.

providers: [DebugService]

Rerun the application and check the result.

Advertisements