Angular 15: hostDirectives + InjectionToken

How to avoid duplications and override directive’s inputs value with injection token

Hey, as you know, Angular 15 has been come with a new great feature: hostDirectives.

Since Angular 15 release, I have made some refacto in my angular project and I found this use case: I extend a directive which have an input to handle css classes. In the subsclass, I override the input property and set it a default value. I wanted to remove the extends and find a way to use only the new concept of hostDirectives. And Actually, I found a way which result of the combination of hostDirectives and injection token.

Let’s go for the demo.

Here is a directive which is used to be extended to add some css classes :

@Directive({
selector: '[typography]',
standalone: true,
host: {
'[class.headline-1]': 'typography === "headline-1"',
...
'[class.headline-6]': 'typography === "headline-6"',
'[class.subtitle-1]': 'typography === "subtitle-1"',
'[class.subtitle-2]': 'typography === "subtitle-2"',
'[class.body-1]': 'typography === "body-1"',
'[class.body-2]': 'typography === "body-2"',
'[class.button-typo]': 'typography === "button"',
'[class.caption]': 'typography === "caption"',
},
})
export class TypographyDirective {
@Input('rcTypography') public typography!:
| 'headline-1'
| 'headline-2'
| 'headline-3'
| 'headline-4'
| 'headline-5'
| 'headline-6'
| 'subtitle-1'
| 'subtitle-2'
| 'body-1'
| 'body-2'
| 'button'
| 'caption';
}

And this is how it was used before:

@Component({
selector: 'my-selector',
standalone: true,
template: `...`,
})
export class SubClassComponent extends TypographyDirective {
public override readonly typography = 'body-2';
}

Why? because I wanted to use the advantage of the directive directly on my component and to not rewrite the css classes handling.

It’s ok, it works, it’s readable.

But what if I need to extend a more functionnal class? I have to use mixins ? yes ok I know it’s possible too. But it needs to write more code each time.

A simple way will be to use the combination of hostDirectives metadata and an injection token.

Let’s do it.

// the injection token
export const TYPOGRAPHY_TOKEN: InjectionToken<TypographyDirective['typography']> = new InjectionToken<
TypographyDirective['typography']
>('TYPOGRAPHY_TOKEN');

// the updated base directive
@Directive({
selector: '[typography]',
standalone: true,
host: {
'[class.headline-1]': 'typography === "headline-1"',
...
'[class.headline-6]': 'typography === "headline-6"',
'[class.subtitle-1]': 'typography === "subtitle-1"',
'[class.subtitle-2]': 'typography === "subtitle-2"',
'[class.body-1]': 'typography === "body-1"',
'[class.body-2]': 'typography === "body-2"',
'[class.button-typo]': 'typography === "button"',
'[class.caption]': 'typography === "caption"',
},
})
export class TypographyDirective {
@Input('rcTypography') public typography:
| 'headline-1'
| 'headline-2'
| 'headline-3'
| 'headline-4'
| 'headline-5'
| 'headline-6'
| 'subtitle-1'
| 'subtitle-2'
| 'body-1'
| 'body-2'
| 'button'
| 'caption'
| null = inject(TYPOGRAPHY_TOKEN, { optional: true });
}

The property will be valued by the Input value of by the token value by default. Here’s the updated subclass:

@Component({
selector: 'my-selector',
standalone: true,
template: `...`,
hostDirectives: [
{
directive: TypographyDirective,
},
],
providers: [
{
provide: TYPOGRAPHY_TOKEN,
useValue: 'body-2',
},
],
})
export class SubClassComponent {
}

Et voila! My subclass is free to extend another class, the final code is still readable, it’s perfect. Hmm, wait, what if I want to allow to override the typography property with an input ?

Okay, you just have to indicate the input property like this:

@Component({
selector: 'my-selector',
standalone: true,
template: `...`,
hostDirectives: [
{
directive: TypographyDirective,
inputs: ['typography']
},
],
providers: [
{
provide: TYPOGRAPHY_TOKEN,
useValue: 'body-2',
},
],
})
export class SubClassComponent {
}

and call it like this:

<my-selector ... [typography]="'subtitle-2'"></my-selector>

Even if the injection token is valued, the input value will have the final word.

I hope this will help someone to simplify his code et to write more readable code.

Thanks for reading.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store