概要
一回触ってみたいと思っていたAngular2
をようやく触ってみた。最近は新しいフレームワークやライブラリを触る場合はゲームを作ってみるか、React
のチュートリアルをやるようにしていて、今回はReact
のチュートリアル(コメントフォームのやつ)をAngular2
でやってみることにした。
基本的には環境構築周りは以下のシェルスクリプトマガジンとAngular
のSlackチームng-japan
のビギナー用に紹介されていた手順を踏んでいる。

- 作者: 當仲寛哲,岡田健,佐川夫美雄,大岩元,松浦智之,後藤大地,白羽玲子,水間丈博,濱口誠一,すずきひろのぶ,花川直己,しょっさん,法林浩之,熊野憲辰,桑原滝弥,USP研究所,ジーズバンク
- 出版社/メーカー: USP研究所
- 発売日: 2016/04/25
- メディア: 雑誌
- この商品を含むブログを見る
なにか間違いやより良い方法などありましたらご指摘願います。
バージョン
package.json
も最後にのせるが
Angular: 2.0.0-rc.3
Typescript: 1.8.10
で試している。
Repository
作業ログ
インストール
Angular2
関連のパッケージは@angular/xxxxx
という形式になっているよう。すこし前まではangular2/xxxxx
だったのかな。
一点注意が必要なのはRx.js
のバージョンが上がって、symbol-observable
が必要になった点でしょうか。その他はほぼ、slackで紹介されていた手順だと思う。
$ npm init -y $ npm i -S es6-shim systemjs symbol-observable zone.js reflect-metadata rxjs @angular/common @angular/compiler @angular/core @angular/platform-browser @angular/platform-browser-dynamic $ npm i -D concurrently lite-server typescript typings
npm script
の記述。
- package.json
"scripts": { "start": "concurrently \"npm run tsc:w\" \"npm run lite\" ", "tsc": "tsc", "tsc:w": "tsc -w", "lite": "lite-server", "typings": "typings", "postinstall": "typings install" },
typescript
の設定。
- tsconfig.json
{ "compilerOptions": { "target": "es5", "module": "system", "moduleResolution": "node", "sourceMap": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "removeComments": false, "noImplicitAny": false }, "exclude": [ "node_modules", "typings/main", "typings/main.d.ts" ] }
型情報のインストール
$ node_modules/.bin/typings init node_modules/.bin/typings install -GS es6-shim jasmine --source dt
touch index.html mkdir scripts touch scripts/main.ts mkdir -p components/home touch components/home/home.ts touch system-config.ts touch bs-config.json
- bs-config.json
{ "port": 8000, "files": ["./**/*.{html,htm,css,js}"], "server": { "baseDir": "./" } }
system.jsの設定。この辺りよく作法を理解していないので要復習。
- system-config.js
System.config({ map: { '@angular': 'node_modules/@angular', 'rxjs': 'node_modules/rxjs', 'scripts/main': 'app/main.js', 'symbol-observable': 'node_modules/symbol-observable', }, packages: { '@angular/core': { main: 'index' }, '@angular/common': { main: 'index' }, '@angular/compiler': { main: 'index' }, '@angular/http': { main: 'index' }, '@angular/router': { main: 'index' }, '@angular/platform-browser': { main: 'index' }, '@angular/platform-browser-dynamic': { main: 'index' }, 'symbol-observable': { defaultExtension: 'js', main: 'index' }, 'rxjs': { main: 'Rx' }, 'components': { main: 'index' } } });
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <base href="/"> <title>ng2 comment system</title> </head> <body> <comment-box>Loading...</comment-box> <script src="node_modules/es6-shim/es6-shim.js"></script> <script src="node_modules/reflect-metadata/Reflect.js"></script> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="node_modules/zone.js/dist/zone.js"></script> <script> System.import('system-config.js') .then(function () { System.import('app/main'); }) .catch(console.error.bind(console)); </script> </body> </html>
- app/main.ts
import {bootstrap} from '@angular/platform-browser-dynamic' import {AppComponent} from '../components/home/home' bootstrap(AppComponent);
- components/home.ts
import {Component} from '@angular/core' @Component({ selector: 'my-app', template: ` <h1>Hello ng2</h1> ` }) export class AppComponent { }
これでnpm start
するとブラウザが開いて、Hello ng2
と表示されるはず。
Commentボックスの実装
app/main.ts
から呼ぶのをcomponents/comment-box
に修正。
- scripts/main.ts
import { bootstrap } from '@angular/platform-browser-dynamic'; import { CommentBox } from '../components/comment-box'; bootstrap(CommentBox);
index.html
から呼ぶコンポーネント名を修正
<ng-app>を<comment-box>に変更
<body> <comment-box>Loading...</comment-box> ...
components/home.ts
をcomponents/comment-box.tx
に修正して、以下のように修正した。
comment-box
- components/comment-box.ts
import { Component, OnInit } from '@angular/core'; import { CommentList } from './comment-list'; import { CommentForm } from './comment-form'; import { CommentService } from '../service/comment'; import Comment from '../interfaces/comment'; @Component({ selector: 'comment-box', providers: [CommentService], directives: [CommentList, CommentForm], template: ` <div class="commentBox"> <h1>Comments</h1> <comment-list [comments]="comments"></comment-list> <comment-form (onCommentSubmit)="handleCommentSubmit($event)"></comment-form> </div> `, }) export class CommentBox implements OnInit { comments: Comment[]; constructor(private commentService: CommentService) { } ngOnInit() { this.commentService .startIntervalFetch() .subscribe(comments => this.comments = comments); } handleCommentSubmit(comment) { comment.id = this.comments.length; this.commentService .add(comment) .subscribe(res => this.comments.push(res)); } }
こうなるまで結構右往左往したんだけど、ひとまず結果だけを載せておく。 また、ポイントとなりそうな箇所を以下に記載しておく。
コンポーネント
Angular2
ではコンポーネントはDecorator
を使用して以下のような形で記述するらしい。
@Component({ selector: 'comment-box', providers: [CommentService], directives: [CommentList, CommentForm], template: ` <div class="commentBox"> <h1>Comments</h1> <comment-list [comments]="comments"></comment-list> <comment-form (onCommentSubmit)="handleCommentSubmit($event)"></comment-form> </div> `, }) export class CommentBox implements OnInit { ...省略
selector
他のコンポーネントからこのコンポーネントを呼ぶ場合はこの名前を使って呼ぶ。上記の場合であれば<comment-box></comment-box>
とする。(<comment-box />
とは書けないよう。)また、カスタムディレクティブの場合は使用するディレクティブを次のdirectives
で指定する必要があるっぽい。
directives
使用するカスタムディレクティブをここで指定する。この場合はCommentList, CommentForm
というディレクティブを使用する。
providers
Inject
するサービスを指定する。View
に関心のない処理などはサービスとして切り出してInject
するらしい。このコメントシステムであればサーバとAjax通信する処理をサービス(CommentService
)として切り出して、Inject
している。Inject
される側(CommentService
)は後述する。
templete
テンプレートストリングを使用して、以下のように書く。
template: ` <div>Hello</div> `
また、templateUrl
を使って次のように別ファイルのhtml
を使用するこもできるっぽい。
templateUrl: 'foo/bar.html'
どっちが主流なのだろうか。デザイナーとの協業であれば後者のほうがやりやすいだろうし、チームの体制に左右されるだろうか。テンプレートストリングだとエディタのシンタックスハイライトとかインデントが死んでてみんなどうしているのって感じ。
class
デコレートされる側のclass
は以下のようになっている。
export class CommentBox implements OnInit { comments: Comment[]; constructor(private commentService: CommentService) { } ngOnInit() { this.commentService .startIntervalFetch() .subscribe(comments => this.comments = comments); } handleCommentSubmit(comment) { comment.id = this.comments.length; this.commentService .add(comment) .subscribe(res => this.comments.push(res)); } }
OnInit
ってのを継承していて、これはLifecycle Hooks
を使うためのもの。今回はReact
でcomponentWillMount
相当する(?あってる?)タイミングでサーバへリクエストを投げたかったので、OnInit
を使用している。以下によくまとまっていた。
constructor
では後述するcommentService
(主にサーバとのAjax通信するサービス)を受け取って、ngOnInit
でポーリングする処理を呼んでいる。handleCommentSubmit
では子コンポーネントのcomment-form
からのデータを受け取りcommentService
に渡すことで、コメントを投稿している。startIntervalFetch()
、add()
ともにobservable
が返ってくるのでsubscribe
してcomments
を書き換えたり、追加したりしている。
comment-list
基本的にはcomment-box
での知識があればほぼほぼ書けた。
- components/comment-list.ts
import { Component, Input } from '@angular/core'; import { CommentItem } from './comment-item'; @Component({ selector: 'comment-list', directives: [CommentItem], template: ` <div class="comment-list"> <div *ngFor="let comment of comments"> <comment-item [author]="comment.author" [text]="comment.text"></comment-item> </div> </div> ` }) export class CommentList { @Input() comments; }
新しいポイントは以下。
@Input()
親コンポーネントからディレクティブに対する入力は@input()
で定義する必要がある。
*ngFor
みたまんま繰り返しを行う記述でstructural directives
と呼ぶらしい。他には、*ngIf="condition"
等が用意されている。ちょっと前まではlet
ではなく#
と書いていたようで、#
を使うと現在では動作はするが、#" inside of expressions is deprecated. Use "let" instead!
という警告がでる。
ここではcomments
の要素数分繰り返し<comment-item></comment-item>
に渡している。
詳細は以下を見ると良さそう。
comment-form
フォーム部分については以下のようになった。
- components/comment-form.ts
import { Component, Output, EventEmitter } from '@angular/core'; import { CommentList } from './comment-list'; import { CommentService } from '../service/comment'; import Comment from '../interfaces/comment'; @Component({ selector: 'comment-form', template: ` <div class="comment-form"> <input type="text" value={{author}} (keyup)="onAuthorChange($event)" placeholder="Your name" /> <input type="text" value={{text}} (keyup)="onTextChange($event)" placeholder="Say something..." /> <input type="submit" value="Post" (click)="handleSubmit()" /> </div> `, styles: [` .comment-form { margin-top: 50px; } `] }) export class CommentForm { @Output() onCommentSubmit: EventEmitter<any> = new EventEmitter(); public author: string public text: string onAuthorChange(e: KeyboardEvent): void { this.author = (<HTMLInputElement>event.target).value; } onTextChange(e: KeyboardEvent): void { this.text = (<HTMLInputElement>event.target).value; } handleSubmit(): void { const author = this.author.trim(); const text = this.text.trim(); if (!text || !author) return; this.onCommentSubmit.emit({ author, text }); this.text = ''; this.author = ''; } }
新しい要素は以下。
styles
コンポーネントのスタイルはstyle
で指定できる。
styles: [` .comment-form { margin-top: 50px; } `]
Angular2
はScoped CSS
を実現しているという話しをちらっと聞いていたんだけどどのように実現しているか見たら以下のようになっていた。
スコープはどのように実現しているか確認すると以下のようになっていた。attributeを使用して擬似的にスコープを実現しているように見える。
- html
<comment-form _nghost-dkv-3=""> <div _ngcontent-dkv-3="" class="comment-form"> <input _ngcontent-dkv-3="" placeholder="Your name" type="text" ng-reflect-value=""> <input _ngcontent-dkv-3="" placeholder="Say something..." type="text" ng-reflect-value=""> <input _ngcontent-dkv-3="" type="submit" value="Post"> </div> </comment-form>
- css
.comment-form[_ngcontent-dkv-3] { margin-top: 50px; }
またstyleUrls
によりcss
ファイルを使用することもできるっぽい。
styleUrls: ['app/style.css'],
バインディング
<input type="text" value={{author}} (keyup)="onAuthorChange($event)" placeholder="Your name" />
のようにしてkeyup
イベントで値をセットし、反映している。この辺りは以下のように書くことで1way
か2way
か選択できるっぽい。
- 2way binding
<input type="text" [(ngModel)]=”author” placeholder=”Your name” />
- 1way binding
<input type="text" value={{author}} placeholder="Your name" />
また、change
だとハンドラが呼ばれるのはblur
のタイミングっぽくて、React
でいうonChange
のタイミングで呼びたかったらkeyup
イベントを使うのが良さそうに見える。
@output
@input()
があったように出力は@output()
を使用するっぽい。ここではフォームの内容をEventEmitter
を使って<comment-box></comment-box>
まで引き上げている。多分、<comment-form></comment-form>
に直接CommentService
をInject
することもできるんだろうけど、Dumb Component
とSmat Component
を意識するとこういうことになるんじゃないかと思う。
comment-item
特筆すべき箇所はないが、この時点でmarked
を追加した。
あと、タグを使用する場合は[innerHTML]
を使用するっぽい。
- components/comment-item.ts
import { Component, Input } from '@angular/core'; import { parse } from 'marked'; @Component({ selector: 'comment-item', template: ` <div class="comment"> <h2 class="comment-author"> {{author}} </h2> <span [innerHTML]="rawMarkup()"></span> </div> ` }) export class CommentItem { @Input() author: string @Input() text: string rawMarkup():string { return parse(this.text, { sanitize: true }); }
markedの追加
以下の手順で行った。
npm i -S marked
node_modules/.bin/typings -GS install marked --source dt
system-config.jsに追加
System.config({ map: { ...省略 'marked': 'node_modules/marked', }, packages: { ...省略 'marked': { main: 'index' }, }
comment-service
CommentService
に使用するHttpモジュール
については以下を参考にした。
- service/comment.ts
import { Injectable } from '@angular/core'; import Comment from '../interfaces/comment'; import { Http, Request, Response } from '@angular/http'; import { Observable } from 'rxjs'; import 'rxjs/add/operator/map'; @Injectable() export class CommentService { constructor(private http: Http) { } startIntervalFetch(): Observable<Comment[]> { return Observable.interval(1000) .flatMap(() => this.http.get("http://localhost:3001/comments")) .map(res => res.json() as Comment[]); } add(comment: Comment): Observable<Comment> { return this.http .post("http://localhost:3001/comments", comment) .map(res => res.json() as Comment); } }
startIntervalFetch()
では1秒ごとにサーバにコメントを取りに行って、add()
でコメントを追加している。
この時点でjson-server
をインストールしてやり取りはそっちと行っている。
@Injectable()
@Injectable()
でDI
を可能にする。
使う側(components/comment-box.ts)は以下のようになる。
@Component({ providers: [CommentService], }) export class CommentForm { constructor(private commentService: CommentService) { }
package.json
package.jsonは最終的に以下。
{ "name": "ng2-comment-tutorial", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "concurrently \"npm run tsc:w\" \"npm run lite\" \"npm run json\"", "tsc": "tsc", "tsc:w": "tsc -w", "lite": "lite-server", "typings": "typings", "json": "json-server --watch db.json --port 3001", "postinstall": "typings install" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@angular/common": "^2.0.0-rc.3", "@angular/compiler": "^2.0.0-rc.3", "@angular/core": "^2.0.0-rc.3", "@angular/http": "^2.0.0-rc.3", "@angular/platform-browser": "^2.0.0-rc.3", "@angular/platform-browser-dynamic": "^2.0.0-rc.3", "es6-shim": "^0.35.1", "marked": "^0.3.5", "reflect-metadata": "^0.1.3", "rxjs": "^5.0.0-beta.9", "symbol-observable": "^1.0.1", "systemjs": "^0.19.31", "zone.js": "^0.6.12" }, "devDependencies": { "concurrently": "^2.1.0", "json-server": "^0.8.14", "lite-server": "^2.2.0", "typescript": "^1.8.10", "typings": "^1.3.0" } }
まとめ
- 1.xのときよりシンプルになったような印象を受ける
- 二の足を踏んでいたけど、導入障壁も高くないように感じた
- ただ、逐一追っているわけではないがまだ破壊的な変更が入っているような話しを耳にする
- 次に触るのはもう少し安定してからでいいかな、と感じた
- その際はなにか独立したコンポーネント、例えばreact-resizable-and-movableあたりを移植してみたい
Singleton store脳
な自分には状態を一元管理するような方法があるとありがたいng-redux
とか?あまりいい評判は聞かない。。。
- テスト周りがわからん。rc.4から
jasmine
に限らずmocha
も使用できるようになった?