hostDirectives ended my BaseComponent inheritance
Every Angular codebase grows a BaseButtonComponent that three teams extend and nobody dares touch. The directive composition API replaces the inheritance tree with small behaviors you bolt onto the host.
Somewhere in every mature Angular codebase there is a BaseButtonComponent. It started innocently — shared disabled handling, maybe a loading spinner. Then someone added analytics to it. Then keyboard handling. Then a BaseIconButtonComponent extends BaseButtonComponent, and now there's a class four levels up the chain that nobody can modify without re-testing thirty components, because inheritance gives you everything your ancestors ever did, forever, whether you wanted it or not.
The directive composition API is Angular saying: stop inheriting behaviors, apply them.
@Component({
selector: 'app-save-button',
hostDirectives: [
DisabledStateDirective,
{
directive: TooltipDirective,
inputs: ['tooltipText: hint'],
},
],
template: '...',
})
export class SaveButtonComponent {}SaveButtonComponent now behaves as if DisabledStateDirective and TooltipDirective were written on its host element by every consumer, automatically. Each behavior lives in its own small, individually testable directive. No chain, no god-class.
Inputs and outputs are private by default
This is the design decision I appreciate most. A host directive's inputs and outputs are not exposed on the component unless you list them. In the example above, DisabledStateDirective might have inputs of its own, but consumers of app-save-button can't see them; the tooltip's tooltipText input is exposed, renamed to hint. Compare that to inheritance, where every @Input up the chain leaks into your public API and removing one is a breaking change you don't discover until a template somewhere fails to compile. With composition, your component's surface is a deliberate allowlist.
They share the host's DI node
Host directives are instantiated alongside the component, on the same element injector. Which means the component can just ask for them:
export class SaveButtonComponent {
private disabled = inject(DisabledStateDirective);
save() {
if (this.disabled.isDisabled()) return;
// ...
}
}The directive holds the state and the host-element bindings; the component reads it through DI. This is the same relationship a base class gave you via this.isDisabled, except the dependency is explicit, mockable in a test, and swappable. It also works the other way — the directive can inject the host component when it genuinely needs to — though every time I've been tempted by that direction it was the design telling me the directive knew too much.
The sharp edges
Three constraints, all consequences of host directives being a compile-time feature. The directives must be standalone — NgModule-declared ones don't qualify. Application is static: you cannot conditionally attach a host directive at runtime, so if a behavior should sometimes be inert, put the condition inside the directive (an input that gates its listeners) rather than trying to gate its application. And the same directive reaching one host via two paths — say, directly and through another host directive that also applies it — is a compile error, not a silent double application.
Ordering is worth knowing too: host directives' lifecycle hooks run before the host component's own, in the order they're listed. Their host bindings are applied first as well, which lets the component win when both write the same attribute — usually what you want, occasionally a surprise when it isn't.
Where it changed my defaults
The pattern that sold me was aria wiring. We had accessibility logic — role, aria-disabled, keyboard activation on Space/Enter — copy-pasted across a dozen interactive components because putting it in a base class would have forced unrelated widgets into one hierarchy. It's one directive now. Each widget adds it to hostDirectives, exposes nothing, and the behavior has one home and one test file. Inheritance answers "what is this component?" with an ancestry; composition answers with a parts list. For UI behaviors, the parts list is almost always the truthful answer.