taketiyo.log

Web Engineering 🛠 & Body Building 💪

【Angular】@ViewChild / @ContentChild の違い【v6.x】

Programming

  / / / /

Angularが提供しているコアAPIにViewChildViewChildrenデコレーターがありますが、これととても似た名前/機能を持つContentChildContentChildrenというデコレーターも存在しています。これらの違いに関してなるべく詳細にまとめました。
 
なお、ViewChildViewChildrenに関する解説はこちらの記事にて解説しています。

 

目次

 

ContentChildとは

ContentChildViewChildと同様、デコレーターの引数に渡したセレクターと一致するDOMの最初の要素またはディレクティブを取得するためのAPIです。セレクターと一致したDOMが見つかった場合、デコレーターを設定したプロパティが更新されます。 ライフサイクルフックのngAfterContentInitコールバック以降で取得されたDOMにアクセスすることが出来ます。

 

ViewChildContentChildの違い

ViewChildContentChildの違いを理解するためには、まず初めにLight DOMShadow 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 { /* ... */ }

 

まとめ

これらを踏まえると、ViewChildContentChildの違いはとてもシンプルです。

`ViewChild`は`Shadow DOM`を取得する際に利用し、`ContentChild`は`Light DOM`を取得する際に利用します。

 

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をViewChildViewChildrenで取得しようとしても出来ません。
 
console.log()の結果をコメントアウトにて記載しておいたので参考にしてみて下さい。

 

ContentChildrenに関して

こちらは前項の例中にもある通り、デコレーターの引数に渡したセレクターにマッチする要素が複数存在する場合に利用します。

 

まとめ

ViewChildContentChildの違いを一言でまとめると、Shadow DOMを取得するのがViewChildLight DOMを取得するのがContentChild、ということとなります。
どうしても直感的に理解できない場合は、<ng-content></ng-content>内のDOMを取得したい場合にはContentChild/ContentChildren、それ以外はViewChild/ViewChildren、という理解でも問題ないかと思います(`・ω・´)ゞ