ng-container, ng-template and Structural Directive Patterns

๐Ÿ“‹ Table of Contents โ–พ
  1. ng-container and ng-template
  2. Common Mistakes

ng-container and ng-template are Angular’s template primitives that provide structural flexibility without adding DOM elements. ng-container is an invisible grouping element โ€” it groups content and supports structural directives without adding a wrapper <div> that could break CSS grid or flexbox layouts. ng-template is a reusable template fragment โ€” it renders nothing on its own but can be instantiated with ngTemplateOutlet, passed as an @Input to child components, or referenced in *ngIf; else patterns.

ng-container and ng-template

import { NgTemplateOutlet, NgIf, NgFor } from '@angular/common';

@Component({
  standalone:  true,
  imports:    [NgTemplateOutlet, NgIf, NgFor],
  template: `
    <!-- โ”€โ”€ ng-container โ€” groups without adding a DOM element โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -->
    <table>
      <tr>
        <!-- Apply *ngFor directly on tr without extra wrapper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -->
        <ng-container *ngFor="let col of columns">
          <th *ngIf="col.visible">{{ col.label }}</th>
        </ng-container>
      </tr>
      <tr *ngFor="let row of rows">
        <ng-container *ngFor="let col of columns">
          <td *ngIf="col.visible">{{ row[col.field] }}</td>
        </ng-container>
      </tr>
    </table>

    <!-- โ”€โ”€ ng-template โ€” reusable template fragment โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -->
    <ng-template #loadingTemplate>
      <div class="loading"><app-spinner /> Loading...</div>
    </ng-template>

    <ng-template #errorTemplate let-err="error">  <!-- context variable โ”€โ”€โ”€ -->
      <div class="error">Error: {{ err }}</div>
    </ng-template>

    <!-- โ”€โ”€ ngTemplateOutlet โ€” render a template fragment โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -->
    <div *ngIf="isLoading; else contentTemplate">
      <ng-container [ngTemplateOutlet]="loadingTemplate" />
    </div>
    <ng-template #contentTemplate>
      <div>Content here</div>
    </ng-template>

    <!-- ngTemplateOutlet with context โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -->
    <ng-container
      [ngTemplateOutlet]="errorTemplate"
      [ngTemplateOutletContext]="{ error: errorMessage }"
    />

    <!-- โ”€โ”€ Pass template as @Input to child component โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -->
    <app-data-table
      [rowTemplate]="customRowTemplate"
      [data]="tableData"
    />
    <ng-template #customRowTemplate let-row>
      <td>{{ row.title }}</td>
      <td>{{ row.status }}</td>
    </ng-template>
  `,
})
export class PostTableComponent {
  columns      = [{ field: 'title', label: 'Title', visible: true }, ...];
  rows         = signal<PostSummaryDto[]>([]);
  isLoading    = signal(false);
  errorMessage = signal<string | null>(null);
  tableData    = signal<any[]>([]);
}

// โ”€โ”€ DataTable component accepting a template as @Input โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import { Component, Input, ContentChild, TemplateRef } from '@angular/core';

@Component({
  selector:   'app-data-table',
  standalone:  true,
  imports:    [NgFor, NgTemplateOutlet],
  template: `
    <table>
      <tbody>
        <tr *ngFor="let item of data">
          <!-- Render the parent-provided row template โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -->
          <ng-container
            [ngTemplateOutlet]="rowTemplate"
            [ngTemplateOutletContext]="{ $implicit: item }"
          />
        </tr>
      </tbody>
    </table>
  `,
})
export class DataTableComponent {
  @Input() rowTemplate!: TemplateRef<{ $implicit: any }>;
  @Input() data: any[] = [];
}
Note: ng-container renders no DOM element โ€” it is completely invisible in the rendered HTML. This is critical for table layouts (<tr>, <td>), flex containers, and CSS grid where extra wrapper <div> elements break the layout. If you have a <div> wrapper only to apply a structural directive and that wrapper breaks your CSS, replace it with <ng-container>. The @if / @for new syntax from Angular 17 also eliminates this need since they are block-level, not attribute directives.
Tip: The TemplateRef pattern โ€” where a component accepts an ng-template as an @Input โ€” is the Angular way to build truly customisable components. A table component that accepts a rowTemplate input allows each usage site to define exactly how rows are rendered, while the table component handles sorting, pagination, and layout. This is how Angular CDK table (<cdk-table>) and Angular Material table work. Building your own reusable table with this pattern teaches the fundamental Angular component composition model.
Warning: Template references (#templateRef) to ng-template elements create TemplateRef objects โ€” not DOM elements. If you use @ViewChild('loadingTemplate') in the component class, TypeScript types it as TemplateRef<any>, not as ElementRef. Passing a TemplateRef where an ElementRef is expected (or vice versa) is a common type error. Always declare @ViewChild('name') name!: TemplateRef<any> for template references to ng-template elements.

Common Mistakes

Mistake 1 โ€” Using <div> instead of <ng-container> as structural directive wrapper (breaks table/flex layout)

โŒ Wrong โ€” <div *ngFor="let row of rows"><tr>; invalid HTML; div inside tbody breaks the table.

โœ… Correct โ€” <ng-container *ngFor="let row of rows"><tr>; no extra DOM element.

Mistake 2 โ€” Confusing ng-template reference with TemplateRef and ElementRef types

โŒ Wrong โ€” @ViewChild('tmpl') tmpl!: ElementRef; ng-template references are TemplateRef, not ElementRef.

โœ… Correct โ€” @ViewChild('tmpl') tmpl!: TemplateRef<any>.

🧠 Test Yourself

A CSS grid container has direct children that must all be grid items. Using <div *ngFor> inside the grid adds an extra wrapper. What should be used instead?