Angularが提供しているコアAPIにViewChild
、ViewChildren
デコレーターがありますが、これととても似た名前/機能を持つContentChild
、ContentChildren
というデコレーターも存在しています。これらの違いに関してなるべく詳細にまとめました。
なお、ViewChild
、ViewChildren
に関する解説はこちらの記事にて解説しています。
目次
ContentChild
とは
ContentChild
はViewChild
と同様、デコレーターの引数に渡したセレクターと一致するDOMの最初の要素またはディレクティブを取得するためのAPIです。セレクターと一致したDOMが見つかった場合、デコレーターを設定したプロパティが更新されます。 ライフサイクルフックのngAfterContentInit
コールバック以降で取得されたDOMにアクセスすることが出来ます。
ViewChild
とContentChild
の違い
ViewChild
とContentChild
の違いを理解するためには、まず初めにLight DOM
とShadow DOM
を理解する必要があります。
Shadow DOM
とは
Shadow DOM
とはコンポーネント内部に隠蔽されたDOMの事です。コンポーネント作成者自身がテンプレートに直接配置したDOMのことを指します。
そのコンポーネント内のDOMはエンドユーザー(利用者)からは隠蔽されます。
例を見てみましょう。
@Component({
selector: 'shadow-dom-example-component',
template: `
<h1>これはShadow DOMです。</h1>
<h2>このように、テンプレートに直接定義されたコンポーネント内部のDOMはShadow DOMと呼ばれます。</h2>
<ng-content></ng-content>
`;
})
class ShadowDOMExampleComponent { /* ... */ }
Light DOM
とは
Light DOM
とはコンポーネントのエンドユーザー(利用者)がそのコンポーネントに供給するDOMのことを指します。
例を見てみましょう。
@Component({
selector: 'light-dom-example-component',
directives: [ShadowDOMExampleComponent],
template: `
<shadow-dom-example-component>
<h1>これはLight DOMです。</h1>
<h2>これらはLight DOMと呼ばれ、ShadowDOMExampleComponent内部の<ng-content></ng-content>内へ供給されます。</h2>
</shadow-dom-example-component>
`
})
class LightDOMExampleComponent { /* ... */ }
まとめ
これらを踏まえると、ViewChild
とContentChild
の違いはとてもシンプルです。
ContentChild
の使い方
では実際に例を見てみましょう。まずはindex.html
(エントリーポイント)です。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Angular Playground</title>
<base href="/">
</head>
<body>
<app-root></app-root>
</body>
</html>
例として3つのテスト用コンポーネントCmpA
, CmpB
, CmpC
を準備します。
CmpA
@Component({
selector: 'cmp-a',
template: `
<div>
<span>Component A</span>
</div>
`
})
export class CmpA
{
}
CmpB
@Component({
selector: 'cmp-b',
template: `
<div>
<span>Component B</span>
</div>
`,
})
export class CmpB
{
}
CmpC
@Component({
selector: 'cmp-c',
template: `
<div>
<span>Component C</span>
<ng-content></ng-content>
</div>
`,
})
export class CmpC implements AfterViewInit
{
@ContentChild('pInCmpC') public p: HTMLParagraphElement;
@ContentChild('cmpAInCmpC') public cmpA: CmpA;
@ContentChildren('cmpBInCmpC') public cmpBList: QueryList<CmpB>;
public ngAfterViewInit(): void
{
console.log(this.p); // > ElementRef {nativeElement: p}
console.log(this.cmpA); // > CmpA {}
console.log(this.cmpBList); // > QueryList {dirty: false, _results: Array(10), changes: EventEmitter, length: 10, last: CmpB, …}
}
}
AppComponent
次に<app-root>
コンポーネントの実装です。
@Component({
selector: 'app-root',
template: `
<div>
<!-- CmpA(単体) -->
<cmp-a #cmpA></cmp-a>
<!-- CmpB(複数) -->
<div *ngFor="let _ of repeat(10)">
<cmp-b #cmpB></cmp-b>
</div>
<!-- CmpC単体(子要素あり) -->
<cmp-c>
<!-- p要素 -->
<p #pInCmpC>Hello CmpC, I'm from CmpA.</p>
<!-- CmpC内部のCmpA(単体) -->
<cmp-a #cmpAInCmpC></cmp-a>
<!-- CmpC内部のCmpB(複数) -->
<div *ngFor="let _ of repeat(10)">
<cmp-b #cmpBInCmpC></cmp-b>
</div>
</cmp-c>
</div>
`,
})
export class AppComponent implements AfterViewInit
{
@ViewChild('cmpA') public cmpA: CmpA;
@ViewChildren('cmpB') public cmpBList: QueryList<CmpB>;
public ngAfterViewInit(): void
{
console.log(this.cmpA); // > CmpA {}
console.log(this.cmpBList); // > QueryList {dirty: false, _results: Array(10), changes: EventEmitter, length: 10, last: CmpB, …}
}
public repeat(count: number): number[]
{
return Array.from(Array(count).keys());
}
}
AppComponent
のテンプレート内にある#cmpA
や#cmpB
のように、コンポーネントのテンプレートに直接定義された要素(Shadow DOM)を取得する際はViewChild
、もしくはViewChildren
を用います。
一方CmpC
内に存在している<ng-content></ng-content>
は内部のコンテンツが親から注入されるため(Light DOM)、その内容を取得する際はContentChild
、もしくはContentChildren
を用います。
※Light DOMをViewChild
、ViewChildren
で取得しようとしても出来ません。
console.log()
の結果をコメントアウトにて記載しておいたので参考にしてみて下さい。
ContentChildren
に関して
こちらは前項の例中にもある通り、デコレーターの引数に渡したセレクターにマッチする要素が複数存在する場合に利用します。
まとめ
ViewChild
、ContentChild
の違いを一言でまとめると、Shadow DOM
を取得するのがViewChild
、Light DOM
を取得するのがContentChild
、ということとなります。
どうしても直感的に理解できない場合は、<ng-content></ng-content>
内のDOMを取得したい場合にはContentChild
/ContentChildren
、それ以外はViewChild
/ViewChildren
、という理解でも問題ないかと思います(`・ω・´)ゞ