First, avoid it as much as possible. Extending components by itself is bad practice because it’s confusing code and adds more issues than actually helps. However, sometimes it can be helpful to combine common logic of similar components or directives into abstract class that they all will extend.
Let’s say, we have two components like this:
import { Component, Injectable } from "@angular/core"; | |
import { ActivatedRoute } from "@angular/router"; | |
import { EMPTY, Observable, of, switchMap } from "rxjs"; | |
@Injectable() | |
export class SomeService { | |
getData(param: string | null): Observable<string> { | |
return param ? of(`DATA of ${param}`) : EMPTY; | |
} | |
} | |
@Component({ | |
selector: 'app-first', | |
template: `<p>First {{ data$ | async }}</p>` | |
}) | |
export class FirstComponent { | |
data$ = this.route.paramMap.pipe(switchMap(paramMap => this.someService.getData(paramMap.get('query')))); | |
constructor(private readonly route: ActivatedRoute, private readonly someService: SomeService) {} | |
} | |
@Component({ | |
selector: 'app-second', | |
template: `<p>Second {{ data$ | async }}</p>` | |
}) | |
export class SecondComponent { | |
data$ = this.route.paramMap.pipe(switchMap(paramMap => this.someService.getData(paramMap.get('query')))); | |
constructor(private readonly route: ActivatedRoute, private readonly someService: SomeService) {} | |
} |
Of course, these components can be combined into one, but let’s assume that this is not the case and they have much more logic and templates specific to each.
Let’s modify our components and put shared logic into abstract base class that they will extend.
import { Component, Injectable } from "@angular/core"; | |
import { ActivatedRoute } from "@angular/router"; | |
import { EMPTY, Observable, of, switchMap } from "rxjs"; | |
@Injectable() | |
export class SomeService { | |
getData(param: string | null): Observable<string> { | |
return param ? of(`DATA of ${param}`) : EMPTY; | |
} | |
} | |
export abstract class Zero { | |
data$ = this.route.paramMap.pipe(switchMap(paramMap => this.someService.getData(paramMap.get('query')))); | |
constructor(protected readonly route: ActivatedRoute, protected readonly someService: SomeService) {} | |
} | |
@Component({ | |
selector: 'app-first', | |
template: `<p>First {{ data$ | async }}</p>` | |
}) | |
export class FirstComponent extends Zero { | |
constructor(protected override readonly route: ActivatedRoute, protected override readonly someService: SomeService) { | |
super(route, someService); | |
} | |
} | |
@Component({ | |
selector: 'app-second', | |
template: `<p>Second {{ data$ | async }}</p>` | |
}) | |
export class SecondComponent extends Zero { | |
constructor(protected override readonly route: ActivatedRoute, protected override readonly someService: SomeService) { | |
super(route, someService); | |
} | |
} |
Much DRYer. The problem with this approach is dependencies. Since our base class has dependencies, it can’t inject it by itself because it doesn’t exist. Therefore, we must inject all those dependencies by ourselves in child classes and supply it to
super()
.
Inject function
Starting with version 14 of Angular we can use function inject()
to obtain instances of injection tokens in other functions, components, services, almost everywhere. This opens us great possibility to avoid repeating of these injections in derived classes. From now on, we can add static method to our base class that will initialize its dependencies.
import { Component, inject, Injectable } from "@angular/core"; | |
import { ActivatedRoute } from "@angular/router"; | |
import { EMPTY, Observable, of, switchMap } from "rxjs"; | |
@Injectable() | |
export class SomeService { | |
getData(param: string | null): Observable<string> { | |
return param ? of(`DATA of ${param}`) : EMPTY; | |
} | |
} | |
export type ZeroDeps = [ActivatedRoute, SomeService]; | |
export abstract class Zero { | |
data$ = this.route.paramMap.pipe(switchMap(paramMap => this.someService.getData(paramMap.get('query')))); | |
constructor(protected readonly route: ActivatedRoute, protected readonly someService: SomeService) {} | |
static deps(): ZeroDeps { | |
const route = inject(ActivatedRoute); | |
const someService = inject(SomeService); | |
return [route, someService]; | |
} | |
} | |
@Component({ | |
selector: 'app-first', | |
template: `<p>First {{ data$ | async }}</p>` | |
}) | |
export class FirstComponent extends Zero { | |
constructor() { | |
super(...Zero.deps()); | |
} | |
} | |
@Component({ | |
selector: 'app-second', | |
template: `<p>Second {{ data$ | async }}</p>` | |
}) | |
export class SecondComponent extends Zero { | |
constructor() { | |
super(...Zero.deps()); | |
} | |
} |
Conclusion
I widely use abstractions and this feature excited me much. I was looking for the ability to avoid manual injections for a long time and now here it is.
Photo by Andrew Seaman on Unsplash
Top comments (0)