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