<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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?