Angular 2 @Outputのアレコレ
おはようございます、@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 なのに対して、少しこの説明だけでは分かりにくいですね。
早速例を見てみましょう。
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!`;
}
}
@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);
}
}
親 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 日のトリですか、それではまた。
Discussion