Dev Study
← 解説「ng-template と ng-container — 構造ディレクティブの正体」に戻る

サンプルコードで身につける: ng-template と ng-container — 構造ディレクティブの正体

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1ng-template は描画されない下書き

ng-templateの中身はDOMに出力されません。まずこの「書いたのに出ない」挙動を最小の例で確認します。誰かが実体化を要求して初めて画面に現れます。

<p>ここは表示される</p>

<ng-template>
  <p>ここは表示されない(下書きのまま)</p>
</ng-template>

<!-- 実際のDOM出力:
     <p>ここは表示される</p>
     <!—— ng-template はコメントの目印だけ残る ——>

     下書きが実体化されるのは、*ngIf の else 参照・
     ngTemplateOutlet・@defer など「誰かが要求したとき」だけ -->

2* 記法の展開を自分の目で確かめる

*ngIfと*ngForがng-templateへ展開される対応を並べます。「*は下書き化の合図」と分かれば、構造ディレクティブという言葉の意味も、1要素に1つの制約も自然に理解できます。

<!-- 書いたコード -->
<p *ngIf="visible">こんにちは</p>

<!-- Angularが解釈する形 -->
<ng-template [ngIf]="visible">
  <p>こんにちは</p>
</ng-template>

<!-- *ngFor も同じ(let-item で変数を受け取る形になる) -->
<ng-template ngFor let-item [ngForOf]="items">
  <li>{{ item }}</li>
</ng-template>

<!-- 1要素に * を2つ書けないのは、この展開が
     入れ子の ng-template になって曖昧になるため。
     旧コードで ng-container の入れ子が多いのはこれが理由 -->

3ng-container — タグを出さない箱の使い所

divで包むとflexやgridのレイアウトが壊れる場面が、ng-containerの定番の出番でした。DOM出力を比べると存在意義がはっきり分かります。

<!-- × div で包むと flex の直接の子が変わり、レイアウトが崩れる -->
<nav style="display: flex">
  <a>ホーム</a>
  <div *ngIf="loggedIn">
    <a>マイページ</a><a>ログアウト</a>
  </div>
</nav>

<!-- ○ ng-container ならタグが出ないので崩れない -->
<nav style="display: flex">
  <a>ホーム</a>
  <ng-container *ngIf="loggedIn">
    <a>マイページ</a><a>ログアウト</a>
  </ng-container>
</nav>
<!-- DOM出力: <nav> の直接の子は <a> が3つ並ぶだけ -->

4ngTemplateOutlet — 行の描画を利用側に差し込ませる

下書きを名前で受け取って任意の場所に実体化するのがngTemplateOutletです。「一覧の枠は共通、各行の見た目は使う側が決める」という柔軟な部品の定番実装で、これは現代のコードでも現役です。

// 一覧コンポーネント: 行の描画方法を外から受け取る
@Component({
  selector: 'app-list',
  template:
    '<div *ngFor="let item of items">' +
    '  <ng-container ' +
    '    *ngTemplateOutlet="rowTemplate; context: { $implicit: item }" />' +
    '</div>',
})
export class ListComponent {
  @Input() items: string[] = [];
  @ContentChild(TemplateRef) rowTemplate!: TemplateRef<unknown>;
}

// 利用側: 行の見た目を ng-template で差し込む
// <app-list [items]="fruits">
//   <ng-template let-item>
//     <strong>{{ item }}</strong> <button>削除</button>
//   </ng-template>
// </app-list>
// let-item は context の $implicit を受け取る書き方

5@if / @defer も同じ機構の上にいる

新しい@ブロック構文も、内部では「中身を下書き化して条件で実体化する」という同じ機構で動いています。新旧は見た目が違うだけで原理は地続きだと分かると、レガシーコードへの心理的な壁が下がります。

<!-- この3つは「中身を下書きにして、条件で実体化する」仲間 -->

<!-- 世代1: ng-template を手で書く -->
<ng-template [ngIf]="show"><p>本体</p></ng-template>

<!-- 世代2: * 糖衣構文(世代1に展開される) -->
<p *ngIf="show">本体</p>

<!-- 世代3: @ ブロック(コンパイラが直接同じ機構を生成) -->
<!-- @if (show) { <p>本体</p> } -->

<!-- @defer は「実体化のついでにJSの読み込みまで遅らせる」
     発展形。SSRのセクションで学んだ hydrate トリガーも
     この下書き機構の応用 -->