I really like the component HostDirective pattern, which enables logic composition.
I use it each time I want to reuse UI/UX logic in my dumb component.
But recently, I realized that I can use HostDirective to write my component facade.
While there are already some great articles that talk about hostDirective for dumb component.
Let's explore, why you should use HostDirective for your component facade.
What are HostDirectives ?
Host directives allow you to attach a directive to a component without needing to reference it explicitly in the template. They enable behavior composition, code reuse, and cleaner templates.
- Attach directives directly to components
- Share logic without template pollution
- Expose properties and methods from directives
- Simplify architecture and improve reusability
Simple Example
// This directive adds hover behavior to an element
@Directive({
selector: "[appHover]",
standalone: true,
})
export class HoverDirective {
@HostBinding("style.background") bg = "transparent";
@HostListener("mouseover")
onHover() {
this.bg = "lightblue";
}
@HostListener("mouseout")
onOut() {
this.bg = "transparent";
}
}
// Component using HoverDirective as a hostDirective
@Component({
selector: "app-card",
template: `<p>Hover me!</p>`,
standalone: true,
hostDirectives: [HoverDirective],
})
export class CardComponent {}
The component automatically inherits the hover behavior without needing to add the directive in the template.
Share directive inputs/outputs
It is also possible from the component, to expose the inputs/outputs of the directive.
@Component({
selector: "admin-menu",
template: "admin-menu.html",
hostDirectives: [
{
directive: MenuBehavior,
inputs: ["menuId"],
outputs: ["menuClosed"],
},
],
})
export class AdminMenu {}
Check the official doc about HostDirective, that show other options.
Now, let's create a component facade to see how perfect they are for this case.
Create a component facade directive
For this demo, I will create a counter directive, that will be used as a facade for the child counter component.
The counter directive will have a value and 2 method increase and decrease.
It will also expose isOdd$ output.
@Directive({ standalone: true })
export class ChildCounterFacadeDirective {
public readonly counterValue = model<number>(0);
public readonly counterValueChange = outputFromObservable(
toObservable(this.counterValue)
);
public readonly isOdd = computed(() => !!(this.counterValue() % 2));
public readonly isOdd$ = outputFromObservable(toObservable(this.isOdd));
public increase() {
this.counterValue.update((v) => v + 1);
}
public decrease() {
this.counterValue.update((v) => v - 1);
}
}
For now, the hostDirective pattern does not fully support the
model, so I had to definecounterValueChange.
How to implement the counter facade directive in a component
The child counter component will use the counter directive as a facade.
import { ChangeDetectionStrategy, Component, inject } from "@angular/core";
import { ChildCounterFacadeDirective } from "./child-counter-facade.directive";
@Component({
selector: "app-child-counter",
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [
{
directive: ChildCounterFacadeDirective,
inputs: ["counterValue"],
outputs: ["counterValueChange", "isOdd$"],
},
],
template: `
Child Counter: {{ childCounterFacadeDirective.counterValue() }}
<button (click)="childCounterFacadeDirective.increase()">Increase</button>
<button (click)="childCounterFacadeDirective.decrease()">Decrease</button>
`,
})
export class ChildCounterComponent {
protected readonly childCounterFacadeDirective = inject(
ChildCounterFacadeDirective
);
}
-
ChildCounterFacadeDirectiveis only referenced in hostDirectives You do not need to import and provideChildCounterFacadeDirective - 'counterValue' is exposed as an input
- 'counterValueChange' is exposed as an output
- 'isOdd$' is exposed as an output
-
ChildCounterFacadeDirectiveis injected in the component and can be directly used in the template
As you can see, using a host directive instead of a service reduces drastically the code.
You do not need to struggle to create input / outputs in your component then to pass this value to your service...
So the component looks very clean.
Let's see how the parents can interact with the child counter component.
How to interact with a child component that use an host directive ?
@Component({
selector: "app-root",
imports: [ChildCounterComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div>parentValue: {{ parentValue() }}</div>
<app-child-counter
[(counterValue)]="parentValue"
(isOdd$)="isOdd($event)"
/>
@if(logCD()) {
<div></div>
}
`,
})
export class App {
protected parentValue = signal(10);
isOdd(isOdd: boolean) {
console.log("isOdd", isOdd);
}
logCD() {
console.log("CD run");
return false;
}
}
After importing the child component, it still one line:
<app-child-counter [(counterValue)]="parentValue" (isOdd$)="isOdd(($event))"/>
The app-child-counter looks like a regular component.
That's why I like this pattern, it looks the same from outside, but it helps to remove almost all logic from our component.
I do not say that you should implement a facade for each component. It is your decision.
There are some limitations about this pattern that you may appreciate.
Facade directive - Demo link
Here you can find a stackblitz demo
HostDirective facade pattern limitations
- As I mentioned, the
modelfrom the directive is not fully supported, that's why I had to definecounterValueChange.
I hope it will be solved, that will reduce the code.
- It is not possible to derive a service to a directive.
For eg. if you rely on SignalStore to create your component facade, you can not generate a directive or something that can be used as an hostDirective.
I hope, Angular will evolve and enable this kind of pattern.
(Maybe with the selectorless feature)
My point of view about Angular input/output
Even if I am used to component and directive inputs / outputs, this is a weird concept.
I think this concept can be removed and I think Angular should allow to bind all public properties and methods.
All public is bindable.
It will enable to write a component, almost like you write a service.
Conclusion
HostDirective is an awesome pattern, mainly because it allows to compose your component logic.
Creating a directive for facade component logic reduces the needed code.
If you like this article, please add a like, or a comment.
Have you already used hostDirective like that?
Follow me on LinkedIn for an Angular content.
👉 Romain Geffrault
My other articles that you may like:
Top comments (0)