Legacy *ngIf and *ngFor — Structural Directives

The legacy structural directives *ngIf and *ngFor are still widely used in existing Angular codebases and third-party component libraries. Understanding them is essential for reading and maintaining existing code, and for using Angular Material components and other library components that still use the directive syntax. Structural directives are distinguished by the asterisk (*) prefix — they transform the DOM structure by creating, moving, or destroying DOM elements.

*ngIf and *ngFor Patterns

// Import NgIf and NgFor for standalone components (or import CommonModule)
import { NgIf, NgFor, AsyncPipe } from '@angular/common';

@Component({
  standalone:  true,
  imports:    [NgIf, NgFor, AsyncPipe],
  template: `
    <!-- ── *ngIf — basic conditional ────────────────────────────────────── -->
    <div *ngIf="isLoggedIn">Welcome back!</div>

    <!-- ── *ngIf with else ──────────────────────────────────────────────── -->
    <div *ngIf="post; else loading">
      <h1>{{ post.title }}</h1>
    </div>
    <ng-template #loading>
      <app-spinner />
    </ng-template>

    <!-- ── *ngIf as — capture value in template variable ─────────────────── -->
    <div *ngIf="posts$ | async as posts; else noData">
      <p>{{ posts.length }} posts loaded</p>
      <!-- Use 'posts' (typed as PostDto[], not Observable) within this block -->
    </div>
    <ng-template #noData><p>Loading...</p></ng-template>

    <!-- ── *ngFor — basic list ────────────────────────────────────────────── -->
    <ul>
      <li *ngFor="let post of posts; trackBy: trackByPostId">
        {{ post.title }}
      </li>
    </ul>

    <!-- ── *ngFor with index and last variables ──────────────────────────── -->
    <div *ngFor="let tag of tags; let i = index; let isLast = last">
      {{ i + 1 }}. {{ tag }}{{ isLast ? '' : ', ' }}
    </div>

    <!-- ── ng-container — structural wrapper without a DOM element ─────────── -->
    <ng-container *ngIf="canEdit">
      <button>Edit</button>
      <button>Delete</button>
      <!-- No wrapping div in the DOM — ng-container renders nothing itself -->
    </ng-container>
  `,
})
export class PostListLegacyComponent {
  posts$     = this.service.getPublished();
  posts:     PostSummaryDto[] = [];
  tags:      string[] = [];
  isLoggedIn = true;
  canEdit    = false;

  trackByPostId(index: number, post: PostSummaryDto): number {
    return post.id;  // stable identity for DOM recycling
  }
}
Note: *ngIf does not hide elements — it adds or removes them from the DOM entirely. An element with *ngIf="false" does not exist in the DOM at all (unlike CSS display: none which keeps the element present). This means child components inside an *ngIf block are created and destroyed as the condition changes — their lifecycle hooks run (ngOnInit when shown, ngOnDestroy when hidden). If you need to hide an element while preserving its state, use CSS [style.display]="condition ? 'block' : 'none'" instead.
Tip: Always provide a trackBy function for *ngFor when rendering items from an API. Without trackBy, Angular destroys and recreates all list item DOM nodes whenever the array changes — even if only one item was added. The trackBy function returns a unique identifier; Angular uses it to match old DOM nodes with new data, reusing unchanged nodes. This is especially important for lists with animated transitions, input fields inside items, or complex child components with their own state.
Warning: You cannot apply two structural directives to the same element: <div *ngIf="show" *ngFor="let item of items"> is a compile error. Use ng-container to wrap one of them without adding a DOM element: <ng-container *ngIf="show"><div *ngFor="let item of items"></div></ng-container>. This is one of the most common Angular template mistakes for beginners. The new @if / @for syntax eliminates this restriction since they are block-level control flow, not attribute directives.

Common Mistakes

Mistake 1 — Two structural directives on the same element (compile error)

❌ Wrong — <tr *ngIf="show" *ngFor="let row of rows">; Angular cannot have two structural directives on one element.

✅ Correct — wrap in ng-container: <ng-container *ngFor="let row of rows"><tr *ngIf="row.visible">.

Mistake 2 — Not using trackBy for *ngFor with API data (full DOM recreation on every update)

❌ Wrong — *ngFor="let post of posts"; every navigation or filter recreates all DOM nodes.

✅ Correct — *ngFor="let post of posts; trackBy: trackByPostId".

🧠 Test Yourself

A template has *ngIf="post" around a child component. The condition toggles from true to false. What happens to the child component?