Dependency Inheritance with Angular Injector

angular
typescript
Inheritance
angular-injector

How to use Angular Injector to build generic components and use dependency injection.


Using Angular and Typescript allows us to use several concepts of Object Oriented Programming, such inheritance between components. However, we face some problems when we need our component to use the injection of some dependency, such as angular services.

Creating a base component

Let's start by creating a base component, to be inherited by others.

import { Injectable, OnDestroy, OnInit } from "@angular/core";
import { LoggerService } from './../logger.service';

@Injectable()
export abstract class BaseComponent implements OnInit, OnDestroy {
  
  constructor(
    private logger: LoggerService,
  ) {}

  ngOnInit(): void {
    this.logger.log("BaseComponent > ngOnInit");
  }

  ngOnDestroy(): void {
    this.logger.log("BaseComponent > ngOnDestroy");
  }
}

Why we used the decorator @Injectable instead of @Component?
Because Angular AOT compiler force everything that can be used in runtime, needs to be declared previously. Since we don't want to instantiate our BaseComponent, we declare it as abstract. This way, it will never be instantiated and cannot be declared with @Component.

So let's create 2 components that will inherit from BaseComponent:

import { Component } from '@angular/core';
import { BaseComponent } from '../base/base.component';

@Component({
  selector: 'app-component-a',
  templateUrl: './a.component.html',
  styleUrls: ['./a.component.scss']
})
export class AComponent extends BaseComponent { }


@Component({
  selector: 'app-component-b',
  templateUrl: './b.component.html',
  styleUrls: ['./b.component.scss']
})
export class BComponent extends BaseComponent { }

Our child components are ready! When we navigate between our child components, log will be correctly saved by LoggerService injected in BaseComponent.

But we got our first problem:
If we need to add any dependency on any child component, we will need to declare its constructor. In this constructor we'll need to call the super() method and pass the parameters declared by BaseComponent, in the same order. You can imagine the confusion.

Let's make the scenario a little harder, let's imagine that we want to add another dependency on our BaseComponent. For this, we'll need to edit all children components that have a constructor and add the desired dependency.
This process becomes unfeasible in larger projects.

Fortunately, there is a solution. We can make our BaseComponent manually inject its dependencies when it needs. For that, we will need to use the Injector. The biggest change will be in the BaseComponent constructor, which will now receive only the Injector, looking like this:

import { OnInit, Injector, OnDestroy } from '@angular/core';
import { LoggerService } from './../logger.service';

export abstract class BaseComponent implements OnInit, OnDestroy {

  protected logger: LoggerService;
  constructor(injector: Injector) {
    this.logger = injector.get(LoggerService);
  }

  // ...
}

We only inject the Injector and the other dependencies we'll need we can get using injector.get().
Let's also modify our children component, looking like this:

import { Component } from '@angular/core';
import { BaseComponent } from '../base/base.component';

@Component({
  selector: 'app-component-a',
  templateUrl: './a.component.html',
  styleUrls: ['./a.component.scss']
})
export class AComponent extends BaseComponent {

  constructor(injector: Injector) {
    super(injector);
  }
}

This way, our BaseComponent can have as many dependencies as there are, and when we'll need to add a new one, Wont be necessary to refactor the components that inherit it.

But it is still necessary to inject the Injector in the constructor of the child components. Let's see how to further improve this code.
Our goal is to completely remove the injecting of Injector into the children components constructors.
For that, we can create our own injector service and instantiate it in AppModule, so that will be available for the entire project.

Let's create the app-injector.service.ts

import { Injector } from '@angular/core';

export class AppInjector {

    private static _injector: Injector;

    static set injector(injector: Injector) {
        this._injector = injector;
    }

    static get injector(): Injector {
        return this._injector;
    }
}

Change the app.module.ts

// ...
export class AppModule {
  constructor(injector: Injector) {
    AppInjector.injector = injector;
  }
}

Now you can change the BaseComponent to look like this:

import { OnInit, Injector, OnDestroy } from '@angular/core';
import { LoggerService } from './../logger.service';

export abstract class BaseComponent implements OnInit, OnDestroy {

  protected logger: LoggerService;
  constructor() {
    this.logger = AppInjector.injector.get(LoggerService);
  }

  // ...
}

Children components now can have their own constructor, without needing to inject the Injector:

import { Component } from '@angular/core';
import { BaseComponent } from '../base/base.component';

@Component({
  selector: 'app-component-a',
  templateUrl: './a.component.html',
  styleUrls: ['./a.component.scss']
})
export class AComponent extends BaseComponent {

  constructor() {
    super();
  }
}

Exceptions

Scoped Dependency

Some dependencies are different depending on what scope are they injected. Since our AppInjector is a global singleton, it can only get dependencies that are global.

ActivatedRoute

Angular ActivatedRoute its another scoped dependency, causing it to be in the case described above. If BaseComponent needs to inject ActivatedRoute, its better to inject Injector as we did before, instead of using the AppInjector.

ElementRef

This is another scoped dependency which change depending on host component. Inject the Injector instead of ActivatedRoute

Use with caution

The pattern used in this example is known as the ServiceLocator pattern. It is known as an anti-pattern, so use it ONLY on base components. For example, BaseComponent, BaseFormComponent, BaseCrudComponent, etc.

Read more about Service Locator Pattern:
https://en.wikipedia.org/wiki/Service_locator_pattern


References:

Angular: Inheritance Without Effort
https://betterprogramming.pub/angular-inheritance-without-effort-8200c8d87972

Angular docs: @Injectable
https://angular.io/api/core/Injectable

Angular docs: @Component
https://angular.io/api/core/Component

Angular docs: Compilador AOT
https://angular.io/guide/aot-compiler

Wiki: Service Locator Pattern
https://en.wikipedia.org/wiki/Service_locator_pattern