Creating Custom Attribute and Structural Directives in Angular

In the previous lessons, we explored built-in directives like ngIf and ngStyle. While these are powerful, Angular allows you to build your own custom directives to encapsulate complex behavior and reuse logic across your application. This lesson focuses on creating Custom Attribute Directives and Custom Structural Directives from scratch.

Understanding Attribute vs. Structural Directives

Before diving into the code, it is essential to distinguish between the two main types of directives you will create:

  • Attribute Directives: These change the appearance or behavior of an existing element. They look like regular HTML attributes (e.g., [appMyDirective]).
  • Structural Directives: These change the DOM layout by adding or removing elements. They are characterized by the asterisk prefix (e.g., *appMyDirective).

1. Creating a Custom Attribute Directive

Attribute directives are used to listen to events or change the style of the host element. We use the @Directive decorator and inject ElementRef to access the DOM.

Example: A Highlighting Directive

Let's create a directive that changes the background color of an element when the user hovers over it.

import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  @Input() highlightColor: string = 'yellow';

  constructor(private el: ElementRef) {}

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.highlightColor);
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string | null) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}
    

How it works:

  • Selector: The square brackets [appHighlight] mean this directive is applied as an attribute.
  • ElementRef: This grants direct access to the DOM element.
  • HostListener: This decorator listens to events on the host element (like mouseenter).
  • Input: Allows us to pass data (color) into the directive.

2. Creating a Custom Structural Directive

Structural directives are more complex because they manage a Template. To create one, you need TemplateRef (the content inside the directive) and ViewContainerRef (the container where the content is rendered).

Example: The "Unless" Directive

We will create a directive that does the opposite of ngIf. It will render the content only if the condition is false.

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appUnless]',
  standalone: true
})
export class UnlessDirective {
  private hasView = false;

  @Input() set appUnless(condition: boolean) {
    if (!condition && !this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (condition && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {}
}
    

The Logic Flow:

  • Condition is False: If the view hasn't been created yet, we use createEmbeddedView to inject the template into the DOM.
  • Condition is True: We use viewContainer.clear() to remove the element from the DOM.

Usage in HTML:

<p *appUnless="isLoggedIn">Please log in to see this content.</p>
    

Visualizing the Directive Mechanism

Here is a simple logic flow of how Angular processes a Structural Directive:

  • Step 1: Angular encounters the * prefix.
  • Step 2: It transforms the * syntax into an <ng-template> element.
  • Step 3: The Directive class receives the TemplateRef.
  • Step 4: The Directive logic decides whether to use ViewContainerRef to render that template.

Common Mistakes to Avoid

  • Forgetting the Asterisk: Forgetting the * on a structural directive will prevent Angular from treating the element as a template, leading to errors.
  • Direct DOM Manipulation: While ElementRef allows direct DOM access, it is better to use Renderer2 for better compatibility with server-side rendering (SSR).
  • Case Sensitivity: Ensure the @Input property name matches the directive's selector name if you want to pass values directly through the attribute name.

Real-World Use Cases

  • Role-Based Access: A structural directive *appHasRole="'admin'" that hides elements if the user lacks permissions.
  • Lazy Loading Images: An attribute directive that only sets the src attribute when the image enters the viewport.
  • Input Masking: An attribute directive that automatically formats phone numbers or currency as the user types.

Interview Notes

  • Question: What is the difference between TemplateRef and ViewContainerRef?
  • Answer: TemplateRef represents the "what" (the HTML content inside the ng-template), while ViewContainerRef represents the "where" (the location in the DOM where the template will be attached).
  • Question: Can an element have two structural directives?
  • Answer: No, Angular does not allow multiple structural directives on a single element (e.g., *ngIf and *ngFor together). You must use a wrapper like <ng-container>.

Summary

Custom directives are a cornerstone of Angular's extensibility. Attribute directives help you modify element behavior and appearance, while structural directives give you control over the DOM structure itself. By mastering HostListener, TemplateRef, and ViewContainerRef, you can create highly reusable and clean code. In the next part of our Complete Angular Masterclass, we will explore Angular Pipes to transform data directly in your templates.

Be sure to check our previous lesson on understanding-angular-directives to solidify your foundation before moving forward!