🅰️ Angular Q102 / 128

How do you implement custom structural directives?

AI-Powered Answer ✓ Answered

Structural directives are powerful Angular features that manipulate the DOM by adding or removing elements. Built-in examples include `*ngIf`, `*ngFor`, and `*ngSwitch`. Creating your own custom structural directive allows you to encapsulate complex DOM manipulation logic into a reusable pattern, giving you fine-grained control over how parts of your template are rendered.

Understanding Structural Directives

Structural directives are easily identifiable by the asterisk (*) prefix, which is syntactic sugar for an <ng-template> element. They are responsible for shaping or reshaping the DOM's structure, typically by adding, removing, or manipulating elements and their subtrees. Unlike attribute directives, they don't just change an element's appearance or behavior; they change the actual element hierarchy and presence in the DOM.

Core Concepts: TemplateRef and ViewContainerRef

To implement a structural directive, you need to interact with Angular's rendering engine using two fundamental concepts:

  • TemplateRef<T>: Represents an embedded template, such as an <ng-template> element or the template created by the * syntax on an element. It allows you to create instances of the template (EmbeddedViewRef).
  • ViewContainerRef: Represents a container where one or more views (like embedded views created from TemplateRefs) can be attached. It's typically the host element itself where the directive is applied. You use it to create, insert, move, and destroy views.

Step-by-Step Implementation

1. Create the Directive

Generate a new directive using the Angular CLI. Let's create a directive similar to *ngIf, called *appMyCustomIf:

bash
ng generate directive my-custom-if

This command creates src/app/my-custom-if.directive.ts and automatically declares it in your nearest Angular module (e.g., AppModule).

2. Inject TemplateRef and ViewContainerRef

In the directive's constructor, inject TemplateRef (of type any or a specific context type) and ViewContainerRef.

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

@Directive({
  selector: '[appMyCustomIf]'
})
export class MyCustomIfDirective {

  constructor(
    private templateRef: TemplateRef<any>, // The template to be rendered
    private viewContainer: ViewContainerRef // The container to render the template into
  ) { }

}

3. Define an Input Property to Control Rendering

Structural directives typically take an input value that dictates their behavior (e.g., the condition for *ngIf). By convention, the input property should have the same name as the directive's selector (without the asterisk). Use a setter for this input to react to changes.

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

@Directive({
  selector: '[appMyCustomIf]'
})
export class MyCustomIfDirective {

  private hasView = false; // Track if the view is already rendered

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

  @Input()
  set appMyCustomIf(condition: boolean) {
    if (condition && !this.hasView) {
      // If the condition is true and the view is not present, create it
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (!condition && this.hasView) {
      // If the condition is false and the view is present, clear it
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}

In this example: - The @Input() setter appMyCustomIf is triggered whenever the value assigned to *appMyCustomIf changes in the template. - If condition is true and the view hasn't been created yet, this.viewContainer.createEmbeddedView(this.templateRef) renders the content of the <ng-template> (which is the element the directive is applied to) into the DOM. - If condition is false and the view is currently present, this.viewContainer.clear() removes all views from the container, effectively removing the content from the DOM. - The hasView flag prevents unnecessary DOM manipulations.

Usage Example

Once your custom structural directive is declared in an Angular module (e.g., AppModule's declarations array), you can use *appMyCustomIf just like *ngIf in your component templates:

Component Template (e.g., `app.component.html`)

html
<!-- Content rendered only if 'true' -->
<div *appMyCustomIf="true">
  This content will always be shown.
</div>

<!-- Content NOT rendered if 'false' -->
<p *appMyCustomIf="false">
  This paragraph will NOT be shown.
</p>

<button (click)="toggleVisibility()">Toggle Content</button>

<!-- Content toggles based on a component property -->
<h2 *appMyCustomIf="isVisible">
  This content will toggle based on 'isVisible'.
</h2>

Component Class (e.g., `app.component.ts`)

typescript
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  isVisible = true;

  toggleVisibility() {
    this.isVisible = !this.isVisible;
  }
}

This complete setup provides a functional custom structural directive that conditionally renders content based on an input boolean, demonstrating the core principles of structural directive implementation in Angular.