- Angular
Angular viewChildの使い方|DOM取得とcontentChild
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つです。
- テンプレートの要素に
#名前を付ける(テンプレート参照変数)。 - クラスのフィールドで
viewChild('名前')を呼ぶ。型はviewChild<ElementRef<HTMLInputElement>>('名前')のように指定する。 - 使うときは関数として呼び出し、
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期連続の黒字決算。