In Angular, what is the difference between the `host` metadata object and the `@HostBinding`/`@HostListener` decorators, and which should new code use?
answer
- same bindings, two places to declare them
- metadata versus member decorators
- the docs call one backwards compatibility
- listener arguments listed explicitly
basics
~20 sBoth bind properties, attributes, classes, styles and events on a directive's own element; the host object declares them in @Component or @Directive metadata, the decorators on class members. Angular's docs recommend the host object and keep the decorators only for backwards compatibility.
solid answer
~40 sThe host element is the element a component or directive is attached to. You can bind to it in two ways. The `host` object in `@Component`/`@Directive` metadata maps keys to expressions: `'[class.active]': 'isActive()'`, `'[attr.aria-expanded]': 'open()'`, `'(keydown)': 'onKey($event)'`, and plain keys like `'role': 'button'` for static attributes. The decorators do the same from class members: `@HostBinding('attr.aria-expanded')` on a property or getter, and `@HostListener('keydown', ['$event'])` on a method, where the second argument lists the expressions passed to it. Behaviour is identical, and both are type-checked by default since v21. Angular's documentation says to always prefer the `host` property: the decorators exist only for backwards compatibility. It keeps all host bindings in one place and reads naturally with signals.
code
ts · 18 linesimport { Directive, signal } from '@angular/core';
@Directive({
selector: '[appToggle]',
host: {
'role': 'button',
'[attr.aria-pressed]': 'pressed()',
'[class.is-pressed]': 'pressed()',
'(click)': 'toggle()',
'(keydown.enter)': 'toggle()',
},
})
export class ToggleDirective {
readonly pressed = signal(false);
toggle(): void {
this.pressed.update((v) => !v);
}
}go deeper
Recall that both syntaxes bind to the directive's own element and that new code uses the host object in the decorator metadata.
Explain the key syntax for classes, attributes, styles and listeners, how $event reaches a method, and why listeners need no manual cleanup.
Justify the host object for signal-based, OnPush code, and describe how you would handle a codebase that mixes both styles.
Set a convention: host object for new directives, decorators left in place until a file is touched, so the codebase converges without a risky bulk rewrite.
## What a host binding is Every component and directive in Angular is attached to a DOM element, its **host element**. For a component it is the element matching its selector, such as `<app-slider>`; for an attribute directive it is the element carrying the attribute, such as the `<button>` in `<button appTooltip>`. A **host binding** sets a property, attribute, class or style on that element from the class, and a **host listener** runs a class method when the element (or a global target) fires an event. They let a directive change its own element without asking the parent template to add bindings. ## The two syntaxes Angular offers two ways to declare them, and they compile to the same thing. **The `host` metadata object.** Keys use the same syntax as template bindings: - `'role': 'slider'` sets a **static attribute**. - `'[attr.aria-valuenow]': 'value()'` binds an **attribute**. - `'[class.active]': 'isActive()'` toggles a **class**. - `'[style.width.px]': 'width()'` binds a **style** with a unit. - `'[tabIndex]': 'disabled() ? -1 : 0'` binds a DOM **property**. - `'(keydown)': 'onKey($event)'` adds a **listener**; `$event` is the DOM event. - `'(document:click)': 'onDocClick($event)'` listens on a **global target**; the allowed prefixes are `document:`, `window:` and `body:`. **The decorators.** `@HostBinding('attr.aria-valuenow')` goes on a field or getter, and the member's value is bound. With no argument, `@HostBinding()` binds a property named after the member. `@HostListener('keydown', ['$event'])` goes on a method; the first argument is the event name, the second an optional array of expressions whose values become the method's arguments. ## Comparing them | Aspect | `host` object | `@HostBinding` / `@HostListener` | | --- | --- | --- | | Where it lives | Class metadata, all in one place | Scattered across class members | | Static attributes | Plain key such as `'role': 'button'` | No direct form | | Listener arguments | Written inline: `onKey($event)` | Listed as strings: `['$event']` | | Signals | Call them in the expression: `open()` | Usually needs a getter wrapping the signal | | Type checking (v21+) | Checked by default | Checked by default | | Documentation status | Recommended | Kept for backwards compatibility | ## Which to use Angular's host-elements guide is explicit: **always prefer the `host` property**, because the decorators exist exclusively for backwards compatibility. Practical reasons follow from the table: 1. All host behaviour is visible in the metadata, next to the selector, instead of spread across members. 2. Expressions call signals directly, which fits signal-based components and the `OnPush` default in v22. 3. Listener arguments read like template event bindings, rather than as a separate string array that is easy to get out of step with the method's parameters. You will still meet the decorators in most existing codebases and libraries, and they are not deprecated, so an interview answer should explain both and state that new code uses `host`. Mixing both in one class works but makes the class harder to read. ## Converting a directive from decorators Moving an existing directive to the `host` object is mechanical, which makes it a good change to do when a file is touched anyway: 1. For each `@HostBinding('x') member`, add `'[x]': 'member'` to `host`; for a getter, reference the getter's name, or replace the getter with a `computed()` and call it. 2. For each `@HostListener('event', ['a', 'b']) method`, add `'(event)': 'method(a, b)'`, keeping the argument order. 3. For each `@HostBinding('attr.role') role = 'button'` that never changes, use a static key, `'role': 'button'`. 4. Delete the decorators and their imports, then build: host type checking reports any expression or argument that no longer matches. Keep one style per class. A directive that binds `aria-pressed` through a decorator and `class.is-pressed` through the `host` object forces readers to check two places to understand the element's state. ## Details interviewers check - **Listeners are removed automatically.** Host listeners, including `document:` and `window:` ones, are registered by Angular and cleaned up when the directive's view is destroyed, so there is no `removeEventListener` to write. - **Returning `false` prevents the default.** If a host listener returns exactly `false`, the renderer calls `preventDefault()` on the event. - **Listeners mark the view for checking.** A host listener firing marks its view and ancestors dirty, so an `OnPush` component refreshes after its own host event without `markForCheck()`. - **Key filters work in both.** `'(keydown.enter)'` and `@HostListener('keydown.enter')` use the same key-modifier syntax as template event bindings.
- How do you pass the event, or part of it, to a method with `@HostListener`?List the expressions in the decorator's second argument: `@HostListener('keydown', ['$event'])` passes the event, and `['$event.key']` would pass just the key. The array's order must match the method's parameters. In the `host` object you write the call inline instead: `'(keydown)': 'onKey($event)'`.
- Do you need to remove a `window:resize` host listener in `ngOnDestroy`?No. Angular registers host listeners, including those on `window:`, `document:` and `body:`, through its renderer and stores a cleanup for each one, which runs when the directive's view is destroyed. Manual cleanup is only needed for listeners you add yourself.
saying these in an interview costs you the question
- @HostBinding and @HostListener are deprecated and will stop working.
- The host object only supports static attributes, not bindings or listeners.
- Host listeners on window or document must be removed manually in ngOnDestroy.
- @HostListener passes the event to the method automatically without listing $event.
- The two syntaxes behave differently at runtime, so they cannot be swapped.