viewChild とは、コンポーネントが自分のテンプレートの中にある要素・子コンポーネント・ディレクティブを取得するための関数で、結果はシグナルとして返ります。 テンプレートの要素に #nameInput のようなテンプレート参照変数を付け、クラス側で viewChild('nameInput') と書くと、その要素を包んだ ElementRef を nameInput() で読めます。

この記事では、フォーカス・スクロール・子コンポーネントのメソッド呼び出しの3つをコピペ用の完成コードで示し、動かした結果を添えます。さらに、viewChild / viewChild.required / viewChildren / contentChild / contentChildren とデコレーター版の対応、@if の中で undefined になる例、required で出る NG0951 エラー、read オプションで取れる型、値が確定するタイミングを、実際のログで確かめてまとめました。

確認環境:Ubuntu 24.04 / Node.js 22.22.2 / Angular 21.2.25(@angular/cli 21.2.24、ng new の既定構成で zone.js なし)/ TypeScript 5.9.3 / Chromium 141(Playwright 1.56.0 で操作)。

viewChildの早見表

やりたいこと シグナル版(関数) 返る値 デコレーター版
自分のテンプレートから1つ取得 viewChild('ref') 見つからなければ undefined になるシグナル @ViewChild('ref')
必ずある要素を1つ取得 viewChild.required('ref') undefined を含まないシグナル(無ければ NG0951) 対応なし(! で代用)
自分のテンプレートから複数取得 viewChildren('ref') 配列のシグナル @ViewChildren('ref')(QueryList)
投影されたコンテンツから1つ取得 contentChild('ref') 見つからなければ undefined になるシグナル @ContentChild('ref')
投影コンテンツに必ずあるものを取得 contentChild.required('ref') undefined を含まないシグナル 対応なし
投影されたコンテンツから複数取得 contentChildren(Tab) 配列のシグナル @ContentChildren(Tab)(QueryList)
第1引数に渡すもの 取れるもの
文字列(テンプレート参照変数の名前) DOM 要素なら ElementRef、コンポーネントに付けた変数ならそのインスタンス、ng-template なら TemplateRef
コンポーネントやディレクティブのクラス そのインスタンス

新しく書くコードでは関数の viewChild を選び、必ず存在する要素には viewChild.required を使うと、undefined の判定が減って短く書けます。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular viewChildの使い方|DOM取得とcontentChild」

基本の書き方:テンプレート参照変数でDOMを取得する

手順は3つです。

  1. テンプレートの要素に #名前 を付ける(テンプレート参照変数)。
  2. クラスのフィールドで viewChild('名前') を呼ぶ。型は viewChild<ElementRef<HTMLInputElement>>('名前') のように指定する。
  3. 使うときは関数として呼び出し、nativeElement で実際の DOM 要素を取り出す。
import { Component, ElementRef, viewChild } from '@angular/core';

@Component({
  selector: 'app-search',
  template: `<input #keyword placeholder="検索" /><button id="clear" (click)="clear()">消す</button>`,
})
export class Search {
  keyword = viewChild.required<ElementRef<HTMLInputElement>>('keyword');

  clear() {
    this.keyword().nativeElement.value = '';
  }
}

入力欄に abc と入れてから「消す」を押すと、入力欄の値は空文字になりました。

コピペ用コード:フォーカス・スクロール・子のメソッド呼び出し

よく使う3つの操作を1つのコンポーネントにまとめました。

// src/app/vc.ts
import { Component, ElementRef, signal, viewChild, viewChildren } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `<p id="count">count: {{ count() }}</p>`,
})
export class Counter {
  count = signal(3);
  reset() {
    this.count.set(0);
    console.log('Counter.reset() が呼ばれました');
  }
}

@Component({
  selector: 'app-vc-basic',
  imports: [Counter],
  template: `
    <input #nameInput placeholder="名前" />
    <button id="focus" (click)="focusInput()">入力欄へフォーカス</button>

    <ul #list style="height: 100px; overflow: auto">
      @for (n of items; track n) {
        <li #row>行 {{ n }}</li>
      }
    </ul>
    <button id="scroll" (click)="scrollToLast()">最後の行へ</button>

    <app-counter />
    <button id="reset" (click)="resetCounter()">リセット</button>
  `,
})
export class VcBasic {
  items = Array.from({ length: 30 }, (_, i) => i + 1);

  // 1. DOM 要素(必ずあるので required)
  nameInput = viewChild.required<ElementRef<HTMLInputElement>>('nameInput');
  // 2. 繰り返しの中の要素をまとめて取得
  rows = viewChildren<ElementRef<HTMLLIElement>>('row');
  // 3. 子コンポーネントのインスタンス(クラスを渡す)
  counter = viewChild.required(Counter);

  focusInput() {
    this.nameInput().nativeElement.focus();
  }
  scrollToLast() {
    this.rows().at(-1)?.nativeElement.scrollIntoView({ block: 'nearest' });
  }
  resetCounter() {
    this.counter().reset();
  }
}

ブラウザーでボタンを順に押し、そのつど状態を調べた結果です。

操作 調べた値 結果
「入力欄へフォーカス」を押す document.activeElement.placeholder 名前
「最後の行へ」を押す前 リストの scrollTop 0
「最後の行へ」を押したあと リストの scrollTop 530
「リセット」を押す前 カウンターの表示 count: 3
「リセット」を押したあと カウンターの表示 count: 0(コンソールに「Counter.reset() が呼ばれました」)

ポイントは次の3つです。

  • DOM 要素は文字列(参照変数名)で、子コンポーネントはクラスで指定します。クラスで指定すると型引数を書かなくても型が付きます。
  • @for の中の要素に同じ参照変数を付けると、viewChildren で配列として取れます。
  • 子コンポーネントのメソッドを直接呼ぶ方法は、フォーカスやリセットのような「命令」に向いています。値の受け渡しは、入力(input)と出力(output)で書くほうが流れを追いやすくなります。

値が確定するタイミングをログで確認

「いつから読めるのか」を、シグナル版・デコレーター版・デコレーター版の static: true の3つで比べました。

import {
  Component, ElementRef, ViewChild, afterNextRender, effect, viewChild,
  AfterContentInit, AfterViewInit, OnInit,
} from '@angular/core';

const show = (v: unknown) => (v === undefined ? 'undefined' : '取得できた');

@Component({
  selector: 'app-vc-timing',
  template: `<div #box>箱</div>`,
})
export class VcTiming implements OnInit, AfterContentInit, AfterViewInit {
  box = viewChild<ElementRef<HTMLDivElement>>('box');
  @ViewChild('box') boxDecorator?: ElementRef<HTMLDivElement>;
  @ViewChild('box', { static: true }) boxStatic?: ElementRef<HTMLDivElement>;

  constructor() {
    this.report('constructor');
    effect(() => console.log('effect              : viewChild=' + show(this.box())));
    afterNextRender(() => this.report('afterNextRender'));
  }
  ngOnInit() { this.report('ngOnInit'); }
  ngAfterContentInit() { this.report('ngAfterContentInit'); }
  ngAfterViewInit() { this.report('ngAfterViewInit'); }

  private report(where: string) {
    console.log(where.padEnd(20) + ': viewChild=' + show(this.box()) +
      ' / @ViewChild=' + show(this.boxDecorator) +
      ' / static:true=' + show(this.boxStatic));
  }
}
constructor         : viewChild=undefined / @ViewChild=undefined / static:true=undefined
ngOnInit            : viewChild=取得できた / @ViewChild=undefined / static:true=取得できた
effect              : viewChild=取得できた
ngAfterContentInit  : viewChild=取得できた / @ViewChild=undefined / static:true=取得できた
ngAfterViewInit     : viewChild=取得できた / @ViewChild=取得できた / static:true=取得できた
afterNextRender     : viewChild=取得できた / @ViewChild=取得できた / static:true=取得できた
読む場所 viewChild(シグナル版) @ViewChild @ViewChild の static: true
constructor undefined undefined undefined
ngOnInit 取得できた undefined 取得できた
effect(初回) 取得できた ― ―
ngAfterContentInit 取得できた undefined 取得できた
ngAfterViewInit 取得できた 取得できた 取得できた
afterNextRender 取得できた 取得できた 取得できた

読み取れることは次のとおりです。

  • constructor ではどの書き方でも取得できません。 テンプレートがまだ作られていないためです。
  • 条件分岐や繰り返しの外にある要素は、シグナル版なら ngOnInit の時点で取得できました。デコレーター版で同じことをするには static: true が必要です。
  • デコレーター版(既定)は ngAfterViewInit から取得できます。Angular 日本語版ドキュメントのクエリのページにも、デコレーター版の結果は ngAfterViewInit(コンテンツクエリは ngAfterContentInit)以降に利用可能になると書かれています。
  • シグナル版は「いつ読めるか」を覚えなくても、effect や computed の中で読めば、値が入った時点で自動的に再実行されます。次の節でその例を示します。

DOM の大きさを測る、フォーカスを当てるなど、描画が終わっていることが前提の操作は afterNextRender の中で行うのが確実です。

@if の中の要素は undefined から始まる

@if の中にある要素は、条件が true になるまでテンプレートに存在しません。そのあいだ viewChild の結果は undefined です。

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

const show = (v: unknown) => (v === undefined ? 'undefined' : '取得できた');

@Component({
  selector: 'app-vc-if',
  template: `
    <button id="open" (click)="openPanel()">開く</button>
    <button id="open2" (click)="openAndFocus()">開いてフォーカス</button>
    @if (open()) {
      <input #panelInput placeholder="パネル内の入力欄" />
    }
  `,
})
export class VcIf {
  private injector = inject(Injector);
  open = signal(false);
  panelInput = viewChild<ElementRef<HTMLInputElement>>('panelInput');

  constructor() {
    effect(() => console.log('effect: panelInput=' + show(this.panelInput())));
  }

  // 開いた直後に読む(まだ undefined)
  openPanel() {
    this.open.set(true);
    console.log('set(true) の直後: panelInput=' + show(this.panelInput()));
  }

  // 描画を待ってからフォーカスする
  openAndFocus() {
    this.open.set(true);
    afterNextRender(() => {
      console.log('afterNextRender: panelInput=' + show(this.panelInput()));
      this.panelInput()?.nativeElement.focus();
    }, { injector: this.injector });
  }
}

「開く」を押したときの出力です。

effect: panelInput=undefined
set(true) の直後: panelInput=undefined
effect: panelInput=取得できた

1行目は初回表示時の effect です。条件を true にした直後の行では、まだ undefined でした。テンプレートの更新は、クリックの処理が終わったあとに行われるためです。そのあと effect が再実行され、取得できています。

「開いてフォーカス」を押したときの出力です(1行目は初回表示時)。

effect: panelInput=undefined
effect: panelInput=取得できた
afterNextRender: panelInput=取得できた

このあと document.activeElement.placeholder を調べると「パネル内の入力欄」で、フォーカスが移っていました。「表示を切り替えた直後にその要素を操作したい」ときは、afterNextRender の中で操作するか、effect の中で viewChild を読む書き方にします。クリックの処理の中から afterNextRender を呼ぶときは、例のように inject(Injector) で受け取った injector を第2引数に渡します。

undefined を考慮しないとビルドで止まる

required なしの viewChild は、型に undefined が含まれます。?. を付けずに書くと、ng build で次のエラーになります。

✘ [ERROR] TS2532: Object is possibly 'undefined'. [plugin angular-compiler]

    src/app/vc.ts:108:6:
      108 │       this.panelInput().nativeElement.focus();
          ╵       ~~~~~~~~~~~~~~~~~

直し方は2つです。

状況 書き方
条件によって存在しないことがある this.panelInput()?.nativeElement.focus() と ?. を付ける
必ず存在する viewChild.required(...) に変える

viewChild.required と NG0951 エラーの再現

viewChild.required は、型から undefined を外せる代わりに、値がない状態で読むと NG0951 エラーになります。2つの場面を再現しました。

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

const show = (v: unknown) => (v === undefined ? 'undefined' : '取得できた');

@Component({
  selector: 'app-vc-required',
  template: `
    <p #exists>ここにある要素</p>
    @if (false) { <p #missing>表示されない要素</p> }
  `,
})
export class VcRequired implements OnInit, AfterViewInit {
  exists = viewChild.required<ElementRef>('exists');
  missing = viewChild.required<ElementRef>('missing');

  constructor() { this.peek('constructor'); }
  ngOnInit() { this.peek('ngOnInit'); }
  private peek(where: string) {
    try {
      console.log(where + ': exists=' + show(this.exists()));
    } catch (e) {
      console.log(where + ': ' + (e as Error).message);
    }
  }
  ngAfterViewInit() {
    console.log('ngAfterViewInit: exists=' + show(this.exists()));
    try { this.missing(); } catch (e) { console.log('ngAfterViewInit: ' + (e as Error).message); }
  }
}
constructor: NG0951: Child query result is required but no value is available. Find more at https://v21.angular.dev/errors/NG0951
ngOnInit: exists=取得できた
ngAfterViewInit: exists=取得できた
ngAfterViewInit: NG0951: Child query result is required but no value is available. Find more at https://v21.angular.dev/errors/NG0951
NG0951 が出た場面 原因 直し方
constructor で読んだ 読むのが早い(テンプレートがまだない) ngOnInit 以降、effect、afterNextRender で読む
ngAfterViewInit で missing を読んだ 条件が false で、一致する要素がテンプレートにない required を外して undefined を扱う、または要素を条件の外に出す

Angular 公式のエラー解説(NG0951)でも、原因は「クエリ結果がまだ計算されていないうちにアクセスした」か「一致するノードがなかった」の2つと説明されています。

コンストラクターで直接読むとビルド時に教えてくれる

上の例では、メソッド(peek)を経由して読んだので実行時のエラーになりました。コンストラクターの中に this.exists() と直接書いた場合は、ng build の時点で次のエラーが出ました。

✘ [ERROR] NG8118: `exists` is a required `viewChild` and does not have a value in this context. [plugin angular-compiler]

    src/app/vc.ts:125:10:
      125 │     try { this.exists(); } catch (e) { console.log('constructor: ...
          ╵           ~~~~~~~~~~~

NG8118 はビルド時、NG0951 は実行時のメッセージで、どちらも「required のクエリを、値がない場所で読んでいる」ことを知らせています。

read オプションで取得する型を変える

同じ要素でも、read オプションで「何として取り出すか」を指定できます。コンポーネント・ディレクティブ・ng-template を置いたテンプレートで、取れる型を出力しました。

import {
  AfterViewInit, Component, Directive, ElementRef, TemplateRef, ViewContainerRef, viewChild,
} from '@angular/core';
// Counter は前の節と同じファイルに定義したものを使います

@Directive({ selector: '[appMark]' })
export class Mark {}

@Component({
  selector: 'app-vc-read',
  imports: [Counter, Mark],
  template: `
    <div #plain>div</div>
    <app-counter #comp appMark />
    <ng-template #tpl><p>テンプレート</p></ng-template>
  `,
})
export class VcRead implements AfterViewInit {
  plain = viewChild('plain');
  comp = viewChild('comp');
  compEl = viewChild('comp', { read: ElementRef });
  compDir = viewChild('comp', { read: Mark });
  compVcr = viewChild('comp', { read: ViewContainerRef });
  tpl = viewChild('tpl');
  tplVcr = viewChild('tpl', { read: ViewContainerRef });
  byType = viewChild(Counter);
  byTypeEl = viewChild(Counter, { read: ElementRef });

  ngAfterViewInit() {
    const kind = (v: unknown) =>
      v instanceof Counter ? 'Counter(コンポーネント)' :
      v instanceof Mark ? 'Mark(ディレクティブ)' :
      v instanceof ElementRef ? 'ElementRef <' + v.nativeElement.tagName + '>' :
      v instanceof TemplateRef ? 'TemplateRef' :
      v instanceof ViewContainerRef ? 'ViewContainerRef' : String(v);
    console.log("viewChild('plain')                         -> " + kind(this.plain()));
    console.log("viewChild('comp')                          -> " + kind(this.comp()));
    console.log("viewChild('comp', {read: ElementRef})      -> " + kind(this.compEl()));
    console.log("viewChild('comp', {read: Mark})            -> " + kind(this.compDir()));
    console.log("viewChild('comp', {read: ViewContainerRef})-> " + kind(this.compVcr()));
    console.log("viewChild('tpl')                           -> " + kind(this.tpl()));
    console.log("viewChild('tpl', {read: ViewContainerRef}) -> " + kind(this.tplVcr()));
    console.log("viewChild(Counter)                         -> " + kind(this.byType()));
    console.log("viewChild(Counter, {read: ElementRef})     -> " + kind(this.byTypeEl()));
  }
}
viewChild('plain')                         -> ElementRef <DIV>
viewChild('comp')                          -> Counter(コンポーネント)
viewChild('comp', {read: ElementRef})      -> ElementRef <APP-COUNTER>
viewChild('comp', {read: Mark})            -> Mark(ディレクティブ)
viewChild('comp', {read: ViewContainerRef})-> ViewContainerRef
viewChild('tpl')                           -> TemplateRef
viewChild('tpl', {read: ViewContainerRef}) -> ViewContainerRef
viewChild(Counter)                         -> Counter(コンポーネント)
viewChild(Counter, {read: ElementRef})     -> ElementRef <APP-COUNTER>
参照変数を付けた先 read なし read: ElementRef read: ディレクティブのクラス read: ViewContainerRef
ふつうの DOM 要素(div など) ElementRef ElementRef 付いていればそのインスタンス ViewContainerRef
コンポーネント コンポーネントのインスタンス ホスト要素の ElementRef 付いていればそのインスタンス ViewContainerRef
ng-template TemplateRef ― ― ViewContainerRef

「子コンポーネントのホスト要素(DOM)がほしい」ときは { read: ElementRef } を付ける、というのがいちばんよく使う形です。ViewContainerRef は、その位置にコンポーネントやテンプレートを動的に差し込むときに使います。

contentChild / contentChildren:投影されたコンテンツを取得する

viewChild が「自分のテンプレートに書いたもの」を対象にするのに対し、contentChild は「利用する側が自分のタグの内側に書いて渡してきたもの」(投影されたコンテンツ)を対象にします。タブ部品で確かめます。

import {
  AfterContentInit, AfterViewInit, Component, ElementRef, OnInit,
  contentChild, contentChildren, effect, input, signal, viewChild, viewChildren,
} from '@angular/core';

const show = (v: unknown) => (v === undefined ? 'undefined' : '取得できた');

@Component({
  selector: 'app-tab',
  template: `<ng-content />`,
})
export class Tab {
  title = input.required<string>();
}

@Component({
  selector: 'app-tabs',
  template: `
    <nav>
      @for (t of tabs(); track t) { <button>{{ t.title() }}</button> }
    </nav>
    <ng-content />
  `,
})
export class Tabs implements OnInit, AfterContentInit, AfterViewInit {
  tabs = contentChildren(Tab);
  allTabs = contentChildren(Tab, { descendants: true });
  first = contentChild(Tab);
  note = contentChild<ElementRef<HTMLElement>>('note');
  inView = viewChild(Tab);

  constructor() {
    console.log('constructor        : tabs=' + this.tabs().length);
    effect(() => console.log('effect             : tabs=' + this.tabs().map(t => t.title()).join(',')));
  }
  ngOnInit() { console.log('ngOnInit           : tabs=' + this.tabs().length); }
  ngAfterContentInit() {
    console.log('ngAfterContentInit : tabs=' + this.tabs().length +
      ' / descendants:true=' + this.allTabs().length +
      ' / contentChild(Tab)=' + this.first()?.title() +
      ' / note=' + this.note()?.nativeElement.textContent +
      ' / viewChild(Tab)=' + show(this.inView()));
  }
  ngAfterViewInit() { console.log('ngAfterViewInit    : tabs=' + this.tabs().length); }
}

@Component({
  selector: 'app-wrap',
  template: `<ng-content />`,
})
export class Wrap {}

@Component({
  selector: 'app-vc-content',
  imports: [Tabs, Tab, Wrap],
  template: `
    <button id="add" (click)="more.set(true)">タブを追加</button>
    <app-tabs>
      <app-tab title="概要">概要の本文</app-tab>
      <app-tab title="詳細">詳細の本文</app-tab>
      <div><app-tab title="div内">div の中</app-tab></div>
      <app-wrap><app-tab title="別部品内">別コンポーネントの中</app-tab></app-wrap>
      @if (more()) { <app-tab title="追加">追加の本文</app-tab> }
      <p #note>補足メモ</p>
    </app-tabs>
  `,
})
export class VcContent implements AfterViewInit {
  more = signal(false);
  tabsInMyView = viewChildren(Tab);
  ngAfterViewInit() { console.log('親の viewChildren(Tab) = ' + this.tabsInMyView().length); }
}

初回表示と、「タブを追加」を押したあとの出力です。

constructor        : tabs=0
ngOnInit           : tabs=2
effect             : tabs=概要,詳細
ngAfterContentInit : tabs=2 / descendants:true=4 / contentChild(Tab)=概要 / note=補足メモ / viewChild(Tab)=undefined
ngAfterViewInit    : tabs=2
親の viewChildren(Tab) = 4
--- 「タブを追加」を押す ---
effect             : tabs=概要,詳細,追加
確かめたこと 結果
contentChildren の既定の範囲 直下の子だけ(2件)。div や別コンポーネントの内側にあるタブは数えない
{ descendants: true } を付けたとき 内側にあるものも含めて 4件
contentChild(Tab) 最初に一致した「概要」
参照変数(#note)による contentChild 投影された p 要素を取得できた
Tabs の中から viewChild(Tab) undefined(タブは Tabs のテンプレートには書かれていない)
利用する側から viewChildren(Tab) 4件(タブは利用する側のテンプレートに書かれている)
@if でタブが増えたとき contentChildren のシグナルが更新され、effect が再実行された

「その要素はだれのテンプレートに書かれているか」で viewChild と contentChild が決まります。 同じタブが、Tabs から見ると「コンテンツ」、利用する側から見ると「ビュー」になっている点が、上の表の5行目と6行目です。なお、ドキュメントには「クエリはコンポーネントの境界を貫通することはありません」とあり、別のコンポーネントが自分のテンプレートの中に持っている要素までは取得できません。

デコレーター版との対応と書き換え

以前からのコードでは、デコレーター版がよく使われています。ドキュメントでもデコレーター版は引き続き案内されており、どちらも動きます。対応は次のとおりです。

デコレーター版 シグナル版 違い
@ViewChild('box') box?: ElementRef; box = viewChild<ElementRef>('box'); 読むときに box() と呼ぶ
@ViewChild('box') box!: ElementRef; box = viewChild.required<ElementRef>('box'); 無いときは NG0951 で知らせてくれる
@ViewChild('box', { static: true }) viewChild('box') シグナル版は static の指定なしで ngOnInit から読めた(前述のログ)
@ViewChild(Counter, { read: ElementRef }) viewChild(Counter, { read: ElementRef }) read は同じ
@ViewChildren('row') rows!: QueryList<ElementRef>; rows = viewChildren<ElementRef>('row'); QueryList ではなく配列。rows().length や rows().at(-1) が使える
@ContentChild(Tab) tab?: Tab; tab = contentChild(Tab); ―
@ContentChildren(Tab, { descendants: true }) contentChildren(Tab, { descendants: true }) ―
rows.changes.subscribe(...) effect(() => this.rows()) 変化の監視は effect や computed で書ける

デコレーター版の @ViewChildren・@ContentChild・@ContentChildren も同じ環境で動かし、QueryList の length で件数が取れること、@for の行を2件から3件に増やすと changes の購読に「件数=3」と通知が届くことを確かめています。

シグナル版のよいところは、結果が変わると、それを読んでいる computed・effect・テンプレートが自動で更新される点です。@if の例で、要素が現れた時点で effect が再実行されたのがその動きです。

つまずきやすい点と直し方

起きること 原因 直し方
constructor で undefined、または NG0951 / NG8118 テンプレートがまだ作られていない ngOnInit 以降・effect・afterNextRender で読む
@if を true にした直後に undefined 画面の更新はクリック処理のあと afterNextRender(injector を渡す)か effect で操作する
TS2532: Object is possibly 'undefined' required なしの結果には undefined が含まれる ?. を付けるか viewChild.required にする
子コンポーネントを指定したのに DOM 操作ができない 取れているのはコンポーネントのインスタンス { read: ElementRef } を付ける
viewChild で投影された要素が取れない 投影された要素は「コンテンツ」 contentChild を使う
contentChildren で入れ子の要素が数えられない 既定は直下の子だけ { descendants: true } を付ける
keyword.nativeElement と書くと TS2339: Property 'nativeElement' does not exist on type 'Signal…' シグナルは関数として呼ぶ keyword().nativeElement と () を付ける

よくある質問

Q. viewChild と @ViewChild はどちらを使えばよいですか? A. 新しく書くコードでは関数の viewChild が扱いやすいです。結果がシグナルなので effect や computed と組み合わせられ、required で undefined の判定も減らせます。デコレーター版も引き続き動くので、既存のコードは必要になったところから置き換えれば十分です。

Q. viewChild の結果はいつから読めますか? A. 条件分岐の外にある要素は、今回の確認では ngOnInit から読めました。constructor では読めません。@if の中の要素は、条件が true になって画面が更新されたあとに読めるようになります。

Q. NG0951 が出たらどこを見ればよいですか? A. 「読むのが早い」か「一致する要素がない」かのどちらかです。constructor で読んでいないか、参照変数の名前が合っているか、要素が @if の中に入っていないかを順に確認します。

Q. viewChild と contentChild の違いは何ですか? A. viewChild は自分のテンプレートに書いた要素、contentChild は利用する側がタグの内側に書いて渡してきた要素が対象です。タブやカードのように、中身を外から受け取る部品では contentChild を使います。

まとめ

  • viewChild は自分のテンプレートの要素・子コンポーネントをシグナルで取得する関数。#名前 を付けて viewChild('名前') と書く。
  • 必ずある要素は viewChild.required。値がない場所で読むと NG0951(コンストラクターに直接書くとビルド時に NG8118)。
  • constructor では取得できない。条件分岐の外の要素は ngOnInit から読めた。
  • @if の中の要素は、true にした直後はまだ undefined。afterNextRender か effect で操作する。
  • read オプションで ElementRef・ディレクティブ・ViewContainerRef を取り出せる。子コンポーネントの DOM は { read: ElementRef }。
  • 投影されたコンテンツは contentChild / contentChildren。既定は直下の子だけで、descendants: true で内側も対象になる。
  • デコレーター版とは1対1で対応し、QueryList の代わりに配列、changes の代わりに effect で書ける。

参考・出典

  • Angular 日本語版ドキュメント「クエリによるコンポーネントの子の参照」 https://angular.jp/guide/components/queries
  • Angular 公式ドキュメント NG0951 https://angular.dev/errors/NG0951

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular viewChildの使い方|DOM取得とcontentChild」

株式会社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で一緒に開発しながら、 成長を楽しみませんか?