🚀 Stop Extending Components! Use Angular’s Directive Composition API!
Inheritance in Angular can lead to tight coupling and hard-to-maintain code. With the Directive Composition API, we can compose behaviors into a component using hostDirectives, making it more reusable and flexible.
Let’s refactor a simple button component step by step!
❌ The Old Way: Using Directives Manually
In this approach, we manually apply the directives to the template.
Loading Directive (Disables the button when loading)
import { Directive, input, HostBinding } from '@angular/core';
@Directive({
selector: '[appLoading]',
standalone: true,
})
export class LoadingDirective {
appLoading = input<boolean>(false);
@HostBinding('disabled') get isDisabled() {
return this.appLoading();
}
}
Analytics Directive (Emits an event when clicked)
import { Directive, input, output, HostListener } from '@angular/core';
@Directive({
selector: '[appAnalytics]',
standalone: true,
})
export class AnalyticsDirective {
eventName = input<string>('');
eventTracked = output<string>();
@HostListener('click') onClick() {
console.log(`Button Clicked: ${this.eventName()}`);
this.eventTracked.emit(this.eventName());
}
}
Button Component (Without hostDirectives)
Here, we apply the directives directly in the template.
import { Component, input, output } from '@angular/core';
import { LoadingDirective } from './loading.directive';
import { AnalyticsDirective } from './analytics.directive';
@Component({
selector: 'app-button',
template: `
<button [appLoading]="loading()" [appAnalytics]="eventName()" (eventTracked)="buttonEventTracked.emit($event)">
<ng-content></ng-content>
</button>
`,
standalone: true,
imports: [LoadingDirective, AnalyticsDirective], // Manually import directives
})
export class ButtonComponent {
loading = input<boolean>(false);
eventName = input<string>('');
buttonEventTracked = output<string>();
}
âś… Works fine, but:
- We need to import all directives manually.
- Directive bindings clutter the template.
- Every new directive must be manually added to imports and the template.
âś… The Better Way: Using hostDirectives
With hostDirectives, we move directive logic inside the component, making it cleaner and easier to extend.
import { Component, output } from '@angular/core';
import { LoadingDirective } from './loading.directive';
import { AnalyticsDirective } from './analytics.directive';
@Component({
selector: 'app-button',
template: `<button><ng-content></ng-content></button>`,
standalone: true,
hostDirectives: [
{ directive: LoadingDirective, inputs: ['appLoading: loading'] },
{ directive: AnalyticsDirective, inputs: ['eventName'], outputs: ['eventTracked: buttonEventTracked'] },
],
})
export class ButtonComponent {
buttonEventTracked = output<string>();
}
âś… Why is this better?
- Cleaner template (no more [appLoading] and [appAnalytics]).
- No need to import directives manually in imports.
- Directives are attached automatically, making the component more modular.
Usage (Both Versions Work the Same!)
<app-button [loading]="isLoading" [eventName]="'Submit Clicked'" (buttonEventTracked)="handleEvent($event)">
Submit
</app-button>
🔥 Why Use hostDirectives?
- No manual directive imports — directives are automatically added.
- Cleaner, more readable templates.
- Easier to extend — just add another directive in hostDirectives.
💡 Are you still manually applying directives? Try hostDirectives and make your Angular components simpler and more scalable! 🚀
What do you think? Let’s discuss below! 👇