Dev Study
← 解説「viewChild() — テンプレートの要素をクラスから取得する」に戻る

サンプルコードで身につける: viewChild() — テンプレートの要素をクラスから取得する

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

1ボタンから入力欄へフォーカスを移す

viewChild.required でテンプレートの #inputEl を取得し、クラス側のメソッドから focus() を呼ぶ基本形です。取得結果はシグナルなので、使うときは inputEl() と関数として呼んでから nativeElement に触ります。

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

@Component({
  selector: 'app-reply-form',
  template:
    '<button (click)="startReply()">返信する</button>' +
    '<textarea #inputEl placeholder="返信を入力"></textarea>',
})
export class ReplyFormComponent {
  // シグナルとして取得。読むときは inputEl() と呼ぶ
  inputEl = viewChild.required<ElementRef>('inputEl');

  startReply() {
    this.inputEl().nativeElement.focus();
  }
}

2子コンポーネントを型で取得する

viewChild にはコンポーネントのクラスも指定できます。参照変数を付けなくても型で子が見つかり、親のメソッドから子のpublicメソッドを呼べます。タイマーやカルーセルのような「操作の指示を受ける部品」でよく使う形です。

import { Component, viewChild, signal } from '@angular/core';

@Component({
  selector: 'app-timer',
  template: '<p>経過: {{ seconds() }} 秒</p>',
})
export class TimerComponent {
  seconds = signal(0);
  reset() { this.seconds.set(0); }
}

@Component({
  selector: 'app-workout',
  imports: [TimerComponent],
  template:
    '<app-timer />' +
    '<button (click)="restart()">リセット</button>',
})
export class WorkoutComponent {
  // クラスを指定すると、その型の子コンポーネントが手に入る
  timer = viewChild.required(TimerComponent);

  restart() {
    this.timer().reset(); // 子のpublicメソッドを呼べる
  }
}

3存在しないかもしれない要素は required なしで

@if の中にある要素は、条件次第でDOMに存在しません。required 付きで取得すると実行時エラーになるため、required なしの viewChild で受けて undefined を確認してから使います。「必ずある要素か」で使い分けるのがルールです。

import { Component, viewChild, signal, ElementRef } from '@angular/core';

@Component({
  selector: 'app-detail-panel',
  // #panel は isOpen() が true のときしか存在しない
  template:
    '<button (click)="isOpen.set(!isOpen())">開閉</button>' +
    '@if (isOpen()) {' +
    '  <div #panel>詳細情報</div>' +
    '}',
})
export class DetailPanelComponent {
  isOpen = signal(false);

  // required なしなら「ElementRef | undefined」として受け取れる
  panel = viewChild<ElementRef>('panel');

  logHeight() {
    const el = this.panel();
    if (el) {
      console.log('高さ:', el.nativeElement.offsetHeight);
    }
  }
}

4effect と組み合わせて出現に反応する

viewChild の結果はシグナルなので、effect の中で読めば「要素が現れたら実行する」処理が書けます。@if で後から現れるパネルに、表示された瞬間フォーカスを移す例です。ライフサイクルフックを使わず宣言的に書けるのが新APIの利点です。

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

@Component({
  selector: 'app-inline-editor',
  template:
    '<button (click)="editing.set(true)">編集する</button>' +
    '@if (editing()) {' +
    '  <input #editEl placeholder="新しい名前">' +
    '}',
})
export class InlineEditorComponent {
  editing = signal(false);
  editEl = viewChild<ElementRef>('editEl');

  constructor() {
    // 入力欄がDOMに現れたタイミングでフォーカスを移す
    effect(() => {
      this.editEl()?.nativeElement.focus();
    });
  }
}

5まずバインディングで書けないか考える

viewChild で classList を操作するのは、[class.xxx] バインディングで書ける典型的なやりすぎ例です。同じ結果ならバインディングの方が短く安全で、Angularが変更を追跡できます。DOM直接操作はフォーカスや測定など本当に必要な場面に絞りましょう。

import { Component, signal } from '@angular/core';

// ×: viewChild で classList.add / remove を自分で呼ぶ
//    (Angularの外でDOMを書き換えるので追跡できず、コードも長い)

// ○: 同じことは [class.xxx] バインディングで宣言的に書ける
@Component({
  selector: 'app-highlight-note',
  template:
    '<p [class.highlighted]="active()">大事なメモ</p>' +
    '<button (click)="active.set(!active())">強調を切り替え</button>',
  styles: ['.highlighted { background: yellow; }'],
})
export class HighlightNoteComponent {
  active = signal(false);
}