maesblog

Angularの@Outputを理解する

Anuglarの@Outputを使いこなしているでしょうか。@Outputが理解できれば、Angularの初級レベルは脱したと言えるくらい最初は理解しにくいものかと思います。私は実際にAngularを使い始めて1ヶ月くらい過ぎてから、やっと@Outputの使い方を理解しました。と言うのも私はもともとReactとReduxを使った開発をメインで行っていたことから、始めて関わったAngular案件でも@ngrx/storeを導入したため、それほど@Outputの必要性を感じていかなったし、いまいち使いどころがピンときていませんでした。しかし、実際に使ってみると、これがなかなか強力な役割を果たすということがやっとわかりました。今回はこのAnuglarの@Outputについて紹介したいと思います(昨年Qiitaで書いた記事の転載となります)。

はじめに – @Outputは何のために使うか

Angularでは、親コンポーネントから子コンポーネントに値を渡す場合は、まず親コンポーネント側でプロパティバインディング([])に値を渡し、子コンポーネント側で@Inputを通して受け取ります。@Outputはこの逆を行うものと考えてもらえれば良いでしょう。つまり、子コンポーネントから親コンポーネントに値を渡したい場合や、また子コンポーネントで発生したアクションを親コンポーネントに伝えたい場合に使います

@Outputを使うことで、アプリ全体で必要となる処理を親コンポーネントに集約できるようになり、子コンポーネント側には余計な処理を書かなくて済むようになります。使いこなせるようになると、手放せなくなる便利な機能です。

例えば、Reactであれば、親コンポーネント(classコンポーネント)で管理しているstateを更新するにはReact.ComponentsetStateメソッドを呼び出すことになっています。このsetStateを呼び出すメソッドを親コンポーネントで定義され、子コンポーネントはこれをpropsを通して受け取り、呼び出すことで親コンポーネントのstateの更新を可能にしています。 

Reactでは、以下のように親コンポーネントのメソッドを子コンポーネントで呼び出し、stateを更新します。

/** 親コンポーネント(classコンポーネント) */
class Parent extends React.Component {
  constructor(props) {
    super(props);
    /** 親が保持するアプリの状態(state) */
    this.state = {
      value: 'hoge',
    };
    this.sendValue = this.sendValue.bind(this);
  }
  /** stateを更新するためのメソッド */
  sendValue(value) {
    this.setState({
      value,
    });
  }
  render () {
    return (
      <div>
        { this.state.value }
        {/** sendValueと言う名前で子コンポーネントに渡す */}
        <Child sendValue={ this.sendValue } />
      </div>
    );
  }
}

/** 子コンポーネント(関数コンポーネント) */
const Child = (props) => {
  /** 親から受け取ったsendValueを呼び出すメソッド */
  const handleClick = () => {
    props.sendValue('fuga');
  }
  return (
    <button onClick={ handleClick }>send</button>
  ); 
};
Reactで子コンポーネントから親コンポーネントのstateを更新する場合

それ故にReactでは、親コンポーネントにロジックを集約し、子コンポーネントは状態を持たないただの関数でコンポーネントで構築することができるわけです。Angularの@Outputは、まさにこのReact的なデータフローの仕組みをAngularで実現させるものと言うこともできるでしょう。

Reactのこの辺りの仕組みは、以下の公式ドキュメントに詳しく書いてあります。

Angularの@Outputも、実際にコードを書いてみると何が良いかがよくわかるかと思います。これより以下に実例を元に説明していきます。(例ではしれっと@ngrx/storeを使っていますが、そこはあまり難しく考えないでください。)

@Outputを使った例

まず親コンポーネントとなるParentComponentに状態となるhogeState$を更新するためのhandleSendValueメソッドを定義しています。あえてReact的に言うと、このhandleSendValueメソッドを子コンポーネントとなるchild-component要素にsendValueとして渡しています。Angular的に言うと、カスタムイベントであるsendValueが子コンポーネントのアクション(emit)により発火した際にイベントハンドラのコールバック関数として定義したhandleSendValueメソッドが呼ばれるようになっています。

@Component({
  selector: 'parent-component',
  tenplate: `
    <div>{{ (hogeState$ | async)?.value }}</div>
    <!-- sendValueとしてhandleSendValueを子コンポーネントに渡す -->
    <child-component (sendValue)="handleSendValue($event)"></child-component>
  `,
})
export class ParentComponent {
  hogeState$ = Observable<HogeState>;

  constructor(
    private store: Store<State>,
  ) {
    this.hogeState$ = store.select('hogeState');
  }

  /** 状態(hogeState$)を更新するメソッド */
  handleSendValue($event) {
    this.store.dispatch(new HogeAction.sendValue($event));
  }
}
親コンポーネント(parent.component.ts)

以下は子コンポーネントのChildComponentです。ここもあえてReact的に言うと、親コンポーネントから渡されたsendValue@Output()を通して受け取ります。Angular的に言うと、sendValueをイベントを発生させるエミッターとして定義します。その際に@Output()でデコレートすることで、親コンポーネントに通知ができるようになります。

@Component({
  selector: 'child-component',
  template: `
    <button (click)="handleClick()"></button>
  `,
})
export class ChildComponent {
  // sendValueをカスタムイベントとして定義
  @Output() sendValue = new EventEmitter();

  handleClick() {
    // buttonがクリックされた時に、sendValueを発火し親コンポーネントに通知
    this.sendValue.emit({ value: 'hoge' });
  }
}
子コンポーネント(child.component.ts)

Auglarの仕組みとしては、@Output()でデコレートしたsendValueemitメソッドを呼び出すことで、親コンポーネントにイベントが発火したことを通知することができるようになります。その際に親コンポーネントに渡したい値をemitメソッドの引数に指定します。

通知を受け取った親コンポーネントは、イベントを受け取ったsendValueのコールバック関数として定義したhandleSendValueメソッドを呼び出します。またhandleSendValueメソッドの引数を通してemitメソッドに指定した値(ここでの例では{ value: 'hoge' })を受け取ります。

上記で紹介したReactの動きにもかなり似ていますね。このように@Output()を使うと、親コンポーネントに処理を集約しつつ、子から親へのデータの受け渡しができるようになります。

@Outputを使わなかった場合

ちなみに、@Outputを使わなかった場合の例は以下となります。親コンポーネントの状態hogeState$を更新するメソッドを子コンポーネント側に定義しています。

@Component({
  selector: 'parent-component',
  tenplate: `
    <div>{{ (hogeState$ | async)?.value }}</div>
    <child-component></child-component>
  `,
})
export class ParentComponent {
  hogeState$ = Observable<HogeState>;

  constructor(
    private store: Store<State>,
  ) {
    this.hogeState$ = store.select('hogeState');
  }
}
親コンポーネント(parent.component.ts)

子コンポーネント側で直接親コンポーネントの状態を更新するメソッドを呼び出しています。

@Component({
  selector: 'child-component',
  template: `
    <button (click)="handleClick()"></button>
  `,
})
export class ChildComponent {
  constructor(
    private store: Store<State>,
  ) { }

  // buttonがクリックされた時に、親の状態(hogeState$)を更新
  handleClick() {
    this.store.dispatch(new HogeAction.sendValue('hoge'));
  }
}
子コンポーネント(child.component.ts)

正直、例を挙げておいて言うのも何ですが、これくらい単純なものだとよく違いがわからないですかね。でも、アプリケーションの規模が大きくなると管理が大変になるのは、容易に想像がつくことでしょう。親コンポーネントに必要な処理をまとめておき、必要に応じて子コンポーネントからイベントを投げて、親コンポーネント側で処理を実行するようにするとアプリの構造もシンプルになり、同時にメンテナブルにもなります。

@Outputの使い方

ここから@Outputの使い方を紹介していきます。まず@Outputについては、デコレータであり、カスタムイベントを作るためのものとなります。詳細は以下の公式のドキュメントに詳しく書いてあります。

使い方としては、まずは、必要なモジュールを@angular/coreから読み込みます。@Outputを使う場合は、OutputEventEmitterが必要となります。

import { Component, Output, EventEmitter } from '@angular/core';

例えば、hogeEventとしてカスタムイベントを作ります。

@Output() hogeEvent = new EventEmitter();

作ったイベントのemit()メソッドを呼び出すとイベントが発火します。引数にはイベントハンドラーのコールバック関数に渡したい値を指定してもよいです。

this.hogeEvent.emit();

@Outputを定義しているコンポーネントを取り込む際にテンプレート上で、(click)などと同じようにイベントバインディングでイベントハンドリングすることで、hogeEvent.emit()が呼ばれた時に、handleHogeEvent($event)を呼び出すことができるようになります。emit($event)の引数で渡された値は、コールバック関数であるhandleHogeEvent($event)の引数$eventで受け取れます。

<child-component (hogeEvent)="handleHogeEvent($event)"><child-component>

@outputを使う場合は、ここで紹介したパターンさえ覚えておけば何とかなります。

@Outputのユニットテストについて

最後に@Outputのユニットテストについて軽く触れておきます。@Output関連のテストコードを書く場合は以下のようになるかと思います。上記の例のテストコード例をテストケースのみとなりますが、以下に紹介します。

実際に@Outputを定義している子コンポーネント側のテストコードは以下となります。ボタンが押された時にhandleClickが呼び出され、sendValueイベントが発火するかのテストとなっています。ポイントとしては、sendValueイベントの通知をsubscribeで受け取っているところでしょうか。

it('ボタンが押されたら、handleClickが呼び出されること', () => {
  const spy = spyOn(component, 'handleClick');
  const debugElement = fixture.debugElement.query(By.css('button'));
  debugElement.nativeElement.click();
  expect(spy).toHaveBeenCalled();
});

it('ボタンが押されたら、sendValueが発火し、任意の値を受け渡すこと', () => {
  let value;
  const debugElement = fixture.debugElement.query(By.css('button'));
  component.sendValue.subscribe((v: string) => {
    value = v;
    expect(value).toBe('hoge');
  });
  debugElement.triggerEventHandler('click', null);
});

以下は親コンポーネント側のテストコードとなります。こちらでは、sendValueイベントが発火した時にhandleSendValueが呼び出されるかのテストとなっています。

it('sendValueが発火するとhandleSendValueが呼び出されること', () => {
  const spy = spyOn(component, 'handleSendValue');
  const debugElement = fixture.debugElement.query(By.css('child-component'));
  fixture.detectChanges();
  debugElement.triggerEventHandler('sendValue', 'hoge');
  expect(spy).toHaveBeenCalledWith('hoge');
});

@Outputのユニットテストについては公式ドキュメントの以下の箇所に実例が載っているので、こちらも見てらもうと良いでしょう。

まとめ

以上、Angularの@outputについていろいろ書いてみましたが、ちゃんと使い方さえ覚えてしまえば簡単に使えるものだと思います。しかもその効果はかなり協力なので、複雑になりがちなAngularの処理をそれなりにシンプルにすることができるのではないかと思います。慣れるまでが大変かと思いますが、なんとなく使い続けていくうちにわかるようになってくるかと思います。できる限り意識して使うようにしていくと良いでしょう。

最後にAngularの書籍を紹介しておきます。私は昨年の9月からAngular案件に関わるようになりまして、その時にAngularを覚えるために使った本です。体系的にまとめられていて、内容も良いと思っています。Amazonでの評価も高くおすすめです。

AngularアプリケーションプログラミングAngularアプリケーションプログラミング
  • 『Angularアプリケーションプログラミング』
  • 著者: 山田 祥寛
  • 出版社: 技術評論社
  • 発売日: 2017年8月4日

コメント

  • 必須

コメント