skip to content

Attribute-Level Behavior

An attribute directive adds behavior to an existing element through @Directive, its inputs and ElementRef or Renderer2 access. Interviewers ask when NgClass still beats a plain [class] binding.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

6

In Angular, what is an attribute directive, and how would you build and apply an appHighlight directive that highlights an element on hover?

level: juniorimportance: must knowfreq 70%

answer

  1. behaviour without its own template
  2. @Directive with a bracket selector
  3. import it where it is used
  4. input named like the selector
  5. host events toggle the style

basics

~20 s

An Angular attribute directive is a class decorated with @Directive and an attribute selector such as [appHighlight]; it adds behaviour to an existing element or component. Import it into the using component, react to host events and set styles via host bindings or ElementRef.

solid answer

~40 s

An attribute directive changes the look or behaviour of an element it is placed on, without a template of its own; components are directives that do have one. You declare it with `@Directive({selector: '[appHighlight]'})`: the square brackets make it an attribute selector, and an app prefix avoids collisions. Since v19 directives are standalone by default, so the using component lists it in `imports`. For hover highlighting, listen to `mouseenter` and `mouseleave` on the host (through the `host` metadata, which the docs prefer over `@HostListener`) and either set a host style binding from a signal or write `backgroundColor` through the injected `ElementRef`. An `input()` named `appHighlight` lets `[appHighlight]="color"` apply the directive and pass the colour in one attribute.

code

ts · 15 lines
ts
import {Directive, input, signal} from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  host: {
    '(mouseenter)': 'hovered.set(true)',
    '(mouseleave)': 'hovered.set(false)',
    '[style.background-color]': 'hovered() ? appHighlight() || defaultColor() : null',
  },
})
export class Highlight {
  readonly appHighlight = input('');
  readonly defaultColor = input('yellow');
  protected readonly hovered = signal(false);
}

go deeper

for a junior

Recall @Directive, the bracket selector, importing it where used, and the hover highlight example with an input named after the selector.

for a middle

Contrast the declarative host binding with ElementRef writes, and explain static versus bound input values on the same element.

for a senior

Argue when a directive is the right reuse unit versus plain bindings or a wrapper component, and keep it signal-driven so OnPush and zoneless work.

for a principal

Curate a small set of shared directives with a consistent prefix and review rules, rather than one-off directives for every styling need.

## What an attribute directive is Angular has two kinds of directives that you write yourself: - **Components**: directives with a template, which render their own DOM. - **Attribute directives**: directives **without** a template. They attach to an element that already exists, native or component, and change its appearance or behaviour. (Structural directives, which add and remove DOM through `<ng-template>`, are a third kind with their own mechanics.) The Angular docs frame the choice clearly: for a one-off change to a single element's classes, styles, properties or events, use **template bindings** directly. Write an attribute directive when you want to package that behaviour into a **reusable unit** you can apply to any element. ## Building appHighlight ```ts import {Directive, input, signal} from '@angular/core'; @Directive({ selector: '[appHighlight]', host: { '(mouseenter)': 'hovered.set(true)', '(mouseleave)': 'hovered.set(false)', '[style.background-color]': 'hovered() ? appHighlight() || defaultColor() : null', }, }) export class Highlight { readonly appHighlight = input(''); readonly defaultColor = input('yellow'); protected readonly hovered = signal(false); } ``` Piece by piece: 1. **`@Directive`** marks the class; there is no `template`. 2. **`selector: '[appHighlight]'`** is an attribute selector: any element carrying the `appHighlight` attribute, or a binding to it, gets an instance. The style guide recommends a camelCase attribute name with your app's prefix. 3. **`host`** binds the host element's `mouseenter` and `mouseleave` events and its `background-color` style. The docs recommend the `host` property over the `@HostListener` / `@HostBinding` decorators, which remain for backwards compatibility. 4. **`input()`** declares inputs exactly as components do. Naming one after the selector lets a single attribute both apply and configure the directive. 5. A **signal** holds the hover state, so the host style binding updates when it changes, which also works with the `OnPush` default of v22 and zoneless change detection. ## The imperative variant with ElementRef The official tutorial version injects the host element and writes the style directly: ```ts private el = inject(ElementRef); private highlight(color: string) { this.el.nativeElement.style.backgroundColor = color; } ``` It works in the browser, but it is imperative DOM access. The declarative host binding is easier to test, keeps Angular in charge of the DOM, and needs no DOM on the server. Reserve `ElementRef` for things bindings cannot express, such as focus or measuring. ## Applying it Directives are **standalone by default since v19**, so the component that uses one imports it: ```ts @Component({ selector: 'app-price-list', imports: [Highlight], template: ` <p appHighlight>Default colour</p> <p [appHighlight]="accent()" defaultColor="lightgray">Bound colour</p> `, }) export class PriceList { readonly accent = signal('lightblue'); } ``` - `appHighlight` alone applies the directive with an empty input, so the default colour is used. - `[appHighlight]="accent()"` applies it and binds the input. - `defaultColor="lightgray"` passes a **static string** to an input without brackets. Angular creates **one directive instance per matching element**. ## Testing it Attribute directives are tested through a small host component, because they have no template of their own: 1. declare a test host whose template is `<p appHighlight defaultColor="yellow">text</p>` and import `Highlight`; 2. dispatch `mouseenter` on the paragraph and let change detection run; 3. assert the element's `style.backgroundColor`, then dispatch `mouseleave` and assert it is cleared. A declarative host binding makes step 3 deterministic: the style is derived from the `hovered` signal, so there is no stale imperative state to reset between tests. ## Common interview follow-ups | Question | Short answer | |---|---| | Directive or component? | No own template: directive. Own DOM to render: component. | | Why the prefix? | Avoids clashing with native attributes and third-party libraries. | | Can several directives sit on one element? | Yes; each matching directive gets its own instance. | | Can you apply one to a component's host? | Yes, it attaches to the component's host element. | | What if you forget to import it? | A plain attribute stays inert; a bound one fails to compile. |

  • In Angular, what is the difference between an attribute directive and a component?
    A component is a directive with a template: it renders its own DOM inside its host element. An attribute directive has no template; it only adds behaviour to the element it matches, such as styles, classes, listeners or ARIA state. Several attribute directives can share an element, while an element can be the host of at most one component.
  • Why does the Angular documentation prefer the host property over @HostListener in a directive like appHighlight?
    The `host` metadata keeps all host bindings and listeners in one declarative place, and the docs state that `@HostBinding` and `@HostListener` exist only for backwards compatibility. Both compile to the same kind of host bindings; the preference is about consistency with current Angular style.

saying these in an interview costs you the question

  • An attribute directive needs a template like a component
  • Directives are active everywhere once declared, without importing them
  • Directive inputs need a different API from component inputs
  • Only one directive can be applied to an element
  • @HostListener is the recommended way to listen on the host
open as a page

In an Angular directive, when should you use ElementRef.nativeElement, when Renderer2, and when neither?

level: middleimportance: must knowfreq 60%

basics

~20 s

In Angular, prefer host bindings over both. Use ElementRef.nativeElement for focus, measuring or observers, inside render callbacks. Use Renderer2 when created elements need component style encapsulation or animation hooks; otherwise it is equivalent to native DOM APIs.

open as a page

In Angular, when does NgClass or NgStyle still beat a plain [class] or [style] binding, and when should the binding win?

level: middleimportance: should knowfreq 55%

basics

~20 s

Angular's style guide prefers [class] and [style] bindings: no import and less overhead. NgClass still wins when an object key holds several space-separated classes or when the bound object is mutated in place, because [class] compares objects with ===.

open as a page

In Angular, how does an attribute directive's selector match elements, and why can [appHighlight]="color" both apply and configure it?

level: middleimportance: should knowfreq 45%

basics

~20 s

Angular matches directive selectors at compile time against each element's tag, static attributes, classes and binding names. An input with the same name as the attribute selector means [appHighlight]="color" satisfies the selector and sets that input in one step.

open as a page

An Angular appHighlight directive clears its highlight on outside clicks using Renderer2.listen on document, and memory grows as rows come and go; what leaks and how do you fix it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Renderer2.listen on document registers a listener that outlives each directive, and its closure keeps the destroyed directive and its host element reachable. Keep the returned unlisten function and call it from DestroyRef.onDestroy, or use a declarative document listener that Angular removes for you.

open as a page

In Angular, why would you declare exportAs on an attribute directive, and what does a template gain from it?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

Declaring exportAs on an Angular directive gives it a name that templates can use as #x="name" to get the directive instance and call its public members; without exportAs a template cannot reference the directive at all.

open as a page