taketiyo.log

Web Engineering 🛠 & Body Building 💪

【Angular】@ViewChild / @ViewChildren を理解する【v6.x】

Programming

  / / / /

AngularのコアAPIであるViewChildViewChildrenデコレーターを自在に使いこなせるようになると、一段階レベルアップしたカスタムコンポーネントの作成が可能になります。

 

目次

 

ViewChildとは

ViewChildはクラス内プロパティに適用するタイプのデコレーターで、テンプレート側の要素をコンポーネント側から参照したい場合に利用します。

 

ViewChildの使い方 – 基本

コンポーネント内にて下記の様に使用します。

// my-component.ts

@Component({
  selector: 'my-component',
  templateUrl: './my-component.html',
})
export class MyComponent implements AfterViewInit
{
  // テンプレート内に存在する参照変数`elm`を、プロパティ`element`に紐付ける
  @ViewChild('elm') public element: HTMLInputElement;

  public ngAfterViewInit(): void
  {
    // this.elementにてビューDOM内のHTMLInputElementにアクセス出来ます
    console.log(this.element);
  }
}

 
上記MyComponentクラスのテンプレートは下記の様な感じになります。

// my-component.html

<div>
    <input #elm type="text" name="username">
</div>

 
#+変数名の記述によってテンプレート内に参照変数を宣言出来ます。この場合#elmという記述でelmという名前の参照変数を宣言しています。
ほとんどの場合Angularは参照変数の値を宣言された要素に設定するため、この場合elmにはHTMLInputElementが設定されます。
また、参照変数はref-+変数名でも宣言することが出来ます。(この場合、ref-elm
 
最後にコンポーネント側にてViewChildデコレーターを任意のクラス内プロパティに適用してあげれば、テンプレート内の要素と通信が出来るようになります。
その際ViewChildデコレーターには引数として参照変数名を渡してあげます。(※ @ViewChild('elm')

 

ViewChildの使い方 – よくある使い方1

おそらくViewChildの最も多いであろう使われ方として、NgFormディレクティブと併用する方法があります。
コンポーネント側における記述は前項の基本と大きくは変わりませんが、テンプレート側が下記の様になります。

// my-component.html

<div>
  <form #elm="ngForm" (ngSubmit)="submit()">
    <input type="text"
           name="username"
           ngModel>
    <button type="submit">送信</button>
  </form>
</div>

 
大きな違いは参照変数elm<form>要素対して設定されている点、またelmに対してngFormという値が設定されている点です。
参照変数に対して有効なディレクティブ名を指定すると、指定したディレクティブのインスタンスがバインドされるようになります。更にその要素に対して指定したディレクティブが適用されます。
 
つまり<form #elm="ngForm">によって<form>要素に対してNgFormディレクティブが適用され、更に適用後のNgFormディレクティブのインスタンスを参照変数elmを通じて参照出来るようになります。
 

参照変数に対して指定するディレクティブ名は、ディレクティブを定義する際に指定する`exportAs`の値となります。`@angular/forms`で定義されている`NgForm`ディレクティブは`exportAs`に`’ngForm’`が指定されているため、参照変数に指定する値は`ngForm`となります。※`NgForm`を指定するのは間違いです。
※参考:[Angular – NgForm](https://angular.io/api/forms/NgForm)

 
ちなみにコンポーネント側はこんな感じです。

// my-component.ts

@Component({
  selector: 'my-component',
  templateUrl: './my-component.html',
})
export class MyComponent implements AfterViewInit
{
  // テンプレート内に存在する参照変数`elm`を、プロパティ`element`に紐付ける
  @ViewChild('elm') public form: NgForm;

  public ngAfterViewInit(): void
  {
    // this.formを通じてNgFormのインスタンスを参照出来ます
    console.log(this.form);
  }

  public submit(): void
  {
    // this.formを通じてNgFormのインスタンスを参照出来ます
    console.log(this.form);
  }
}

 

ViewChildの使い方 – よくある使い方2

次は<form>要素の内容が動的に書き換わるケースです。
ユーザーからの操作に応じて、動的に入力要素が増減するようなフォームを考えます。
 
まずテンプレート側は下記のとおりです。

// my-form-group.html

<div>
  <form #f="ngForm" (ngSubmit)="submit()">
    <div *ngFor="let item of items">
      <my-input [f]="f" [item]="item"></my-input>
    </div>

    <button type="submit">送信</button>
    <button type="button" (click)="add()">追加</button>

  </form>
</div>

 
コンポーネント内のitemsの個数に応じて<my-input>が動的に増減するようになっています。
<my-input>に対してはNgFormのインスタンスと、itemを渡すようにします。
追加ボタンを押すことで<my-input>を追加、送信ボタンが実行された際にNgForm内の値をコンソールに出力するだけの簡単なコンポーネントを作ってみます。

// my-form-group.ts

@Component({
  selector: 'my-form-group',
  templateUrl: './my-form-group.html'
})
export class MyFormGroup
{
  @ViewChild('f') public f: NgForm;

  public items: object[] = [];

  public add(): void
  {
    this.items.push({ name: `input.${this.items.length}`, value: '' })
  }

  public submit(): void
  {
    console.log(this.f.value);
  }
}

 
<my-input>のテンプレート、及びコンポーネントは下記のとおりです。

// my-input.html

<div *ngIf="item">
  <input type="text"
         name="{{item.name}}"
         ngModel="{{item.value}}">
</div>
// my-input.ts

@Component({
  selector: 'my-input',
  templateUrl: './my-input.html'
})
export class MyInput
{
  @Input() public f: NgForm;

  @Input() public item: object;
}

 
この状態でMyFormGroupコンポーネントの追加ボタンをクリックし<my-input>を追加、追加されたinput要素にテキストを入力した上で送信ボタンを押します。
すると予想に反して、コンソールには空のオブジェクトが表示されますが、これは後から動的に追加されたNgModelNgFormが検知出来ていないために発生します。
 
そのため、MyInputコンポーネントを下記の様に拡張してあげる必要があります。

// my-input.ts

@Component({
  selector: 'my-input',
  templateUrl: './my-input.html'
})
export class MyInput
{
  // 追加
  @ViewChild(NgModel) set onModelDetected(m: NgModel) {
    if (m && !this.f.controls[this.item['name']]) {
      this.f.addControl(m);
    }
  }

  @Input() public f: NgForm;

  @Input() public item: object;
}

 
ViewChildデコレーターの引数に対して参照変数名ではなくクラス型を指定した場合、そのクラス型と一致する要素が検出された瞬間、プロパティに対してインスタンスがバインドされるようになります。
また上記例のようにプロパティではなくセッターとして宣言していた場合、一致する要素が検出された瞬間にそのセッターが呼ばれるようになります。
検出された要素のインスタンスはセッターの引数に渡されます。

指定するクラス型は`@Component`や`@Directive`と共に宣言されている必要があります。またセッターに対して`ViewChild`デコレーターを適用する場合のセッター名は任意の名称を設定可能です。

 
上記例ではViewChildデコレーターが対象の要素を検出し次第、NgFormに対して順次NgModelを登録しています。これによりMyFormGroupsubmit()にて正常に値を検出できるようになります。

以上がViewChildの代表的な使い方となります。

 

ViewChildrenとは

ViewChildrenはクラス内プロパティに適用するタイプのデコレーターで、ViewChildと同様、テンプレート側の要素をコンポーネント側から参照したい場合に利用します。
ViewChildrenという名の通り、検出したい要素がビューDOM内に複数存在する場合にはViewChildではなくViewChildrenを利用します。

 

ViewChildrenの使い方

ViewChildの例で出てきたmy-form-group.htmlを見てみます。

// my-form-group.html

<div>
  <form #f="ngForm" (ngSubmit)="submit()">
    <div *ngFor="let item of items">
      <my-input [f]="f" [item]="item"></my-input>
    </div>

    <button type="submit">送信</button>
    <button type="button" (click)="add()">追加</button>

  </form>
</div>

 
このテンプレートではitemsの個数に応じて<my-input>が増減しますが、ViewChildrenデコレーターを利用することでこの増減する要素全てを取得することが出来るようになります。
my-form-group.tsを下記のように修正します。

@Component({
  selector: 'my-form-group',
  templateUrl: './my-form-group.html'
})
export class MyFormGroup
{
  @ViewChild('f') public f: NgForm;

  // 追加
  @ViewChildren(MyInput) public myInputs: QueryList<MyInput>;

  public items: object[] = [];

  public add(): void
  {
    this.items.push({ name: `input.${this.items.length}`, value: '' })
  }

  public submit(): void
  {
    // 追加されたmy-input全て格納されています。検出されたmy-inputはQueryListインスタンスを通じて参照可能です。
    console.log(this.myInputs);
  }
}

ViewChildrenデコレーターを適用したプロパティにはQueryListがセットされます。QueryListはいわゆるコレクションクラスとなっており、検出された複数の要素をより効率的に参照できるようなAPIを提供しています。
とても便利なので、詳細な使い方はこちら(Angular – QueryList)を確認してみて下さい。

 

最後に

Angularのカスタムコンポーネント作成において@Input@Outputといった基本的なデコレーターに加えて@ViewChild@ViewChildrenを使いこなせるようになると、よりコンポーネント作成が捗るかと思います!是非理解して使いこなしましょう(`・∀・´)ゞ