ハッピーバースデー俺。
さて、Dart界隈で有望そうなクライアントサイドフレームワークといえばフルスタック双方向バインディングでおなじみなAngular2さんくらいのものなのですが、少なくとも1年前はeveryday breaking changes! なヒャッハー状態で使い物にならない雰囲気を感じたものです。
が、あれから1年の間にいろんな事がおこり、GoogleマジAngular2をDartで使ってるからね!見捨ててないからね!アピールをしてきたのであらためて使ってみたいと思います。
とりあえず適当なのを作ってみた
デモページ
http://takyam-git.github.io/angular2_test_20151212/
ソースコード
https://github.com/takyam-git/angular2_test_20151212
アプリケーションの概要としては、右側のフォームで入力した文字列が、左側に順番に表示されるだけの糞みたいなアプリケーションですね。
愚にもつかないアプリケーションではあるのですが、私はAngularに全然明るくないので許してちょんまげ。(半日かけてここまで出来ただけでも驚きだよ!)
で、今回これを作ってみて得られた知見を披露したうえで、後世のAngular2のDartバージョンでアプリケーションを作れる時代にきっと同じポイントでハマる人が出てくると思うのでその一助になればと思っております。
感想は最後に書いてます。
とはいえ
Angular 2 is currently in Developer Preview. We recommend using Angular 1.X for production applications:
ってREADMEの先頭に書いてある程度にはα版なので、この知見もすぐ使えなくなると思うけどね!
知見を箇条書き
- コンポーネントが依存してるコンポーネントは
derectives
に渡してあげる必要がある -
NgIf
やNgFor
といったよく使うディレクティブもderectives
に渡してあげる - コンポーネントからコンポーネントにオブジェクトを渡すときは
@Input
-
nl2br
的モジュールはPipe
を使うと良さそう -
Pipe
を定義するときには@Injectable
を忘れずに - テンプレートでHTMLエスケープせずに埋め込みたい場合は
[innerHtml]
-
event.preventDefault()
みたいなのしたいときはテンプレートから$event
を渡す
アプリケーションの構成
今回作ったclassは↑の図のような感じ。大した量じゃないね。
1個の(シングルトンな)Threadに複数のRes(書き込み)が紐づく感じ。
各Componentは画面上でいうとそれぞれ↑みたいな感じ。
AppComponentの子にThreadComponentとPostComponentの2つがいる。
ThreadComponentの子に書き込みの数だけResComponentがいる。
AppComponent
階層の上から順に見たほうが構成が把握しやすいと思うのでまずはAppComponentを見てみましょ。
@Component(
selector: 'my-app',
templateUrl: 'app_component.html',
directives: const [ThreadComponent, PostComponent]
)
class AppComponent {
Thread thread = new Thread();
}
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Simple BBS (現在の書き込み数: {{thread.resList.length}}個)</a>
</div>
</div>
</nav>
<div class="container">
<div class="row">
<div class="col-md-8">
<my-app-thread></my-app-thread>
</div>
<div class="col-md-4">
<my-app-post></my-app-post>
</div>
</div>
</div>
依存関係を明示的に宣言してあげる
Dartのコードの方はThreadのシングルトンを取得している他、ThreadComponent
と PostComponent
への依存を明示しているくらいです。
HTMLの方もBootstrapのいわゆるテンプレートそのままで、タイトルのところに書き込みの数を表示するために {{thread.resList.length}}
があるのと、ThreadComponent
と PostComponent
を呼び出す <my-app-thread></my-app-thread>
と <my-app-post></my-app-post>
が書いてあるくらいであとは普通のHTML。
directivesがかなりイケてるなぁと思ったのは、ひとつのコンポーネントが依存しているコンポーネントを明示すれば十分で、たとえばこの後説明する ThreadComponent
は ResComponent
に依存してるわけなんですが、AppComponent
はその事を知らなくていいし宣言しなくてよい。
あくまで自分自身が利用しているコンポーネントなりディレクティブを宣言してあげればよいので、非常に明快で分かりやすいですね。
あとは thread.resList.length
なんですが、これはもうDartのオブジェクトのプロパティをHTML上で直接参照できていて、使い勝手として違和感が無いのはすごいなと思った。
ThreadComponent
@Component(
selector: 'my-app-thread',
templateUrl: 'thread.html',
directives: const [NgFor, NgIf, ResComponent]
)
class ThreadComponent {
Thread _thread = new Thread();
List<Res> resList;
ThreadComponent() {
this.resList = this._thread.resList;
}
}
<my-app-res *ngFor="#res of resList.reversed" [res]="res"></my-app-res>
<p *ngIf="resList.length == 0">右の投稿フォームから入力してください</p>
NgIf
やNgFor
もディレクティブとして使うための宣言が必要
こちらも同じように依存している ResComponent
を directives
に追加してあげてます。
AppComponent
では使ってなかったので書いてませんでしたが、ThreadComponent
では*ngFor
と*ngIf
をHTML上で利用しているのでNgFor
とNgIf
もdirectives
に追加しています。
DartのオブジェクトはDartのオブジェクトとして利用可能
あとはRes
オブジェクトの配列である Thraed.resList
を自身のプロパティresList
にセットする事でHTMLからRes
のリストにアクセスできるようにしています。
<my-app-res [res]="res"></my-app-res>
ですが、 res
は Res
のインスタンスで、それをそのまま渡せてる点がマジで便利です。すごい。ただしこれも渡せるようにするのに ResComponent
側で設定がいるので注意です(後述)。
#res of resList.reserved
を見ると分かるように .reserved
はDartのListのメソッドで、順序を反転したIterableを返してくれるのですが、そういったものも特にAngularだからどうのこうのを意識せずにHTML上で呼び出すことができます。すごい。
ResComponent
@Component(
selector: 'my-app-res',
templateUrl: 'res.html',
directives: const [NgIf],
pipes: const [Nl2BrPipe]
)
class ResComponent {
@Input() Res res;
ResComponent();
void destroy(Event event) {
event.preventDefault();
new Thread().removeRes(this.res);
}
}
<div class="panel panel-default">
<div class="panel-heading">
ID: {{res.id}} 投稿日時: {{res.postedAt.toString()}} <a (click)="destroy($event)" class="btn btn-default btn-sm">削除する</a>
</div>
<div class="panel-body" [innerHtml]="res.body|nl2br">
</div>
</div>
これは結構ノウハウが詰まったComponentなんで細かく説明します。
directivesだけじゃなくpipesも宣言が必要
まず、directivesにHTMLで利用しているNgIf
を追加しています。
次にほげほげ\nふがふが
をほげほげ<br>ふがふが
にするためのNl2BrPipe
を利用することをpipes
で宣言しています。ディレクティブと同じように、利用するPipeも個別のコンポーネント毎に宣言が必要なようです。
@Input()
アノテーションが無いと値を渡せない
つぎに @Input() Res res;
についている @Input()
アノテーションが重要で、
これがあることで ThreadComponent
から <my-app-res [res]="res">
のように、まるでComponentの引数のようにプロパティをセットする事ができるようになります。このアノテーションを設定していないと、利用する側が [res]="res"
のように記述したとしてもComponentに渡ることはありません。
HTMLをテンプレートに突っ込みたい場合は[innerHtml]
Nl2BrPipe
は実装を見ると分かるんですが、HTMLエスケープ+改行をBRタグに変換した HTML文字列 を返してくれるのですが、それをAngularのテンプレート(HTML)上に直接埋め込もうとすると(例: <div>{{res.body | nl2br}}</div>
)、HTML文字列をさらにHTMLエスケープしてしまいます。こういった場合は以前は ng-bind-unsafe-html
のような機能を利用したりして埋め込んでいたようなのですが、現在は [innerHtml]="res.body|nl2br"
のように、DOMのinnerHtmlに突っ込むのが割と正しいアプローチのようでした。
ぐぐると [inner-html]
とかいろいろでてきますが [innerHtml]
が正しいです。
イベントは$event
変数を渡してあげればオッケー
最後に (click)="destroy($event)"
とそれから呼び出される destroy(Event event)
を見てもらえると分かるように、クリックイベントをキャンセルさせたい場合は $event
を渡してあげることで、event.preventDefault()
が呼び出せるようになるようです。詳細は分かっていないのですが、この $event
変数は特に明示的に宣言せずに使える変数になっているようです。
データを消せばDOMも消える
なお、 new Thread().removeRes(this.res)
を実行することで、Thread.resList
からこのResComponent
が持ってるRes
が削除され、当然なが同じListを参照している ThreadComponent.resList
からも消えますので、この ResComponent
自体も削除される、といった処理の流れになり、この削除ボタンが動作します。
明示的にDOMの削除のような事は一切せずに、データストアである Thread
からデータを1件削除するとそのデータを表示していたDOMがちゃんと消えるあたり、ナウい感じはします。
PostComponent
@Component(
selector: 'my-app-post',
templateUrl: 'post.html'
)
class PostComponent {
Thread _thread = new Thread();
PostComponent();
void postNewRes(Event event, TextAreaElement resInput) {
event.preventDefault();
String resBody = resInput.value;
resInput.value = '';
this._thread.addRes(resBody);
}
}
<form (submit)="postNewRes($event,resInput)">
<div class="form-group">
<label for="resInput">投稿内容</label>
<textarea #resInput id="resInput" class="form-control" rows="5"></textarea>
</div>
<button class="btn btn-block btn-primary">書き込む</button>
</form>
投稿フォーム部分で使ったノウハウとしては<textarea #resInput>
のように #resInput
でDOMを変数化(のようなイメージ)でき、postNewRes($event, resInput)
のように関数呼び出し時にそのDOMのオブジェクトを渡すことができるようになります。
ここは本来は入力中の文字列をデータバインディングする設計のほうが美しいのですが、何かうまいこといかなかったのでDOMを直接Component側が操作するような対応になっています。
ComponentがDOMを操作する事は許されることでしょうし、必要不可欠な場面もあるとは思うのですが、やらなくてすむならやりたくないですね。
Nl2BrPipe
@Pipe(name: "nl2br")
@Injectable()
class Nl2BrPipe implements PipeTransform {
final _regexp = new RegExp('(\r\n|\n\r|\n|\r)');
dynamic transform(dynamic value, List<dynamic> args) {
return HTML_ESCAPE.convert(value).replaceAll(this._regexp, '<br>');
}
}
以前は class HogePipe extends Pipe {}
のような宣言方法だったようですが、現在はこのように @Pipe()
アノテーションをつけたうえで PipeTransform
をimplementsするのが正しいようです。また、Componentのpipes
に渡せるようにするために@Injectable()
アノテーションも併せて宣言しておく必要があるようでした。
感想
1年前と比較して、それほど開発しやすくなった印象はありませんでした。
CHANGELOGを見てもBREAKING CHANGESがかなり並んでますし。
開発しづらい理由
特にとっかかりとして開発しづらい理由は明確で、 最新のDartの資料が無い ことにつきます。
ドキュメントとしてはTypeScriptが一番充実しているので、TypeScriptのドキュメントを参考にしながら紐解いていく感じになります。
とはいえそもそも公式ドキュメントの言葉足らず感がパネェので、結局はググることになるんですが、その際に出てくるものの大半が 「 既にBreakingChangeされた情報 」「 Angular1.xの情報 」「 JSあるいはTypeScriptにおける情報 」の3種類で構成されており、今まさに必要な 「 Angular2.xのDartの場合の情報 」を拾ってくる難易度が超高いです。
Angular2の出来の良さ
ある程度分かってみれば、非常に洗練された設計になっているし、セーフティだし、パフォーマンスも想像以上に良いしで、かなり良いフレームワークに仕上がってきている印象は受けるのですが、いかんせん「 こうしたい場合にどうしたらいいの? 」を探しだすコストがパネェので、早くBREAKINGなCHANGESが無くなってくれて、Angular2の確かな情報が出回る世界になるといいなぁと思います。
いまとこれからザックリまとめ
- 悪くない(他のDartのクライアントサイドフレームワークと比べると非常に良い出来)
- あるていど走りだしが辛いのは覚悟が必要
- 頑張って手に入れたノウハウは枯れる前に使えなくなる覚悟も必要
- 普通に考えてDeveloper Previewじゃなくなるまで待てばいいんじゃないかな