In Angular, how do you expose and alias a host directive's inputs and outputs on a component, and which mapping mistakes does the compiler reject?
answer
- strings in inputs and outputs arrays
- original name, colon, public name
- required inputs cannot stay hidden
- one public name per binding
basics
~20 sUse the object form of a hostDirectives entry and list names in inputs and outputs, writing 'text: hint' to expose Tooltip's text as hint. A required host directive input must be exposed, and names must exist and not clash with another binding.
solid answer
~50 sEach `hostDirectives` entry can be a class or `{ directive, inputs, outputs }`. The arrays list the host directive's public binding names; `'text'` exposes it unchanged and `'text: hint'` exposes it under the alias `hint`, so consumers write `<app-menu-item hint="Archive" (hintShown)="track()">` with `outputs: ['shown: hintShown']`. Aliasing lets the component present a vocabulary that fits it rather than the directive's. The compiler validates the mapping: naming a binding the directive does not have is an error; aliasing to a name the directive already uses for a different binding is an error; and a `required` input on the host directive must be exposed (NG2019, "Required input 'text' from host directive Tooltip must be exposed."), because otherwise nothing could ever set it. Exposed inputs then behave like the component's own: they are type-checked in templates, and a required one must be bound by every consumer.
code
ts · 22 linesimport { Component, Directive, input, output } from '@angular/core';
@Directive({
host: { '[attr.title]': 'text()', '(mouseenter)': 'shown.emit()' },
})
export class Tooltip {
readonly text = input.required<string>();
readonly shown = output<void>();
}
@Directive({ host: { '[class.focus-ring]': 'true' } })
export class FocusRing {}
@Component({
selector: 'app-menu-item',
template: '<ng-content />',
hostDirectives: [
{ directive: Tooltip, inputs: ['text: hint'], outputs: ['shown: hintShown'] },
FocusRing,
],
})
export class MenuItem {}go deeper
Recall the object form with inputs and outputs arrays and that 'text: hint' exposes text under the name hint.
Explain the validation errors, especially why a required host directive input must be exposed, and how exposed bindings are type-checked.
Design the component's public API deliberately: expose the minimum, alias for the component's vocabulary, and keep aliases consistent across a library.
Set library-wide naming rules for composed behaviours so that shared directives merge cleanly and consumers see one consistent vocabulary.
## The scenario A design system has two standalone directives: `Tooltip`, with a required `text` input and a `shown` output, and `FocusRing`, which shows a focus outline for keyboard users and has no inputs. The `MenuItem` component wants both behaviours on every `<app-menu-item>`, and it wants its public API to read naturally for a menu: `hint` rather than `text`, `hintShown` rather than `shown`. ## Exposing bindings A `hostDirectives` entry has two forms: - **The bare class**, `FocusRing`, applies the directive and exposes nothing. - **The object form**, `{ directive: Tooltip, inputs: [...], outputs: [...] }`, applies it and lists which bindings become part of the component's API. Each string is the host directive's **public name** of the binding, optionally followed by a colon and the **alias** to expose it under: 1. `inputs: ['text']` exposes the input as `text`. 2. `inputs: ['text: hint']` exposes it as `hint`; `text` itself is not bound from outside. 3. `outputs: ['shown: hintShown']` exposes the output as `hintShown`, so a consumer writes `(hintShown)="track()"`. The resulting template reads `<app-menu-item hint="Archive this project" (hintShown)="track()">`. ## What the compiler checks The mapping is validated at build time, and again in development mode at runtime: | Mistake | Example | Result | | --- | --- | --- | | Name the directive does not have | `inputs: ['label']` on `Tooltip` | Error: the directive has no input with that public name | | Alias taken by another binding of the directive | Aliasing `text` to a name `Tooltip` already uses for a different input | Error: cannot alias, a different binding has that public name | | Required input left hidden | `Tooltip` has `text = input.required<string>()`, entry omits it | NG2019: required input from host directive must be exposed | | Same shared directive exposed under two aliases (v22 merge) | Two paths expose `text` as `hint` and as `label` | NG8024: conflicting host directive binding | The **required-input rule** is the one people meet first. A required input promises that every usage supplies a value; if the component did not expose it, no template could, so the compiler insists. Exposing it under an alias satisfies the rule, and the component's consumers then get the usual "required input" template error if they forget to bind `hint`. ## How exposed bindings behave - **Type-checked like the component's own.** Binding `[hint]="count"` where `text` is a `string` input is reported by the template type checker. - **Required-ness carries over.** A required host directive input becomes a required input of the component's element. - **Outputs are subscribed as usual.** `(hintShown)` subscribes to `Tooltip.shown`, and the handler runs in the consumer's context. - **The component does not get a class property.** `MenuItem` has no `hint` field; the value lives in the `Tooltip` instance, which `MenuItem` can `inject()` to read. ## Walking through the scenario The `MenuItem` author works through four decisions: 1. **Which behaviours?** `Tooltip` for the hint and `FocusRing` for keyboard focus. Both are standalone, selector-less directives written for composition. 2. **Which bindings become API?** The hint text must be settable per item, and analytics wants to know when a hint is shown, so `text` and `shown` are exposed. `FocusRing` has nothing to configure, so it stays a bare class entry. 3. **Which names?** On a menu item, `hint` and `hintShown` describe the purpose better than `text` and `shown`, so both are aliased. 4. **What is mandatory?** `text` is required on `Tooltip`, so it must be exposed; consumers who omit `hint` now get a template error, which is what the component wants. A consumer never sees `Tooltip` or `FocusRing` by name. From the outside, `<app-menu-item>` simply has a required `hint` input and a `hintShown` output. ## Design guidance - **Expose the minimum.** Every exposed binding becomes a contract consumers can depend on, so expose only what the component should support. - **Alias for the component's vocabulary**, not for the sake of it: `hint` reads better on a menu item, while `text` might be fine on a button. - **Keep aliases consistent across a library.** If several components compose `Tooltip`, choosing the same alias everywhere avoids the NG8024 conflict when two of them share a shared directive through de-duplication, and makes the API predictable.
- After aliasing `text` to `hint`, can a consumer still bind `[text]` on `<app-menu-item>`?No. Only the listed public names are exposed, and the entry exposes the input as `hint`. `text` is not an input of the element, so a property binding to it is reported as unknown.
- Why does Angular force a required host directive input to be exposed rather than letting the component set it?The host component has no API for writing a host directive's signal input; inputs are set by template bindings. If the required input were hidden, no binding could ever supply it, so the requirement could never be met. Exposing it moves the obligation to the component's consumers.
saying these in an interview costs you the question
- The alias syntax is 'publicAlias: originalName', with the alias first.
- Aliasing an input also creates a property with that name on the component class.
- A required host directive input can stay hidden and default to undefined.
- Outputs cannot be exposed from host directives, only inputs.
- Exposing an input under an alias also keeps the original name bindable.