Angular2
Angular 2Day 14

Angular 2 @Outputのアレコレ

More than 3 years have passed since last update.

おはようございます、@armorik83です。Angular 2の特徴的なAPIとして、前回@Inputを紹介しましたが、今回は@Outputを取り上げます。


記事のサポート遅れについてお知らせとお詫び

先週の記事ではAngular 2 alpha.47を使用していたはずが、本稿執筆時点での最新バージョンはalpha.53となっており、何やら怒涛の勢いでalphaリリースが行われています。APIが安定する(と予想される)betaまでに詰め込めるだけのBreaking Changeを詰め込めという様子で、すでに先週公開した記事が最新alphaでは動かないという信じがたい状況になっております。変更の傾向を見ていると、機能そのものの追加や削減は見られないため、本記事が役に立たなくなることは無いと思っていますが、betaまで見守っているという状況です。このような情報鮮度のため読者諸氏にはご迷惑をお掛けしています。すみません。

本記事は動作する最新版を元にしていますが、12月中に書いた他の記事についてはサポートが追いついてませんので、今後の追記までの間ご了承くださいませ。

そして最新のa53はPlunkerにてCDNが未サポートで使用できなかったため、本記事執筆時点ではa52にて検証しています。

【追記151226】Angular 2 beta.0に対応しました。


@Outputとは

本題に戻ります。@Output (Docs) は、イベントのバインディングを定義するAPIです。@Inputが属性を定義するAPIなのに対して、少しこの説明だけでは分かりにくいですね。

早速例を見てみましょう。

http://plnkr.co/edit/fZdBKILY1EPWo3oauhFD?p=preview

import {Component, Directive, Output, EventEmitter} from 'angular2/core';

import {CORE_DIRECTIVES} from 'angular2/common'

@Directive({
selector: `my-interval`,
})
class Interval {
@Output() everySecond = new EventEmitter();

constructor() {
setInterval(() => {
this.everySecond.emit("event");
}, 1000);
}
}

@Component({
selector: `my-app`,
template: `
<my-interval (everySecond)="onEverySecond()"></my-interval>
<p>{{i}}</p>
`
,
directives: [CORE_DIRECTIVES, Interval]
})
export class App {
constructor() {
this.i = 0;
}

onEverySecond(): void {
this.i++;
}
}

Interval Directiveを定義しました。Angular 2ではテンプレートを持たないものをDirective、テンプレートを持つものをComponentと呼びます。ここはAngularJSのDirectiveと若干異なるので注意。

@Inputの時と同じように、classのプロパティに@Output()アノテーションを付与するだけで宣言完了です。簡単ですね。これでInterval DirectiveにeverySecond Outputが定義されました。

このOutputから発せられるイベントのハンドリングは親が扱います。

<my-interval (everySecond)="onEverySecond()"></my-interval>

(everySecond)=""に指定した処理が、子のイベントによって発火します。例で言うと、1000ms毎にemitが呼ばれ、これをトリガにして発火します。

なおAngular 2 a50-52辺りで(変更が細切れすぎて追えていないのですが)属性名は全てキャメルケースにする、という規定になりました。どうしてもケバブケース(ハイフン)で扱いたい場合は@Output()の引数にエイリアスとして@Output("every-second")と指定せねばなりません。逆に考えるとHTMLとJSのコンテキストで脳を切りかえる必要が無くなったので、シンプルっちゃあシンプルです。今後覚えるみなさんは「どっちを書くときもキャメルケースで統一」でかまいません。

この辺の変更が、古い記事を軒並み動かなくさせた原因でもあります。


Outputの何が嬉しいか

上の例では単純にクロックを生成するだけなので、使い道がイメージしにくかったかもしれません。Outputの嬉しさはWebアプリ開発の現場からすると幾つか考えられます。


イベントの命名をドメインに即したものとして扱える

例えばボタンにはonClickがあるとします。これを@Outputで書くならばこのようになるでしょう。

import {Component, Directive, Output, EventEmitter} from 'angular2/core';

import {CORE_DIRECTIVES} from 'angular2/common'

@Component({
selector: `my-button`,
template: `
<button>I am a button</button>
`

})
class MyButton {}

@Component({
selector: `my-app`,
template: `
<my-button (click)="onClick()"></my-button>
<p>{{notification}}</p>
`
,
directives: [CORE_DIRECTIVES, MyButton]
})
export class App {
notification: string;

constructor() {
this.notification = ``;
}

onClick(): void {
this.notification = `clicked!`;
}
}

http://plnkr.co/edit/0q84u6jOCyzFSS68hK0G?p=preview

@Output clickは最初から使えるようになっており、親はテンプレート内で(click)=""するだけで扱えます(言い換えるとclass MyButton@Output() clickと宣言する必要はありません)。これだけでも十分手軽ですが、対象が増えるとテンプレートのあちこちに(click)ばかり並ぶことになります。

Outputの活用として、onClickをアプリケーション・ドメインに即した命名で再宣言する、例えばタスク管理の機構があったとして、完了ボタンコンポーネントに対してcomplete Outputを持たせる、などが考えられます。

@Component({

selector: `task-button`,
template: `
<button (click)="onClick($event)">□</button>
`

})
class TaskButton {
@Output() complete = new EventEmitter();

onClick(ev: MouseEvent): void {
this.complete.emit(ev);
}
}

http://plnkr.co/edit/bZbWokQfAXF4XwCPEMlv?p=preview

親Component側は<TaskButton (complete)="onComplete()"></TaskButton>と書けます。まだ一つだけなので(click)と大差ないように見えますが、ツールバー実装などでは命名がはっきりしていると重宝しそうです。Fluxアーキテクチャを採用すると、多くの処理はすぐに裏側に飛んでいきますが、とはいえ各種汎用ボタンがベタっと実装(たとえばAPI側へのパラメータの送出など)を持つわけにもいかず、親による何らかのハンドリングは避けられません。

UIパーツは汎用で作りたい、そのパーツの動作は外部から与えたい、でもView全体は出来る限りFat Controllerの悪夢から逃れたい…。そんな悩みは@Outputですっきり整理して、複雑な処理は【ここに好きなアーキテクチャ名を入れよう】に乗せて裏へ送りましょう!帰ってきたデータは@Inputで親がどんどん与えていき、子はそれに従い、ただバインドして描画していけばよいのです。


マウスイベントの取得は簡単

clickについて触れたので、ついでに話しておきましょう。

clickはAngularJSでのng-clickの使い勝手と変わりません。引数に$eventとするとマウスイベントが扱える点も同じです。

@Component({

selector: `task-button`,
template: `
<button (click)="onClick($event)">□</button>
`

})
class TaskButton {
@Output() complete = new EventEmitter();

onClick(ev: MouseEvent): void {
this.complete.emit(ev);
}
}

this.complete.emit(ev);として、ちゃんとマウスイベントをemitしている点に注意してください。ここを見落とすと親側では何も受け取れません。emit()の引数ということは、つまりどんな値を渡してもよいのです。計算済みの座標だけを送ってもよいですし、キーコンビネーションとタイムスタンプでも構いません。UIの機能に必要なプロパティのみを抽出して整流できるのも@Outputの嬉しい点です。

親側のテンプレートでは子のemit()の値について、必ず$eventで受け取ります。Angular 2のCORE_DIRECTIVEだろうが自作Componentだろうが関係なく、<my-example (fooBar)="onFooBar($event)"></my-example>のように扱います。

一方、受けるメソッド名は自由です。(fooBar)="bazQux($event)"とかやっても問題ありません。分かるならなんでもいいです。

@Outputを表す属性表記()on-と書き換えることも可能です。ただしこの場合(click)on-clickになりますが、(fooBar)on-fooBarとなり少々不格好なので、Angular 2を活用するならば()表記に慣れていくほうがよさそうです。(一応@Output("foo-bar")とすることでon-foo-barと書くことは可能)


RxJSとの組み合わせ

いまAngular 2のalphaリリースの中で最も熱いのがRxJS周りなのですが、Plunkerでサンプルをお見せできないため紹介に留めます。

Angular 2では、上記の通りEventEmitter@Outputで独自のイベントを定義できますが、この他にRxJSも標準的に扱えるようになります。RxJSを用いることで、非同期かつ連続したデータの受信を捌いたり、連続したウインドウやマウスのイベントをfilterできるなど、UI/UX実装のための様々な面でstreamの恩恵が得られます。サーバからのレスポンスを整流できるのも大きな点ですが、UI実装でunderscore/lodashを使いながら苦労していた方も多いのではないでしょうか。

残念ながらまだ具体的なサンプルコードに至らなくて申し訳ないのですが、今後のBetaリリースなどでAngular 2 + RxJSの知見が集まり次第、また改めて記事にしようと思います。

【このカレンダー内にもいくつかあるのでご紹介】


$broadcastと$emitは廃止

AngularJSにあった$broadcastおよび$emitは、この@Outputの登場により廃止されます。伏線を回収できたようで個人的には嬉しいのですが、実は今年の3月に開催されたng-japanにて、私はこんな質問をしていました。

「Angular 2で$broadcast, $onなどのEmitterの仕組みは用意されるのか?」

この時の回答は、次のようなものです。


1つはDOM Eventですね。Angular specificなEventを使う代わりにDOM Eventを使っていただくという方法と、2つ目はbacon.jsといったサードパーティのライブラリを使っていただくということもできます。


DOM Eventというのは、すなわちマウスやウインドウのEventで、bacon.jsはちょうどRxJSに置き換わったと見られます。この頃からもう@Input@Outputの設計構想はあったようですね。質問をした当時はどうなることかと思いましたが、素敵な方向に解決してくれました。


(追記)@OutputはObserverパターンなのか?

興味があったので後日調べてみました。追記しておきます。Outputの実装を読んでおり濃い目の内容となっているので、興味のある方は。


まとめ

今回のまとめです。


  • Angular 2のalphaリリースが怒涛のように加速していて追うのが大変(アドベントカレンダーな時期に限って…)


  • @OutputとEventEmitterでアプリケーションに則したイベントを整える

  • RxJSは勉強しておくと吉

Angular 2のalphaリリースが本当に矢継ぎ早なせいで、細部の検証がままならない点は本当にすみません。来年以降も引き続きAngular 2の技術記事を掲載していく予定にしています。

次回は12月25日のトリですか、それではまた。