angular2
の RC
もリリースされたことですし、とりあえずどんなワークフローになるか知りたかったので簡単な SPA
を作ってみることにしました。せっかくなので Material Design for Angular2 も使ってみました(執筆時点でまだ 2.0.0-alpha.5 granite-gouda
ですが)。作ってみて現時点で思ったことを記しておきます。 申し訳ないですがあまりまとまりのない文章です! 誤った記載などありましたらご指摘いただければすごくうれしいです
↓こんなの作りました(ソース)
概要
画面としては
- ログイン画面
- ユーザー登録画面
- ダッシュボード(投稿一覧)
- 新規投稿/編集画面
を作りました。サーバーサイドはMockの json-server
を、特にカスタマイズせずに使ったので、ユーザー登録周りとかはただのハリボテです。 Material Design
のコンポーネントがまだ無いもの(例えばダイアログとか)はいったんダサいですけど confirm
とかで代用しました(どんな感じに使うのか知りたかっただけなので)。また、他の css
フレームワークとかはややこしくなるので使ってません。
ざっくりとした動きは↓のような感じです。
所感
まだ本当に表面しか触ってないですけど、率直に楽しいです(これは個人的な意見すぎるかもしれませんが)。特に TypeScript
信者な自分としては、サクサクとコード補完の恩恵も受けながら ES2015
記法でコンポーネントを作っていくのは楽しいです。と、そんな偏った意見はさておき、こんなシンプルなSPAだけど気づいた点についてメモっておきます。
「良い」と思った
- 一つの
class
でコンポーネントが完結するのはうれしい -
View
にローカル変数作れるのうれしい(#) -
http
がデフォルトでObservable
を提供してくれるのはうれしい - Style Guide があるのうれしい
-
Material Design
使うの簡単。けど部品がまだまだ無い(alphaですしね)
「悪い」というよりは「気になった」(と、よくわかってない)
-
Angular2
の公式ドキュメントが結構いろいろなところでコードが壊れている - コンポーネント内で使いたいコンポーネントは
directives
に都度指定する必要があるのがちょっとだけ手間 -
Router
関連の仕様がまだ安定していない(CanActivate
とかpath
からname
消えちゃったりとか、useAsDefault
なくなっちゃったりとか)
順番にもうちょっと詳しくまとめます
一つのclassでコンポーネントが完結するのはうれしい
Decorator
が使えるおかげで一つの class
でコンポーネントを定義できます。Angular1の場合、 component
というものが1.5で追加されたものの、 component
も directive
も controller
を別途指定しないといけないので、下のように2箇所で宣言する必要があります。また class
はホイストされないので component
の定義よりも先に宣言しておく必要があります(少なくとも私の認識では。。。)。 directive
は function
なので class
を先に書いてもちゃんと動くんですけどね。
class HogeController {
// 実装省略
}
export const HogeComponent = {
template: 'hoge.html',
bindings: {
hoge: '<',
moge: '@',
piyo: '&'
},
controller: HogeController // ここに functionで直接実装してもいいですが、ちょっとそれは。。。
}
なので、 HogeComponent
がコンポーネントの定義を説明しているのに、下までスクロール( class
の定義の下にあるので)しないとその中身が確認できない状態です。超個人的ですが、宣言はやっぱり先に見えてる方がわかりやすいと思います。
Angular2は Decorator
を使ってコンポーネントのメタ情報を定義できるので見やすいと感じました。宣言が先にあって、実装が後にあるイメージです。例えば今回作ったダッシュボードコンポーネントは↓のような感じになります。地味に selector
も Decorator
で指定できるようになったのもうれしいです。
@Component({
selector: 'mta-dashboard',
templateUrl: 'src/app/dashboard/dashboard.component.html',
directives: [
ROUTER_DIRECTIVES,
MD_PROGRESS_CIRCLE_DIRECTIVES,
MD_LIST_DIRECTIVES,
MD_CARD_DIRECTIVES,
MdButton
],
pipes: [TranslatePipe]
})
export class DashboardComponent implements OnInit {
posts: any[] = [];
constructor(
private router: Router,
private postsService: PostsService,
private authService: AuthService
) { }
ngOnInit() {
this.fetchPosts();
}
private fetchPosts() {
this.postsService.getPosts()
.subscribe(posts => this.posts = posts);
}
onNewClicked() {
this.router.navigate(['/posts/new']);
}
onEdit(post: any) {
this.router.navigate([`/posts/${post.id}`])
}
onDelete(post: any) {
// TODO: replace when md-dialog is ready
if (window.confirm('削除してもいいですか?')) {
this.postsService.deletePost(post.id)
.subscribe(res => {
this.fetchPosts();
});
}
}
}
View
にローカル変数作れるのうれしい(#)
Viewの中で DOM
の参照をローカル変数として用意できる(#を使う)のも便利だと思いました。これのおかげでコンポーネント側に、Viewと連動する為のフィールドを別途用意する必要がなくなるケースが増えそうです。例えば今回作ったログイン画面とかで使ってます。
<!-- login.component.html -->
<form>
<div>
<md-input placeholder="{{'Username' | translate}}" #username></md-input>
</div>
<div>
<md-input placeholder="{{'Password' | translate}}" type="password" #password></md-input>
</div>
<button md-raised-button color="primary" (click)="onLogin(username.value, password.value)">{{'Login' | translate}}</button>
</form>
<a [routerLink]="['/signup']">{{'Signup' | translate}}</a>
#username
と #password
をそれぞれ input
にくっつけることで、その DOM
の参照を作れて、それをボタン押下時に onLogin
に値を渡しています。渡ってきた値は LoginComponent
で引数として使えます。
@Component({
selector: 'mta-login',
templateUrl: 'src/app/top/login/login.component.html',
directives: [ROUTER_DIRECTIVES, MdInput, MdButton],
pipes: [TranslatePipe]
})
export class LoginComponent {
constructor(
private router: Router,
private authService: AuthService,
private translater: TranslateService
) { }
onLogin(username: string, password: string) {
this.authService.login(username, password)
.subscribe(user => {
this.router.navigate(['/dashboard']);
});
}
}
Angular1
だったら input
毎に ng-model
指定して、 onLogin
でその値を使うイメージになると思います。
<input type="text" ng-model="$ctrl.model.username" />
<input type="password" ng-model="$ctrl.model.password" />
<button type="buttoon" ng-click="$ctrl.onLogin()" />
class HogeController {
onLogin() {
this.authService.login(this.model.username, this.model.password)
// 以下略
}
}
http
がデフォルトで Observable
を提供してくれるのはうれしい
.NET
時代から Rx
大好き人間なので、 Angular2
の http
が提供してくれるのがデフォルトで Promise
じゃなくて Observable
なのはテンションが上がります。なんでもかんでも Observable
でやりたくなりますね。これを気に RxJS
の知識をもっと増やしたいと思います。
また、 template
側でも asyncPipe
を使えば Observable
を template
で直接使うことができます!どういうことかというと、従来(例えばAngular1とか)であれば↓のようなコードをよく書いていたと思います。
ngOnInit() {
this.fetchPosts();
}
private fetchPosts() {
this.posts = this.postsService.getPosts()
// レスポンスをローカル変数に格納
.subscribe(posts => this.posts = posts);
}
取得してきたデータをローカル変数に格納して、それを template
で↓使います。
<!-- postsをtemplateで使う -->
<md-card *ngFor="let post of posts">
<md-card-title>{{post.title}}</md-card-title>
<!-- 以下略 -->
</md-card>
これを、 asyncPipe
を使えば template
で直接 Observable
を使うことができます。
ngOnInit() {
this.fetchPosts();
}
private fetchPosts() {
// Observableをそのまま`posts`に入れる
this.posts = this.postsService.getPosts().do(() => this.isLoading = false);
}
<!-- postsをtemplateで使う -->
<md-card *ngFor="let post of posts | async">
<md-card-title>{{post.title}}</md-card-title>
<!-- 以下略 -->
</md-card>
一手間減っただけにしか見えないかもですが、 Observable
を template
で直接使えるのは便利!(なはず)
Style Guideがあるのうれしい
Angular1
時代からある @johnpapa 氏の(といっても Community Driven
?)Style Guide。 まだ rc
ですし、本格的なアプリケーションも世にほとんど出ていない状態で Style Guideも何もあったもんじゃない、って思われるかもしれませんが、 Angular1
からの流れでそのまま使える考えなどもあるのでやっぱりこういう指標があるのは使う側としてうれしいです。一人でやる分にも役に立ちますが、チームでコーディングする上でもガイドがあるのはうれしいですね。ガイドのカテゴリも
- Do: 常に守るべきルール
- Consider: 守ったほうが良いけど、自分なりに理由があるなら守らなくてOK
- Avoid: やっちゃダメなルール
のように分かれていてすごく見やすいです。
Material Design使うの簡単。けど部品がまだまだ無い(alphaですしね)
コンポーネントで用意されてるので、そのまま使うだけです。CSSほぼ意識せずにそれっぽい見た目のものができあがるのはうれしいです。まだまだ部品が揃っていないですけど、さくっと何か創りたいときとかにい重宝しそうです。これからのアップデートに期待です!
Angular2の公式ドキュメントが結構いろいろなところでコードが壊れている
まだ一応 rc
といえど、公式ドキュメントのサンプルコードのリンクが割りとそこら中で壊れていて結構困りました。そういうときはもう Github を直接みて issue
とかから現在の正解を見つけるしかない、って状態です。
Router関連の仕様がまだ安定していない
上にも関連していますが、中でも特に Router
の仕様が安定していなくて困りました。「ログインしていなかったら見れない画面」を作りたかったのですが、現状の仕組みでは多分無理かなって結論に達しました。 CanActivate
が無くなってしまっていますし、あったとしても Decorator
内にサービスを Inject
できないので、ログイン状態を知る術がない。(ただし、厳密には injector
を使えば取得することは可能)
あと path
から name
(ui-router
みたいなやつ) 消えちゃったりとか、デフォルトの遷移先である useAsDefault
なくなっちゃったりとかしてて、まだまだこれからって感じがします。
コンポーネント内で使いたいコンポーネントはdirectivesに都度指定する必要があるのがちょっとだけ手間
これは私の認識があっているのかちょっと自信が無いのですが、コンポーネント内で使いたい他のコンポーネントは全部 Decorator
に書く必要ありそうです。なので上の例にも載せていたように、基本的に↓のような感じに directives
配列に使いたいコンポーネントを詰め込む感じです。
@Component({
selector: 'mta-dashboard',
templateUrl: 'src/app/dashboard/dashboard.component.html',
directives: [
ROUTER_DIRECTIVES,
MD_PROGRESS_CIRCLE_DIRECTIVES,
MD_LIST_DIRECTIVES,
MD_CARD_DIRECTIVES,
MdButton
],
pipes: [TranslatePipe]
})
export class DashboardComponent implements OnInit {
Angular1
は一つのグローバルスペースに全てが定義されていたので、とにかく angular.module
で宣言しちゃえばどこででも使えたんですけど、 Angular2
には独自の module
なんてものは無いですし、そもそも Angular1
でのグローバルスペースも、名前の競合とか起きると面倒くさいことになっていたので、これは改善になります。が、いざいろいろコンポーネント作ってたらこの directives
を書くのが割りとめんどくさかったです。が、が、他のコンポーネントを Import
してきて自分のコンポーネントで使うので、当たり前といえば当たり前ですね!
最後に
とにかく最初にも書きましたが、作るのすごく楽しいです。 TypeScript
も ES2015
も RxJS
も使えてワクワクしちゃいます。これにさらに ng2-redux
も組み合わせたいですね。これから Material Design
の部品も増えていくでしょうし、徐々にこのサンプルアプリもブラッシュアップしていけたらと思います。
まとまりのない記事ですがここまで読んでくださりありがとうございます!
ソースは Githubに上げているので、よろしければ遊んでみてください。