Dev Study
← 解説「contentChild() — 差し込まれた中身をクラスから掴む」に戻る

サンプルコードで身につける: contentChild() — 差し込まれた中身をクラスから掴む

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

1view と content の区別を1つの部品で見る

同じコンポーネントの中でも、自分のtemplateに書いたものはview、タグの間に差し込まれたものはcontentです。取得に使う関数もviewChildとcontentChildで対になっています。

// ── card.component.ts ──
// template:
//   '<h2 #heading>カード</h2>   ← view(自分で書いた)
//    <ng-content />'            ← content の差し込み口
//
// ── 利用側 ──
// <app-card>
//   <app-badge />              ← content(差し込まれた)
// </app-card>
//
// ── card.component.ts のクラス側 ──
// heading = viewChild<ElementRef>('heading'); // viewから探す
// badge = contentChild(BadgeComponent);       // contentから探す
//
// viewChild で content は取れないし、その逆も取れない

2contentChild の基本形 — 差し込まれた部品を1つ取る

カードに差し込まれたタイトル部品を取得し、有無で表示を変える例です。差し込まれていない可能性があるので、戻り値はundefinedを含むシグナルになります。

import { Component, contentChild, computed } from '@angular/core';
import { CardTitleComponent } from './card-title.component';

@Component({
  selector: 'app-card',
  template:
    '@if (!hasTitle()) { <h2>無題のカード</h2> }' +
    '<ng-content />',
})
export class CardComponent {
  // 差し込まれた CardTitleComponent を探す(無ければ undefined)
  title = contentChild(CardTitleComponent);

  hasTitle = computed(() => this.title() !== undefined);
}

3contentChildren でタブを束ねる

タブUIの定番実装です。利用側が並べたパネルをcontentChildrenで集め、その数だけボタンを描画します。パネルが増減してもシグナルなので自動で追従します。

import { Component, contentChildren, signal } from '@angular/core';
import { TabPanelComponent } from './tab-panel.component';

@Component({
  selector: 'app-tabs',
  template:
    '@for (panel of panels(); track panel; let i = $index) {' +
    '  <button (click)="active.set(i)">{{ panel.title() }}</button>' +
    '}' +
    '<ng-content />',
})
export class TabsComponent {
  panels = contentChildren(TabPanelComponent);
  active = signal(0);
}

// 利用側:
// <app-tabs>
//   <app-tab-panel title="基本情報">...</app-tab-panel>
//   <app-tab-panel title="設定">...</app-tab-panel>
// </app-tabs>

4テンプレート参照変数の名前でも探せる

取りたい対象はコンポーネントの型だけでなく、#で付けたテンプレート参照変数の名前(文字列)でも指定できます。ただの要素を差し込んでもらう場合に便利です。

import { Component, contentChild, ElementRef, effect } from '@angular/core';

@Component({
  selector: 'app-form-field',
  template: '<label>ラベル</label><ng-content />',
})
export class FormFieldComponent {
  // 利用側が #input を付けた要素を探す
  input = contentChild<ElementRef<HTMLInputElement>>('input');

  constructor() {
    effect(() => {
      // 差し込まれた入力欄に自動でフォーカスする
      this.input()?.nativeElement.focus();
    });
  }
}

// 利用側:
// <app-form-field><input #input type="text" /></app-form-field>

5必須の差し込みは required で宣言する

「この部品は必ず差し込まれる前提」なら、contentChild.requiredを使います。戻り値の型からundefinedが消えて毎回のチェックが不要になり、差し込み忘れは実行時エラーとしてすぐ気づけます。input.required()と同じ考え方です。

import { Component, contentChild, computed } from '@angular/core';
import { StepLabelComponent } from './step-label.component';

@Component({
  selector: 'app-wizard-step',
  template: '<ng-content />',
})
export class WizardStepComponent {
  // 必ず差し込まれる前提。型は undefined を含まない
  label = contentChild.required(StepLabelComponent);

  // ?. なしでそのまま使える
  labelText = computed(() => this.label().text());
}

// 差し込み忘れると実行時にエラーで知らせてくれるので、
// 「なぜか動かない」ではなく「忘れている」とすぐ分かる