DEV Community

Cover image for Extending components, directives and services in Angular 14
Arthur Groupp
Arthur Groupp

Posted on

8

Extending components, directives and services in Angular 14

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

Sentry blog image

How I fixed 20 seconds of lag for every user in just 20 minutes.

Our AI agent was running 10-20 seconds slower than it should, impacting both our own developers and our early adopters. See how I used Sentry Profiling to fix it in record time.

Read more

Top comments (0)

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay