Acquire services dynamically (or generically) in Angular 2+

Acquiring a service dynamically through injector in Angular 2(+) is pretty straightforward. All you have to do is inject the Injector itself into the component's constructor instead of the services and then get the service instance by using the injector.get() method.
To illustrate this let us consider an example in which we want to show the list of students or teachers, but we don't want to repeat our work so instead of creating two separate components for students and teachers we will create just one component. One way to achieve this by injecting both the Student and the Teacher service into the component and then conditionally getting the list of students or teachers based on the value of objectType variable as is shown below :-

import { Component, OnInit } from '@angular/core';
import { StudentService } from './student.service';
import { TeacherService } from './teacher.service';

@Component({
    template: `
    <ul class="items">
      <li *ngFor="let item of items">      
          <span>{{ item.name }}</span>       
      </li>
    </ul>
    `
})
export class ListComponent implements OnInit {
    private objectType: string;
    public items: any[];

    constructor(private studentService: StudentService, private teacherService: TeacherService) { }

    ngOnInit() {
        if (this.objectType === "Student") {
            this.items = this.studentService.getItems();
        }
        else {
            this.items = this.teacherService.getItems();
        }
    }
}

But this is definitely not the best solution to our problem as it involves injecting all the services to the ListComponent each time although only one of them is used at a time. A better solution would be to inject only the service that is needed at that time. This is exactly what we can achieve by injecting the Injector instead of the services into the component. Following this approach the code for ListComponent can be updated as shown below :-

import { Component, OnInit, Injector } from '@angular/core';
import { StudentService } from './student.service';
import { TeacherService } from './teacher.service';

export const serviceMap = {
    teacher: TeacherService,
    student: StudentService
}

@Component({
    template: `
    <ul class="items">
      <li *ngFor="let item of items">      
          <span>{{ item.name }}</span>       
      </li>
    </ul>
    `
})
export class ListComponent implements OnInit {
    private service: any;
    private objectType: string;
    public items: any[];

    constructor(private injector: Injector) { }

    ngOnInit() {
        if (serviceMap.hasOwnProperty(this.objectType)) {
            this.service = this.injector.get<any>(serviceMap[this.objectType]);
            this.items = this.service.getItems();
        }
    }
}

Major highlights of this code are mentioned below :-
  1. An import for Injector class has been added in the statement :-
    import { Component, OnInit, Injector } from '@angular/core';
    
  2. A "serviceMap" is created to access the service type from the string constants.
  3. The component's own Injector is injected into its constructor instead of the services.
  4. The injected Injector's "injector.get()" method is then used to acquire the required service in the "ngOnInit" method of the component. Note that when injector.get() is called for a service then Angular looks for it in the component's own injector first, if it is not found there then it moves upward in the hierarchy looking through its ancestors for the service and if the service is not found in any of the ancestors as well then the injector.get() method will throw an error. In order to avoid the error you can pass a second argument to the injector.get() method that will be returned if the service is not found.

No comments:

Post a Comment