Tuesday, June 9, 2020

Injector Hierarchy and Service Instances in Angular

In the Service in Angular With Examples post we saw an example of creating service where provider metadata was provided with in the @Injectable() decorator.

@Injectable({
 providedIn: 'root',
})

But that is not the only way to register a provider in Angular, there is a complete injector hierarchy and the number of service instances created depends on how you register the provider.


Providing services at different levels

For any service you are going to use in your Angular app you must register at least one provider. The provider can be part of the service's own metadata, making that service available everywhere, or you can register providers with specific modules or components. There are the following three ways for registering a provider for the service.

1. In the @Injectable() decorator for the service itself. By default, the Angular CLI command ng generate service registers a provider with the root injector for your service by including provider metadata in the @Injectable() decorator.

@Injectable({
 providedIn: 'root',
})

When you provide the service at the root level, Angular creates a single, shared instance of the Service and injects it into any class that asks for it. As per Angular docs Using the @Injectable() providedIn property is preferable to the @NgModule() providers array because that allows Angular to optimize an app by removing the service from the compiled app if it isn't used, this process of optimization is knows as tree-shaking.

2. You can also register a provider with a specific NgModule. In that case same servcie instance is available to all components in that NgModule.

For example configuring an app-wide provider in the @NgModule() of AppModule. You will have to use the providers property of the @NgModule() decorator.

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

3. You can also register a provider at the component level, in that case you get a new instance of the service with each new instance of that component.

To register a service provider at the component level you can use the providers property of the @Component() decorator.

@Component({
  selector: 'app-user',
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.css'],
  providers:  [ LoggerService ]
})
export class UserComponent implements OnInit {
 ..
 ..
}

Injector hierarchies in Angular

Based on these types of service providers there are two injector hierarchies in Angular.

  • ModuleInjector hierarchy— Configure a ModuleInjector in this hierarchy using an @NgModule() or @Injectable() annotation. A single service instance will be used at the root level or modulr level based upon the configuration used.
  • ElementInjector hierarchy— Angular creates ElementInjectors implicitly for each DOM element. When you provide services in a component, that service is available via the ElementInjector at that component instance. It will also be available at child components if you are not providing it again in the child component using the providers property.

Service provider example using @Injectable() decorator

In the post Service in Angular With Examples there is an example of creating a LoggerService which is decorated with @Injectable() decorator to provide the metadata that allows Angular to inject it into a component as a dependency.

Service provider example using @NgModule

Let’s see the same example by registering service in providers array of @NgModule.

LoggerService class

Now @Injectable decorator is not used in LoggerService class.

export class LoggerService {
  constructor() { }
  log(msg: string) { 
    console.log(msg); 
  }
  error(msg: string) { 
    console.error(msg); 
  }
}

Service is registered in the AppModule class now, refer the providers properties of @NgModule decorator.


import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { UserComponent } from './user/user.component';
import { LoggerService } from './services/logger.service';

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

Service provider example at component level

In an example for registering provider at component level we’ll create an Angular application that shows the existing user and has a UI to add a new User.

Angular injector hierarchy

When a new user is added and submit button is clicked that user details should be added to the list of existing users.

Angular injector and service instance

After submit button is clicked.

In the example there is a UserService class that has an array of users and a function to add new user.

import { User } from '../user/user.model';

export class UserService {
    users = [new User('Jack', 62, new Date('2005-03-25')),
    new User('Olivia', 26, new Date('2014-05-09')),
    new User('Manish', 34, new Date('2010-10-21'))] ;

    addUser(user: User){
        this.users.push(user);
    }
}

For User objects a User model class is used.

export class User {
    name : string;
    age : number;
    joinDate : Date;
    constructor(name: string, age : number, joinDate : Date) {
        this.name = name;
        this.age = age;
        this.joinDate  = joinDate;
    }
}

AppComponent class

This is the component class where UserService is configured with in the providers array. In the ngOnInit() method all the existing users are assigned to the users variable, which is an array of type User, by calling the UserService.

import { Component, OnInit } from '@angular/core';
import { User } from './user/user.model';
import { UserService } from './services/user.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'], 
  providers: [UserService]
})
export class AppComponent implements OnInit{
  users: User[] = [];
  constructor(private userService: UserService){}
  ngOnInit(){
    this.users = this.userService.users;
  }
}

app.component.html

Template has selector tags for the child components.

<div class="container">
  <div class="row">
    <div class="col-xs-12 col-md-8">
      <app-adduser></app-adduser>
      <h3>User Details</h3>
      <app-user *ngFor="let u of users" [user]="u"></app-user>
    </div>
  </div>
</div>

AddUserComponent

In the AddUserComponent which is the child component of the AppComponent, UserService is provided again. There is also a method addUser() that gets the entered value for the new User, create a User object and call the addUser() method of the userService to add the entered user to the array of existing users.

import { User } from './user.model';

@Component({
  selector: 'app-adduser',
  templateUrl: './adduser.component.html',
  styleUrls: ['./adduser.component.css'],
  providers: [UserService]
})
export class AddUserComponent {
 
  constructor(private userService: UserService){}

  addUser(name: HTMLInputElement, age: HTMLInputElement, date: HTMLInputElement){
    this.userService.addUser(new User(name.value, Number(age.value), new Date(date.value)));
    // resetting input elements
    name.value = '';
    age.value = '';
    date.value = '';
  }
}
adduser.component.html

HTML template for providing a UI to add new user. It has three fields for name, age and joining date and a submit button. There is also click event binding calling the addUser() method.

<div class="row">
  <div class="col-xs-12 col-md-8">
    <div class="form-group">
      <label for="name">Name:</label>
      <input class="form-control" placeholder="Enter name" id="name" #enteredname>
    </div>
    <div class="form-group">
      <label for="age">Age:</label>            
      <input class="form-control" placeholder="Enter age" id="age" #enteredage>
    </div>      
    <div class="form-group">
      <label for="joindate">Joining Date:</label>    
      <input class="form-control" placeholder="Enter joining date" id="joindate" #entereddate>
    </div>
    <button (click)="addUser(enteredname, enteredage, entereddate)" class="btn btn-primary">Submit</button>
  </div>
</div>
UserComponent

In the component class there is a property binding using @Input decorator. In the AppComponent template there is a for loop that binds user to this property in each iteration, that’s how user details are displayed.

import { Component, OnInit, Input } from '@angular/core';
import { User } from './user.model';

@Component({
  selector: 'app-user',
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {
  @Input() user: User;
  
  ngOnInit(): void {
  }
}

user.component.html

<div class="container">
  <div class="row">
    <div class="col-xs-12 col-md-8 px-3">
      <p>{{user.name}} {{user.age}} {{user.joinDate | date:'dd/MM/yyyy'}}</p>
    </div>
  </div>
</div>

So these are all the classes and if you have got everything right then you can try to add new user. It won’t add it to the users array and you won’t see any new User details. That will happen because of the provider at the child component too. In the AddUserComponent too a provider is registered, that results in the creation of new UserService instance overriding the instance created by the provider at the AppComponent level.

If you have registered a service provider at parent component level, the created service instance is used by the child components too if they don't register their own provider. In this example since child component registers its own provider so a new service instance is created for the child component.

When you add a new user from AddUserComponent it is added to the array attached to the UserService instance of AddUserComponent. The details which are showed using the user property of UserComponent gets user from the array which is attached to the UserService instance of AppComponent. That is why new user details are not added.

You can comment the providers property in the @Component decorator of the AddUserComponent. Now only Single instance of UserService is created at the AppComponent level (Parent component), child component AddUserComponent shares the same instance.

@Component({
    selector: 'app-adduser',
    templateUrl: './adduser.component.html',
    styleUrls: ['./adduser.component.css'],
    //providers: [UserService]
  })
  export class AddUserComponent {
 ..
 ..
 }

That's all for this topic Injector Hierarchy and Service Instances in Angular. If you have any doubt or any suggestions to make please drop a comment. Thanks!

>>>Return to Angular Tutorial Page


Related Topics

  1. Angular - Call One Service From Another
  2. Creating New Component in Angular
  3. Angular ngFor Directive With Examples
  4. Angular @Input and @Output Example
  5. Angular Disable Button Example

You may also like-

  1. Angular Two-Way Data Binding With Examples
  2. Angular One-Way Data Binding Using String Interpolation
  3. Angular Routing Concepts With Example
  4. Angular Access Control CanActivate Route Guard Example
  5. ConcurrentHashMap in Java
  6. BigDecimal in Java
  7. Spring MVC - Binding List of Objects Example
  8. Bubble Sort Program in Python

No comments:

Post a Comment