きなこもち.net

.NET Framework × UiPath,Orchestrator × Azure × AWS × Angularなどの忘備録

Angular × ディレクティブとコンポーネントの親子間でのデータ共有 × ClassとInterfaceとStringを使った挙動の比較

Purpose

ディレクティブとコンポーネントの親子間でのデータ共有を参考に、親子間でのデータの共有方法を考える。特に、双方向でデータをやり取りする場合についてまとめる。

Motivation

上記サイトや、他の数多のブログで紹介されているような”String型”の変数を渡すというシナリオも理解したい。それ以上に、次のようなシナリオでどのように実装することができるかをまとめたい。
1)複数のプロパティが実装されたPOCOのインスタンスを親から子に渡す。
2)子画面で親から渡されたデータを使って、何かしらの操作を行い、必要に応じてPOCOを更新する。
3)子画面での更新情報を親画面へ共有し、親画面でそのほかの処理を実行する。

SampleCode

以下の説明で利用するコードとLiveDemoは以下のStackblutzに保存した。 stackblitz.com

String型の変数の共有

Overview

ディレクティブとコンポーネントの親子間でのデータ共有で紹介されている。
子画面でやること・・・@input/output修飾子を持ったフィールド変数をComponentで定義する。outputのほうでは、子画面で値が変更されたタイミングや、任意のタイミングで親に変更を伝えるためのEventEmit型の変数を用意する。
親画面でやること・・・String型の変数を定義する。html上で子画面のディレクティブを呼び出す。その際、角かっこを使い、子画面のInput変数に用意した変数をバインドする。また、子画面のOutputで用意したイベントを受け取り、イベントから送られてくる変数を自身の変数に反映させるためのメソッドを用意し、それを設定する。

コンポーネントの実装

まずは、子画面のコンポーネントに、Inputを定義する。

export class InputComponent implements OnInit {
  @Input() argument1: string;
  ngOnInit() {}
}

HTMLは特に必要なものはない。値が受け取れていることを確認するための表示領域を設定する。

<div>
  <h2>data1: <{{argument1}}></h2>
  <input [(ngModel)]="argument1" (change)="change($event)" />
</div>

Inputだけ定義して、親から呼び出す(Stringをバインド)

※data1 は。string型のフィールド変数。 TSファイル

export class AppComponent {
  data1: string = "SampleData";
}

HTMLファイル

<app-input
  [argument1]="data1"
></app-input>

この時、子コンポーネントで定義したInputに情報を入力する①と、h2のインナーテキストには、その変更が反映される②。これは、argument1変数と双方向バインディングしているため。
一方、親で定義されているh2ディレクティブのインナーテキストには、変更が反映されない③。これは、子コンポーネントの情報を親に反映させるためのOutputを実装していないため。 f:id:kinakomotitti:20210328164323p:plain

親からの呼び出し方法(実行時エラー)

TSファイル

<app-input (argument1)="data1" ></app-input>

適切な使い方ではないが、()を使った場合も試した。この場合、ビルドエラーなどにはならないが、親からのデータを受け取れないという挙動になった。

親からの呼び出し方法(ビルドエラー)

banana-in-a-box 構文 [()]を使うと、簡単に双方向バインドが実装できるそうなので、inputしか定義していないが、子コンポーネントの呼び出しに対して使ってみた。この場合、以下のビルドエラーが出て実行できなくなる。

"Type 'Event' is not assignable to type 'string'."

本来の使い方としては、<Inputの名前>Change という命名規約で定義されたOutput変数を用意したのちに利用するものである。上記のエラーはその定義が足りていないよ、と教えてくれるエラーであるはず。

Outputを定義して、親から呼び出す

子画面のコンポーネントに、Outputを追加する。また、ここでは、arguamnet1という変数が更新された場合に親画面に変更を伝えるためのメソッドをあわせて定義する。

@Output() argument1Change = new EventEmitter<string>();
change(value: any) {
  console.log(value);
  this.argument1Change.emit(this.argument1);
}

htmlファイルで、Inputディレクティブを用意し、何か値が変更されたときに、上記Changeメソッドを実行できるようにChangeイベントを追加する。

<div>
  <h2>data1: <{{argument1}}></h2>
  <input [(ngModel)]="argument1" (change)="change($event)" />
</div>

これで、子画面に入力した文字を親画面に伝えることができるようになる。また、banana-in-a-box構文を使っても、ビルドエラーが表示されなくなり、親画面でのバインド処理を実装することなく変数をバインドすることができるようになる。 f:id:kinakomotitti:20210328164340p:plain

POCOの共有

バインドする変数の型をStringから、以下の定義のClassとInterfceにして試した。Sample Code内ではそれぞれ、data2, data3 という名前で定義している。

export class SampleDataModel {
  public data1: string;
  public data2: number;
}

export interface SampleDataInterface {
  data1: string;
  data2: number;
}

Inputだけの場合

実装は、Stringの時と似ているため、割愛。挙動としては、Stringの時とは違い、Classをバインドした場合、Output定義をしていなくても、変更が親に伝わり、双方向のバインドができているように見える。
また、banana-in-a-box構文を使った場合、ビルドエラーになるところは同じであった。

Outputを追加した場合

ほぼ、Stringの時と同じ。

まとめ

  • ディレクティブとコンポーネントの親子間でのデータ共有には、input/outpuの設定が必要。実装の手間を考えると、banana-in-a-box構文を使ったほうが楽。(親画面での受信設定を実装しなくて済むので)ただし、親画面に変えてきたときに、何か処理をはさみたいなどの要件がある場合は、必要になるはず。
  • ClassやInterfaceなどは、Outputなしでも双方向バインドが成り立ってしまう。これが混乱のもとだった。Outputはなくても双方向バインドが実現できるが、可読性や、想定外の挙動をさせないためにもOutputはあったほうが良いのかもしれない。