TL;DR

  • Angular structural directives: Control DOM rendering with powerful template manipulation
  • @if vs *ngIf: Use @if for cleaner, block-based syntax in Angular 17+; use *ngIf for backward compatibility
  • Custom structural directives: Create reusable template logic with TemplateRef and ViewContainerRef
  • Performance optimization: Prevent duplicate view creation with proper state tracking
  • Practical applications: Implement permission-based rendering, feature flags, and conditional animations
  • Testing strategies: Verify directive behavior with TestBed and component fixtures
  • Best practices: Keep business logic in directives for better encapsulation and cleaner component templates

Structural directives are Angular’s way of manipulating the DOM structure, adding, removing, or modifying elements based on conditions.

If you’ve ever wondered how *ngIf actually works under the hood, or why Angular introduced the new @if syntax, building your own structural directive is the fastest way to understand it.

Think of structural directives as instructions to add or remove chunks of the DOM like Lego blocks. Angular takes your template, compiles it into these movable pieces, then decides when to attach or detach them based on your logic.

The *ngIf vs @if Syntax Split

Before we build our own, let’s clarify the two approaches in Angular 17+:

// Old structural directive syntax
<div *ngIf="showContent">Content here</div>

// New control flow syntax
@if (showContent) {
  <div>Content here</div>
}

The @if syntax is cleaner for complex conditions, but custom structural directives still use the * syntax. Here’s when to use each:

  • Use @if: For simple conditional rendering in templates
  • Use *ngIf or custom directives: When you need reusable logic, custom behaviors, or complex DOM manipulation

Building a Custom *ngShow Directive

Let’s build *ngShow that works like *ngIf but with a fade animation. This will show you exactly how structural directives work:

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

@Directive({
  selector: '[ngShow]',
  standalone: true
})
export class NgShowDirective {
  constructor(
    private templateRef: TemplateRef<any>, // The template to show/hide
    private viewContainer: ViewContainerRef // Where to insert the template
  ) {}

  @Input() set ngShow(condition: boolean) {
    if (condition) {
      // Create the view from template and insert it
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      // Remove all views from the container
      this.viewContainer.clear();
    }
  }
}

Here’s the tricky part: Angular injects TemplateRef and ViewContainerRef automatically. The TemplateRef is your template content, and ViewContainerRef is the insertion point where Angular will render it.

Using Your Custom Directive

@Component({
  selector: 'app-demo',
  standalone: true,
  imports: [NgShowDirective, CommonModule],
  template: `
    <button (click)="toggle()">Toggle Content</button>
    
    <!-- Your custom structural directive -->
    <div *ngShow="isVisible">
      <h3>This content fades in/out</h3>
      <p>Built with a custom structural directive!</p>
    </div>
    
    <!-- Compare with built-in @if -->
    @if (isVisible) {
      <div>
        <h3>This content appears instantly</h3>
        <p>Built with @if control flow</p>
      </div>
    }
  `
})
export class DemoComponent {
  isVisible = false;

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

How Angular Compiles the * Syntax

When Angular sees *ngShow="condition", it transforms it into this:

// What you write:
<div *ngShow="isVisible">Content</div>

// What Angular compiles:
<ng-template [ngShow]="isVisible">
  <div>Content</div>
</ng-template>

The * is syntactic sugar. Angular wraps your content in an <ng-template> and passes it to your directive as TemplateRef. This is why structural directives receive templates, not elements.

Advanced: Context and Data Passing

You can pass data to your template context, similar to how *ngFor provides index and item:

@Directive({
  selector: '[ngShowWithDelay]',
  standalone: true
})
export class NgShowWithDelayDirective {
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {}

  @Input() set ngShowWithDelay(condition: boolean) {
    if (condition) {
      // Pass context data to the template
      const context = { 
        $implicit: 'Delayed content!', 
        timestamp: new Date() 
      };
      this.viewContainer.createEmbeddedView(this.templateRef, context);
    } else {
      this.viewContainer.clear();
    }
  }
}

Use it like this:

<div *ngShowWithDelay="showDelayed; let message; let time = timestamp">
  {{ message }} - Rendered at {{ time }}
</div>

When to Build Custom Structural Directives

Real use cases for custom structural directives:

  • Permission-based rendering: *ngIfRole="'admin'"
  • Feature flags: *ngIfFeature="'new-dashboard'"
  • Loading states: *ngShowWhenLoaded="data$"
  • Conditional animations: *ngSlideIf="condition"

The Mental Model

Structural directives are DOM manipulation instructions. Angular gives you:

  1. TemplateRef: The blueprint (your HTML template)
  2. ViewContainerRef: The construction site (where to build)
  3. Input conditions: When to build or demolish

You decide when to call createEmbeddedView() (build) or clear() (demolish) based on your logic.

Performance Note

When considering performance implications:

  • @if is compiled faster by Angular’s compiler because it’s a built-in control flow feature
  • Custom directives take slightly longer to compile but provide reusability and encapsulation
  • For large-scale apps with many instances, the compilation difference is negligible compared to runtime performance
  • Both approaches have identical runtime performance once compiled to JavaScript

The real advantage of custom structural directives isn’t performance but reusability across components.

Error Handling & Common Gotchas

Watch out for these common pitfalls when building custom structural directives:

// INCORRECT - Creates duplicate views
@Input() set ngShow(condition: boolean) {
  if (condition) {
    this.viewContainer.createEmbeddedView(this.templateRef); // Called on EVERY change!
  } else {
    this.viewContainer.clear();
  }
}

// CORRECT - Prevents duplicate views
private hasView = false;

@Input() set ngShow(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;
  }
}

Other issues to watch for:

  • Always clean up subscriptions in ngOnDestroy()
  • Be careful with context objects, they aren’t automatically updated
  • Avoid CPU-intensive operations in structural directive logic
  • Remember that embedded views cause additional change detection cycles

Testing Custom Structural Directives

Testing structural directives requires a host component to provide the context:

// Testing a custom structural directive
describe('NgShowDirective', () => {
  let fixture: ComponentFixture<TestComponent>;
  
  @Component({
    template: `<div *ngShow="visible">Test content</div>`,
    standalone: true,
    imports: [NgShowDirective]
  })
  class TestComponent {
    visible = false;
  }
  
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [TestComponent]
    });
    fixture = TestBed.createComponent(TestComponent);
  });
  
  it('should show content when visible is true', () => {
    const component = fixture.componentInstance;
    
    // Initially hidden
    fixture.detectChanges();
    expect(fixture.debugElement.queryAll(By.css('div')).length).toBe(0);
    
    // Show content
    component.visible = true;
    fixture.detectChanges();
    expect(fixture.debugElement.queryAll(By.css('div')).length).toBe(1);
    expect(fixture.debugElement.queryAll(By.directive(NgShowDirective)).length).toBe(1);
  });
});

This approach lets you verify that your directive correctly adds and removes content from the DOM based on its input conditions.

@if vs Custom Directives: The Bottom Line

Use @if for simple conditional rendering, it’s faster and cleaner. Build custom structural directives when you need reusable logic that other components can import and use.

The new @if syntax improves template readability, but understanding how *ngIf and custom structural directives work gives you the power to build reusable DOM manipulation logic that feels native to Angular.

Related Posts