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[] = [];
}
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.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.#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>.