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?