はじめに
業務でボタンの複数回クリックされたときの対策を実装するときがありました。そこで「このコンポーネントを使ってボタンを実装すればそういった対策は内部で勝手にやってくれる」魔法のようなボタンコンポーネントを作ろうと思ったのですが、実現しようとしていた方法がアンチパターンらしく、断念…。
ただ無駄な時間を過ごしたか、と言われるとそうでもなく、これのおかげでAngularの@Input(), @Output()の概念を理解できました。そこで今回は自分の理解、記録も兼ねてこれらがどういったものなのかを共有できればと思います。
注意事項
今回使用する例ではAngular-materialを使用しています。
またapp.module.tsに宣言するモジュール等は省略させていただきます。
@Input(), @Output()がどういったものか
まずはじめにこれらがどういう役割を持つかを簡単に説明します。
まず@Input()は 親コンポーネントから子コンポーネントに値を引き渡す 役割を持ちます。
対して@Output()は 子コンポーネントから親コンポーネントにイベント(値)を引き渡す 役割を持ちます。
コードで理解してみる。
今回は簡単なボタンコンポーネントの例で説明します。
まずコンポーネントの親子関係ですが、ボタンコンポーネントを呼ぶ側(今回でいうapp.component)が親、ボタンコンポーネントが子になります。
Input()編
ボタンコンポーネント
import { Component, Input } from '@angular/core';
@Component({
  selector: 'app-amazing-button',
  templateUrl: './amazing-button.component.html',
  styleUrls: ['./amazing-button.component.css']
})
export class AmazingButtonComponent {
    @Input() buttonText: string = 'BUTTON';
    @Input() buttonColor: string = '';
    constructor() { }
}
<button mat-raised-button color="buttonColor">{{buttonText}}</button>
ボタンコンポーネントでは@Input() buttonTextと@Input() buttonColor宣言しました。それぞれボタンの名前、色を指定する値になります。
@Input()と宣言された変数は親コンポーネントからこのコンポーネントを呼ぶ際の 属性として使用する ことができ、その 属性に値を格納する ことで 親から子 への値の連携を図ります。
実際に親コンポーネントから子コンポーネントを呼んでみます。
まずは何も指定しません。
<app-amazing-button></app-amazing-button>
するとデフォルトではBUTTONを入れているのでこのように表示されました。

今度は属性としてbuttonTextを指定してみると…
<app-amazing-button buttonText="!!AmazingButton!!"></app-amazing-button>

こんな感じで親コンポーネントから指定された文字列が子コンポーネントに引き渡されました。
Amazing!
さらにさらに今度は色を渡してみます。(色の指定はこちらから)
<app-amazing-button buttonText="!!AmazingButton!!" buttonColor="accent"></app-amazing-button>
このように、@Input()は 親から子へ値を引き渡す 時に使用します。
@Output()編
引き続きボタンコンポーネントを使用していきます。
ボタンコンポーネント
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
  selector: 'app-amazing-button',
  templateUrl: './amazing-button.component.html',
  styleUrls: ['./amazing-button.component.css']
})
export class AmazingButtonComponent {
    @Input() buttonText: string = 'BUTTON';
    @Input() buttonColor: string = '';
    @Output() addOops: EventEmitter<string> = new EventEmitter(); // 追加
    constructor() { }
    /**
     * 追加
     */
    sendText() {
        this.addOops.emit('Oops!');
    }
}
<!--clickイベントを追加-->
<button mat-raised-button color="{{buttonColor}}" (click)="sendText()">{{buttonText}}</button>
今度は@Output() addOopsとクリックイベントであるsendText()を実装しました。
@Output()は宣言時、 EventEmitterをインスタンス化 します。
@Output()で宣言された変数はそれ自体が 親コンポーネントで使うことができるイベント となり、 そのイベントでemitされた値を親側で受け取る ことで 子から親 への値の連携を図ります。
実際の動きの説明に入る前に親側も実装しましょう。
<!--子コンポーネントのイベントを追加-->
<app-amazing-button buttonText="!!AmazingButton!!" buttonColor="accent" (addOops)="logMessage($event)"></app-amazing-button>
import { Component } from '@angular/core';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
    /**
     * 追加
     */
    logMessage(text: string) {
        console.log(`${text}, Hello Angular!`);
    }
}
親コンポーネント側では、子コンポーネント側で@Output()と宣言した変数をイベントとして使用します。
app.component.htmlの(addOops)=~という部分ですね。
このボタンがクリックされると、まず 子コンポーネントで宣言されたイベントが発火 します。
今回でいうとamazing-button.componentの(click)イベントがこれにあたるので
  sendText() {
      this.addOops.emit('Oops!');
  }
が実行されます。
ここで出てくる、 emit() という関数が呼ばれると、親で呼び出している箇所へ値の引き渡しを行います。
渡したい値を引数に入れます。
これで子コンポーネントから値が引き渡されました。これを親側でどう受け取るかというと、
(addOops)="logMessage($event)
このように、$event とすることで子コンポーネントからemitされた値を親側で受け取ることができます。
これで親側で実装した
  logMessage(text: string) {
      console.log(`${text}, Hello Angular!`);
  }
の引数であるtextの中に子コンポーネントから渡された値が入り、親側で利用できるようになります。
実際に実行してみると、
Oops!, Hello Angular!
とコンソールに表示されます。
ちなみに、@Output()におけるemit()では オブジェクトにしてデータを複数返却することもできます。
長くなりましたが、@Output()の流れをまとめると、
- 子コンポーネントのイベントが発火。
- 子コンポーネント内の処理の中でemit()される。
- 親側で$eventで受け取る。
となります。
このように、@Output()は 子から親へイベント(値)を引き渡す 時に使用します。
ちなみに
多用するであろう@Input(), @Output()ですが、 アンチパターンも存在します。
アンチパターンについてはこちら。
Angular2 アンチパターン集
【ionic】@InputでObjectをバインドするといろいろハマる【Angular】
オブジェクトを@Inputした
どうやら オブジェクトは参照渡し になるよう。思わぬ挙動を起こしかねない…。
自分はcallback関数を@Input()に渡したかったのですが、アンチパターンでした💦
一応やる方法もあるようですが…。
コールバック地獄にならないためにもやめた方がよさそうです。
Angular 2: Component communication with events vs callbacks
最後に
この@Input(), @Output()は非常に便利な機能なのでぜひ理解したいところですね。
正直Angularの公式サイトとか読んでて、

こんな感じだったので、わかりやすい他の方の記事に感謝…!圧倒的感謝…!
余談
Qiitaでアットマークを使うと、アカウントへのリンクになってしまうので地味にめんどくさかった。
参考リンク
Angularの@Outputを理解する
Angular2 アンチパターン集
【ionic】@InputでObjectをバインドするといろいろハマる【Angular】
オブジェクトを@Inputした
Angular 2: Component communication with events vs callbacks
※読んだ本も参考に…
Angularアプリケーションプログラミング

