How do you implement custom structural directives?
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 fromTemplateRefs) 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:
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.
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.
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`)
<!-- 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`)
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.