- Angular
Angular ng-template・ngTemplateOutletの使い方
<ng-template> は「あとで表示するための部品(テンプレート)」で、書いただけでは何も表示されません。表示するのが ngTemplateOutlet で、そのとき渡す値(context)を let- で受け取ります。
<ng-template #greet let-name let-n="count">
<p>こんにちは、{{ name }}さん({{ n }}回目)</p>
</ng-template>
<ng-container *ngTemplateOutlet="greet; context: { $implicit: '佐藤', count: 1 }" />
<!-- 表示:こんにちは、佐藤さん(1回目) -->
| context に入れるキー | テンプレート側の受け取り方 | 上の例での値 |
|---|---|---|
$implicit(既定の値) |
let-name(= の右を書かない) |
name = '佐藤' |
count など名前付き |
let-n="count"(キー名を右に書く) |
n = 1 |
確認環境:Angular 22.2.2・TypeScript 6.0.3・Node.js 24.21.0 で ng build し、Chromium 141(playwright-core 1.64.0)で表示した DOM と、部品が作られた回数を取り出しました。型付けは、わざと誤字を入れてビルドし、エラーになるかを見ました(確認日 2026年10月13日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angular ng-template・ngTemplateOutletの使い方」
基本:2通りの書き方と描画結果
NgTemplateOutlet を、コンポーネントの imports に入れて使います。
import { Component } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
@Component({
selector: 'app-greet-demo',
imports: [NgTemplateOutlet],
template: `
<ng-template #greet let-name let-n="count">
<p>こんにちは、{{ name }}さん({{ n }}回目)</p>
</ng-template>
<!-- ① * を付けた短い書き方 -->
<ng-container *ngTemplateOutlet="greet; context: { $implicit: '佐藤', count: 1 }" />
<!-- ② 入力を分けて書く書き方 -->
<ng-container [ngTemplateOutlet]="greet" [ngTemplateOutletContext]="{ $implicit: '鈴木', count: 2 }" />
`,
})
export class GreetDemo {}
本番ビルドで表示した、この部分の DOM です。
<!----><p>こんにちは、佐藤さん(1回目)</p><!----><p>こんにちは、鈴木さん(2回目)</p><!---->
<ng-template> と <ng-container> は要素として残らず、コメント(<!---->)になりました。 <p> だけが並び、余計な <div> などは入りません。①と②は同じ結果です。
| 書き方 | 向いている場面 |
|---|---|
*ngTemplateOutlet="tpl; context: ctx" |
1行で書きたいとき |
[ngTemplateOutlet]="tpl" [ngTemplateOutletContext]="ctx" |
context が長いとき、入力を分けて読みやすくしたいとき |
NgTemplateOutlet の入力一覧
Angular のドキュメントにある入力は3つです。
| 入力 | 型 | 意味 |
|---|---|---|
ngTemplateOutlet |
TemplateRef<C> | null | undefined |
表示するテンプレート。null なら何も表示しない |
ngTemplateOutletContext |
C | null | undefined |
テンプレートに渡す値。$implicit が既定の値 |
ngTemplateOutletInjector |
Injector | 'outlet' | null | undefined |
テンプレートの中で使うインジェクター。'outlet' で表示した場所のものを使う |
imports に入れ忘れたとき
NgTemplateOutlet を imports から外してビルドすると、書き方によって結果が違いました。
| 書き方 | 結果 |
|---|---|
*ngTemplateOutlet="..." |
警告だけでビルドは通る(NG8116: A structural directive ngTemplateOutlet was used in the template without a corresponding import)。何も表示されない |
[ngTemplateOutlet]="..." |
エラー(NG8002: Can't bind to 'ngTemplateOutlet' since it isn't a known property of 'ng-container'.) |
* の書き方では警告を見落とすと「何も表示されない」だけになるので、ビルドの NG8116 に気を付けます。
部品にテンプレートを渡す:一覧コンポーネントの完成コード
ngTemplateOutlet が一番役に立つのは、「行の見た目だけを呼び出し側で決められる一覧部品」を作るときです。 一覧の部品が @for で回し、1行ごとに呼び出し側のテンプレートへ値(context)を渡します。
一覧の部品(item-list.ts)
import { Component, TemplateRef, contentChild, input } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { ItemContext, ItemTemplate } from './item-template';
@Component({
selector: 'app-item-list',
imports: [NgTemplateOutlet],
template: `
<ul>
@for (item of items(); track $index) {
<li>
<ng-container
[ngTemplateOutlet]="itemTpl() ?? defaultItem"
[ngTemplateOutletContext]="{ $implicit: item, index: $index }" />
</li>
} @empty {
<li class="empty">データがありません</li>
}
</ul>
<!-- 呼び出し側がテンプレートを渡さなかったときの表示 -->
<ng-template #defaultItem let-item>{{ item }}</ng-template>
`,
})
export class ItemList<T> {
items = input.required<readonly T[]>();
/** 呼び出し側の <ng-template [appItem]> を受け取る */
itemTpl = contentChild(ItemTemplate, { read: TemplateRef<ItemContext<T>> });
}
context に型を付けるディレクティブ(item-template.ts)
import { Directive, input } from '@angular/core';
/** テンプレートに渡す context の型 */
export interface ItemContext<T> {
$implicit: T; // let-user で受け取る値
index: number; // let-i="index" で受け取る値
}
/** <ng-template [appItem]="users" let-user> の let-user に型を付ける */
@Directive({ selector: 'ng-template[appItem]' })
export class ItemTemplate<T> {
/** 型を推論するためだけに配列を受け取る */
appItem = input.required<readonly T[]>();
static ngTemplateContextGuard<T>(
_dir: ItemTemplate<T>,
ctx: unknown,
): ctx is ItemContext<T> {
return true;
}
}
呼び出し側
import { Component } from '@angular/core';
import { ItemList } from './item-list';
import { ItemTemplate } from './item-template';
interface User { id: number; name: string; role: string; }
@Component({
selector: 'app-users',
imports: [ItemList, ItemTemplate],
template: `
<!-- 行の見た目を渡す -->
<app-item-list [items]="users">
<ng-template [appItem]="users" let-user let-i="index">
{{ i + 1 }}. <b>{{ user.name }}</b>({{ user.role }})
</ng-template>
</app-item-list>
<!-- 渡さない:既定の表示 -->
<app-item-list [items]="tags" />
<!-- 空の配列 -->
<app-item-list [items]="[]" />
`,
})
export class Users {
users: User[] = [
{ id: 1, name: '佐藤', role: '管理者' },
{ id: 2, name: '鈴木', role: '一般' },
];
tags = ['Angular', 'TypeScript'];
}
表示された文字は次のとおりです。
1. 佐藤(管理者)
2. 鈴木(一般)
Angular
TypeScript
データがありません
テンプレートを渡したときは渡した見た目、渡さないときは ?? の右の既定テンプレートで表示されました。contentChild の使い方は、関連記事「Angular viewChildの使い方|DOM取得とcontentChild」で扱っています。
let- の変数に型を付ける:ngTemplateContextGuard
<ng-template let-user> の user は、何もしないと型が any になります。 上の呼び出し側で [appItem]="users" を外し、user.name を user.nmae(誤字)にしてビルドすると、エラーにならずビルドが通りました。
ItemTemplate ディレクティブを付けた状態で同じ誤字にすると、ビルドがエラーで止まりました。
✘ [ERROR] TS2339: Property 'nmae' does not exist on type 'User'. [plugin angular-compiler]
src/app/s163/demo.ts:25:34:
25 │ {{ i + 1 }}. <b>{{ user.nmae }}</b>({{ user.role }})
╵ ~~~~
しくみは次のとおりです。
| 部分 | 役割 |
|---|---|
selector: 'ng-template[appItem]' |
<ng-template appItem> にだけ付くディレクティブにする |
appItem = input.required<readonly T[]>() |
渡された配列から T(ここでは User)を推論させる |
static ngTemplateContextGuard(...) |
「この ng-template の context は ItemContext<T> だ」とコンパイラーに伝える |
Angular のドキュメントでは、ngTemplateContextGuard の本体は実行時には使われず、TypeScript のエラーを避けるためだけに return true を書く、と説明されています。型のチェックは、Angular 22 の ng new が作る設定(strict モード)で有効でした。
条件でテンプレートを切り替える
ngTemplateOutlet には式を書けるので、表示形式の切り替えにも使えます。
<ng-template #cardView let-u><div class="card">{{ u.name }}</div></ng-template>
<ng-template #rowView let-u><span class="row">{{ u.name }} / {{ u.role }}</span></ng-template>
<button (click)="asCard.set(!asCard())">表示切り替え</button>
@for (u of users; track u.id) {
<ng-container *ngTemplateOutlet="asCard() ? cardView : rowView; context: { $implicit: u }" />
}
ボタンを押す前と後の DOM です(asCard = signal(true))。
<!-- 押す前 -->
<div class="card">佐藤</div><div class="card">鈴木</div>
<!-- 押した後 -->
<span class="row">佐藤 / 管理者</span><span class="row">鈴木 / 一般</span>
ng-template・ng-container・ng-content の違い
| 要素 | 何をするか | DOM に残るか | 主な使い道 |
|---|---|---|---|
<ng-template> |
テンプレートを定義する。書いただけでは表示しない | 残らない | ngTemplateOutlet で表示、部品へ渡す |
<ng-container> |
要素を増やさずにまとめる・ディレクティブを付ける | 残らない(コメントになる) | *ngTemplateOutlet を付ける場所 |
<ng-content> |
親が子タグの間に書いた内容を、そのまま差し込む | 差し込んだ内容が残る | カード・パネルなどの枠の部品 |
見た目の違いは小さくても、「いつ作られるか」が違います。 「開く」ボタンで中身を見せるパネルを、ng-content 版と ng-template 版で作り、中の部品の ngOnInit が呼ばれた回数を記録しました。
/** ng-content 版 */
@Component({
selector: 'app-panel-content',
template: `@if (open()) { <div class="body"><ng-content /></div> }`,
})
export class PanelContent {
open = input(false);
}
/** ng-template 版 */
@Component({
selector: 'app-panel-template',
imports: [NgTemplateOutlet],
template: `@if (open()) { <div class="body"><ng-container [ngTemplateOutlet]="body()" /></div> }`,
})
export class PanelTemplate {
open = input(false);
body = contentChild.required(TemplateRef);
}
<app-panel-content [open]="open()"><app-heavy name="ng-content" /></app-panel-content>
<app-panel-template [open]="open()">
<ng-template><app-heavy name="ng-template" /></ng-template>
</app-panel-template>
| 操作 | ng-content 版の中身 | ng-template 版の中身 |
|---|---|---|
| 最初(閉じている) | 作られた(1回) | 作られない(0回) |
| 開く | 1回のまま(作り直さない) | 作られた(1回) |
| 閉じる → 開く | 1回のまま | 作り直された(2回) |
| もう一度 閉じる → 開く | 1回のまま | 作り直された(3回) |
ng-content の中身は、@if で隠していても最初に作られました。 Angular のドキュメントにも「<ng-content> を @if・@for・@switch で条件付きにしないこと。Angular は ng-content に差し込む内容を、隠れていても必ず作る」とあり、条件付きで表示するなら ng-template を使うよう案内されています。
| 中身の性質 | 向いている方 |
|---|---|
| 重い部品・開くまで API を呼びたくない部品 | ng-template(開いたときに作る) |
| 開閉しても入力の状態を残したい部品 | ng-content(作り直さない) |
ng-content の select での差し込み分けは、関連記事「Angular ng-contentの使い方|selectと投影の基本」で扱っています。
よくある質問
Q. <ng-template> の中身が表示されません。
A. ng-template は書いただけでは表示されない決まりです。ngTemplateOutlet で表示するか、@if などの中で使います。ViewContainerRef.createEmbeddedView() を使って、コードから表示する方法もあります。
Q. *ngIf の else で使っていた <ng-template #elseBlock> はどうなりますか。
A. @if の構文では @else { } を直接書けるので、else のためだけの ng-template は使わずに済みます。関連記事「Angular @if・@for・@switchの使い方|ngIfとの対応表」で扱っています。
Q. テンプレートを input で受け取ってもいいですか。
A. tpl = input<TemplateRef<ItemContext<T>>>() と書いて [tpl]="myTemplate" で渡す方法も使えます。この記事の contentChild の方法は、呼び出し側がタグの間に書ける点が違います。input の書き方は、関連記事「Angular input・output・modelの使い方|親子の受け渡し」で扱っています。
Q. context を変えたら表示は更新されますか。 A. この記事の切り替えの例では、signal の値が変わると、使うテンプレートと表示が切り替わりました。ディレクティブの作り方全般は、関連記事「Angularディレクティブの作り方|属性ディレクティブとhost」で扱っています。
まとめ
<ng-template>は定義だけ。ngTemplateOutletで表示し、context をlet-で受け取る$implicitはlet-名前、名前付きのキーはlet-変数="キー"- 部品にテンプレートを渡すと、行の見た目を呼び出し側で決める一覧が作れる
let-の変数は何もしないとany。ngTemplateContextGuardで型を付けると誤字がビルドエラーになった- ng-content の中身は
@ifで隠しても作られた。開くまで作りたくないなら ng-template
参考・出典
確認日はいずれも 2026年10月13日です。
- Angular「NgTemplateOutlet」:https://angular.dev/api/common/NgTemplateOutlet
- Angular「Create template fragments with ng-template」:https://angular.dev/guide/templates/ng-template
- Angular「Content projection with ng-content」:https://angular.dev/guide/components/content-projection
- Angular「Structural directives」(Typing the directive's context):https://angular.dev/guide/directives/structural-directives
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angular ng-template・ngTemplateOutletの使い方」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。