DEV Community

Cover image for Use Angular HostDirective for all of your component facades
Romain Geffrault
Romain Geffrault

Posted on

Use Angular HostDirective for all of your component facades

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";
  }
}
Enter fullscreen mode Exit fullscreen mode
// Component using HoverDirective as a hostDirective
@Component({
  selector: "app-card",
  template: `<p>Hover me!</p>`,
  standalone: true,
  hostDirectives: [HoverDirective],
})
export class CardComponent {}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

For now, the hostDirective pattern does not fully support the model, so I had to define counterValueChange.

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
  );
}
Enter fullscreen mode Exit fullscreen mode
  • ChildCounterFacadeDirective is only referenced in hostDirectives You do not need to import and provide ChildCounterFacadeDirective
  • 'counterValue' is exposed as an input
  • 'counterValueChange' is exposed as an output
  • 'isOdd$' is exposed as an output
  • ChildCounterFacadeDirective is 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. As I mentioned, the model from the directive is not fully supported, that's why I had to define counterValueChange.

I hope it will be solved, that will reduce the code.

  1. 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)