Wantedly Advent Calendar 2015 17日目の投稿です
Wantedlyエンジニアの @KentoMoriwaki です。
Angular2 触ってみませんか?
Angular2.0 betaが遂にリリースされましたが、後方互換性のなさや、TypeScriptで書かれていることもあり、なかなかとっつきづらい印象がありました。
しかし、実際に触って見ると、1.xで不満だった部分が解消され、かなり綺麗に記述できるようになっていたので、結構気に入ってしまいました。
そこで、Angular1でよくお世話になっているngHideDirectiveの簡易版myHideをAngular1と2で実装して比較してみることで、みなさんにもAngular2を分かった気になってもらえればと思い、この記事を書きました。
対象はAngular1をちょろっとでも触ったことがある人です。TypeScriptは知らない人でも分かるように書いたつもりです。
Angular1のmyHide
まずは、Angular1の方で書いてみます。
my-hide Directiveは与えられたプロパティの値の変化に伴って、ng-hideclassが付いたり消えたりするものです。
Directiveを使用するhtmlはこんな感じになります。
<div my-hide="hidden">
Hide me!
</div>
hiddenの値により、Hide me!の表示・非表示が切り替わるイメージです。
Directiveの定義はこんな形になりますね。
angular.module('myApp').directive('myHide', function() {
return {
restrict: 'A',
link: function(scope, element, attr) {
scope.$watch(attr.ngHide, function(value) {
element[value ? 'addClass' : 'removeClass']('ng-hide')
});
}
};
});
本家のngHideでは、$animateを使っていたり、パフォーマンスのための設定などが書かれていますが、大体の実装は同じです。
やっていることを簡単に説明すると、app.directiveにDirectiveの名前と、その設定を返す関数を渡しています。このDirectiveが使用される要素が見つかった時に、設定中のlink関数が実行され、hiddenパラメータの変化を監視を開始し、変化に伴いDOMを操作してng-hideクラスの付け替えを行うようになります。
Angular2のmyHide
では、このmyHideをAngular2で書いてみましょう。
まず、htmlの方ですが、明らかに1とは変わっています。
<div [myHide]="hidden">
Hide me!
</div>
ハイフン繋ぎで書かれていたmy-hideが、camelCaseのmyHideに代わり、謎の[]で囲まれています。
ng-hideからmyHideになったのは、Directiveの定義との統一性が取れたので嬉しいです。よく定義の方でapp.directive("my-hide")とか書き間違ってしまうことがあったので。
データバインディング
[]が何を表しているのか直感的に分かる人はいないでしょう。
[]の仲間に()と[()]も存在しますが、これらはバインディングの種類を明示的にするためのものです。
Angular1では、ビューだけみても、どんなDirectiveが定義されているか知らなければ、どの属性がバインディングされているのか分かりにくかったですが、Angular2ではひと目見ただけでどのようにバインディングされているのかが分かるようになりました。
Angular2には以下のような3種類のバインディングに分けられています。
-
[]data => view の一方向バインディング- 例:
[myHide]="hidden" - データの変更によりビューを変更する際に使用する。
- Property Bindingと呼ばれる。
- 例:
-
()view => data の一方向バインディング- 例:
(click)="onClick()" - ビューのイベントによりデータを変更する際に使用する。
- Event Bindingと呼ばれる。
- 例:
-
[()]双方向バインディング- 例:
[(ngModel)]="model.name" -
[]と()を組み合わせて双方向のバインディングを実現するためのシンタックス。 - 実際、この例は
[ngModel]="model.name" (ngModelChange)="model.name=$event"のように2つに分けて書くこともできる。
- 例:
このシンタックスは初めはとても奇妙に見えますが、一回理解してしまえば、なんでもng-foo="bar"と書いていたのと比較して、構造の把握が簡単になったと思います。しかし、HTMLのテンプレートエンジンとの親和性が更に下がったのではないかと懸念もあります。
次にDirectiveの定義をしていきましょう。
TypeScriptで書いていますが、初めての人も分かるように説明したいと思います。
import {Directive, Input, ElementRef, Renderer} from 'angular2/core';
@Directive({
selector: '[myHide]'
})
export class HideDirective {
@Input('myHide') set value(val: boolean) {
this.renderer.setElementClass(this.el, 'ng-hide', val);
}
constructor(private el: ElementRef, private renderer: Renderer) { }
}
やっていることは、HideDirectiveクラスを定義しているだけです。
@XXXというのは見慣れない表記があるので混乱してしまいますが、一旦無視して読んで見れば、大体把握できると思います。
Dependency Injection
まずは、HideDirectiveクラスのコンストラクタを見てみましょう。
constructor(private el: ElementRef, private renderer: Renderer) { }
2つの引数、ElementRef型のelと、Renderer型のrendererを受けて、それをインスタンス変数に設定しています。ElementRefとRendererは、Angular2で定義されているクラスで、DIにより初期化時にそれぞれのクラスのインスタンスが渡されます。
次に、ng-hideクラスの付け替えを行っている部分を見てます。
@Input('myHide') set value(val: boolean) {
this.renderer.setElementClass(this.el, 'ng-hide', val);
}
@Inputを無視してみると、set value(val: boolean) {} でvalueプロパティのセッターを定義しています。
セッター内で、受けた値に応じて、要素にng-hideクラスを付けたり外したりしています。
Angular2ではサーバーサイドやweb workerでのレンダリングを可能にするために、DOMを直接操作することは推奨されていません。そのため、先ほどコンストラクタで受け取った ElementRefとRendererを用いて間接的に操作する必要があります。
Decoratorによる
残りの@Inputの部分は、Angular1のscope.$watchに対応する部分です。
この部分を理解するためには、@XXXのような書き方を説明しておく必要があります。これは、TypeScriptのDecoratorという仕組みです。
クラスやメソッド、プロパティの定義の直前に書くことで、その対象にメタ情報を付与したり、拡張したりすることができる機能で、Angular2ではこの書き方を多用しており、一瞬たじろいでしまいますが、メタ情報の定義だと分かってしまえば楽勝です。
@Input('myHide')は、valueプロパティにProperty Bindingのためのメタ情報を付与しています。これで、myHideのプロパティ名が変化したときに、このセッターが呼ばれて、要素にng-hideクラスが付けられるという流れが実現します。
最後に、@DirectiveDecoratorでディレクティブの設定を書いて完了です。
@Directive({
selector: '[myHide]'
})
このDirectiveは属性として使用するので、selectorに[myHide]とcssセレクタを書きます。
Angular1のrestrict: 'AEC'という呪文から開放されて、直感的になり嬉しい限りですね。
ここまで書けば、Angular2の大体の雰囲気が分かったんじゃないかと思います。
Angular1では、簡単なDirectiveを定義するためにも、書き方の雛形を覚えて、linkやscope, $watchなどを使いこなす必要がありましたが、Angular2では、基本的にはクラス定義だけで、Decoratorで幾つかの付加情報を付けるだけで良くなり、かなり綺麗なコードが書けるようになったと感じます。
そのおかげでテストを書くのも簡単になっているんじゃないかと期待していますが、その辺りはまた今度触ってみたいと思います!
まとめ
- 基本的にクラスを定義するだけ
- Decorator便利
- Bindingがひと目みて分かるようになった
- DOMは触らない
- Angular2怖くない