AngularJSAttribute Directives

Attribute Directives in Angular

Attribute directives change the appearance or behaviour of an element, component, or other directive — without touching the DOM structure. They look like HTML attributes and are the most common type of custom directive you will write.

Unlike structural directives (*ngIf, *ngFor), attribute directives do not add or remove elements. They decorate the element they are applied to.

Built-in Attribute Directives

Angular ships several useful attribute directives in @angular/common.

Directive

Purpose

NgClass

Add/remove CSS classes dynamically

NgStyle

Set inline styles dynamically

NgModel

Two-way data binding for form inputs (FormsModule)

NgTemplateOutlet

Render a template reference in place

NgComponentOutlet

Dynamically render a component

NgClass

TS
import { Component } from '@angular/core';
import { NgClass } from '@angular/common';

@Component({
  selector: 'app-ngclass-demo',
  standalone: true,
  imports: [NgClass],
  template: `
    <!-- Object syntax: key=class, value=boolean -->
    <div [ngClass]="{
      'alert':         true,
      'alert-success': type === 'success',
      'alert-error':   type === 'error',
      'alert-warning': type === 'warning'
    }">{{ message }}</div>

    <!-- Array syntax: add multiple classes -->
    <button [ngClass]="['btn', sizeClass, variantClass]">
      Click me
    </button>

    <!-- String syntax: just set classes directly -->
    <p [ngClass]="computedClass">Paragraph</p>

    <!-- Single class toggle shorthand (no NgClass needed) -->
    <div [class.active]="isActive">Shorthand toggle</div>
  `,
})
export class NgClassDemoComponent {
  type = 'success';
  message = 'Operation completed successfully!';
  sizeClass = 'btn-lg';
  variantClass = 'btn-primary';
  isActive = true;

  get computedClass() {
    return this.isActive ? 'text-success font-bold' : 'text-muted';
  }
}
NgStyle

TS
import { Component } from '@angular/core';
import { NgStyle } from '@angular/common';

@Component({
  selector: 'app-ngstyle-demo',
  standalone: true,
  imports: [NgStyle],
  template: `
    <!-- Object map of style properties -->
    <div [ngStyle]="{
      'background-color': bgColor,
      'color':            textColor,
      'font-size.px':     fontSize,
      'border-radius.px': 8,
      'padding':          '12px 16px',
      'opacity':          isVisible ? 1 : 0.3
    }">Styled box</div>

    <!-- Single style shorthand (no NgStyle needed) -->
    <p [style.color]="textColor">Shorthand color</p>
    <p [style.font-size.px]="fontSize">Shorthand size</p>
  `,
})
export class NgStyleDemoComponent {
  bgColor = '#e8f4fd';
  textColor = '#0056b3';
  fontSize = 16;
  isVisible = true;
}
Tip
Prefer CSS classes over inline styles wherever possible. Use \`[ngStyle]\` only for values that genuinely cannot be expressed as a class (e.g. a width percentage calculated from data).
Creating a Custom Attribute Directive

Writing a custom attribute directive involves three steps:

  1. Create a class decorated with @Directive({ selector: '[appMyDirective]' }).
  2. Inject ElementRef to access the host DOM element.
  3. Use @HostListener to respond to events and @HostBinding to set properties.

TS
// tooltip.directive.ts
import {
  Directive,
  ElementRef,
  Input,
  OnInit,
  OnDestroy,
  Renderer2,
  HostListener,
} from '@angular/core';

@Directive({
  selector: '[appTooltip]',
  standalone: true,
})
export class TooltipDirective implements OnInit, OnDestroy {
  @Input('appTooltip') text = '';
  @Input() tooltipPosition: 'top' | 'bottom' = 'top';

  private tooltipEl: HTMLElement | null = null;

  constructor(
    private el: ElementRef,
    private renderer: Renderer2,
  ) {}

  ngOnInit() {
    // Set accessible title attribute as a fallback
    this.renderer.setAttribute(this.el.nativeElement, 'title', this.text);
  }

  @HostListener('mouseenter')
  show() {
    this.tooltipEl = this.renderer.createElement('div');
    this.renderer.addClass(this.tooltipEl, 'tooltip');
    this.renderer.addClass(this.tooltipEl, `tooltip-${this.tooltipPosition}`);
    const textNode = this.renderer.createText(this.text);
    this.renderer.appendChild(this.tooltipEl, textNode);
    this.renderer.appendChild(document.body, this.tooltipEl);

    const rect = this.el.nativeElement.getBoundingClientRect();
    this.renderer.setStyle(this.tooltipEl, 'top', `${rect.top - 36}px`);
    this.renderer.setStyle(this.tooltipEl, 'left', `${rect.left}px`);
    this.renderer.setStyle(this.tooltipEl, 'position', 'fixed');
  }

  @HostListener('mouseleave')
  hide() {
    if (this.tooltipEl) {
      this.renderer.removeChild(document.body, this.tooltipEl);
      this.tooltipEl = null;
    }
  }

  ngOnDestroy() {
    this.hide();
  }
}

HTML
<button [appTooltip]="'Save your changes'" tooltipPosition="top">
  Save
</button>

<span appTooltip="This field is required" class="info-icon">?</span>
Warning
Use \`Renderer2\` instead of directly accessing \`document\` or \`element.style\` where possible. \`Renderer2\` is SSR-safe and works in web workers where \`document\` is unavailable.
HostBinding and HostListener in Depth

@HostBinding binds a directive property to a property of the host element. @HostListener subscribes to events on the host element. Together they are the idiomatic way to react to and modify the host element.

TS
// auto-focus.directive.ts
import {
  Directive,
  ElementRef,
  HostBinding,
  HostListener,
  OnInit,
  Input,
} from '@angular/core';

@Directive({
  selector: 'input[appAutoFocus]',
  standalone: true,
})
export class AutoFocusDirective implements OnInit {
  @Input() selectOnFocus = false;

  // Bind to the host element's tabIndex property
  @HostBinding('tabIndex') tabIndex = 0;

  // Bind to a CSS class on the host element
  @HostBinding('class.focused') isFocused = false;

  // Bind to an aria attribute
  @HostBinding('attr.aria-label')
  @Input() label = '';

  constructor(private el: ElementRef<HTMLInputElement>) {}

  ngOnInit() {
    setTimeout(() => this.el.nativeElement.focus(), 0);
  }

  @HostListener('focus')
  onFocus() {
    this.isFocused = true;
    if (this.selectOnFocus) {
      this.el.nativeElement.select();
    }
  }

  @HostListener('blur')
  onBlur() {
    this.isFocused = false;
  }
}

HTML
<!-- Focuses on page load, selects text when re-focused -->
<input appAutoFocus [selectOnFocus]="true" label="Search field" placeholder="Search..." />
Directive with @Input Using the Directive Name

A common and clean pattern is to use the directive's selector as the @Input alias, so configuring the directive value and applying it happen in one attribute.

TS
// badge-color.directive.ts
import { Directive, Input, HostBinding } from '@angular/core';

type BadgeColor = 'primary' | 'success' | 'warning' | 'danger';

@Directive({
  selector: '[appBadge]',
  standalone: true,
})
export class BadgeDirective {
  // Using the selector as the @Input alias
  @Input('appBadge') color: BadgeColor = 'primary';

  @HostBinding('class')
  get badgeClasses() {
    return `badge badge-${this.color}`;
  }
}

HTML
<!-- The directive selector doubles as the input binding -->
<span appBadge="success">Active</span>
<span appBadge="danger">Banned</span>
<span [appBadge]="userStatusColor">{{ userStatus }}</span>
Reading Host Properties from a Directive

TS
// char-limit.directive.ts
import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: 'textarea[appCharLimit]',
  standalone: true,
})
export class CharLimitDirective {
  @Input('appCharLimit') limit = 200;

  constructor(private el: ElementRef<HTMLTextAreaElement>) {}

  @HostListener('input')
  onInput() {
    const textarea = this.el.nativeElement;
    const value = textarea.value;
    const remaining = this.limit - value.length;

    // Visual feedback: red border when near/over limit
    textarea.style.borderColor = remaining < 0 ? 'red' : remaining < 20 ? 'orange' : '';

    // Hard truncation at limit
    if (value.length > this.limit) {
      textarea.value = value.slice(0, this.limit);
    }
  }
}

HTML
<textarea [appCharLimit]="280" rows="4" placeholder="Tweet (max 280 chars)"></textarea>
Best Practices
  • Use Renderer2 instead of direct DOM manipulation for SSR compatibility.

  • Always clean up in ngOnDestroy — remove event listeners, DOM nodes, subscriptions.

  • Use @HostBinding / @HostListener instead of ElementRef where possible — cleaner and testable.

  • Prefer CSS classes over inline [ngStyle] — easier to maintain and theme.

  • Name custom directives with an app prefix (appTooltip, appHighlight) to avoid collisions.

  • Make directives configurable with @Input so they can be reused across projects.

  • Keep directives focused on a single responsibility — split large directives into smaller ones.