JavaScript
AngularJS
angular
reactjs
Angular2

Angular2 を利用して React の公式Tutorial を実行した際の詳細手順。gulp + browserify も利用。

More than 1 year has passed since last update.

初めに

もう少しで ng-japan なので予習として Angular2 の公式ページにある 5 Min Quickstart および Case Study の Tour of Heros を試してみました。
両方とも英語ではあるのですが、とても分かりやすい Tutorial になっていて、楽しんで進めることができました。せっかくなのでもう少し遊んでみようと思って、過去に以下の記事で書いた React Tutorial の続編として Augular2 で React Tutorial を実行してみようと思いました。Angular2 も React も同じコンポーネント志向ですが、同じものを実装したらどういう風に違ってくるのかが比較できて楽しいかと思って始めました。

過去の React Tutorial シリーズは以下です。
* React.js の 公式Tutorial を gulp を利用して簡単に実行できる環境を作って、ES6も試した
* React.js の 公式Tutorial を Redux を利用して書き直した。また Mocha + power-assert を利用したテストも追加

2016年3月時点の最新環境で実行してます。

ソースファイル

以下の GitHub で公開しています。

https://github.com/ma-tu/react-tutorial-with-angular2

記事の構成

長文な記事になっていますが、以下の構成でまとめています。

  1. Angular2 公式の 5 Min Quick start をスタートとします。
  2. 上記の成果物をベースに gulp + browserify + tsify を利用して動作させるまでの手順をまとめています。
  3. 上記の成果物をベースに React Tutorial を進めるためのサーバー環境などの段取り手順をまとめています。
  4. 上記の成果物をベースに React Tutorial の流れに従い Angular2 を利用して実装していきます。

[ start-react-tutorial ] のブランチを利用して 4 から始めることも可能です。

それでは始めましょう。

1. Angular2 公式の 5 Min Quick start を実行

まずは Angular 公式の 5 Min Quick start を実施します。

[ 1. Angular2 公式の 5 Min Quick start を実行 ] の作業完了時点のソースは以下で公開しています。

https://github.com/ma-tu/react-tutorial-with-angular2

の [ 5-min-quickstart-ends ] ブランチ

git clone https://github.com/ma-tu/react-tutorial-with-angular2.git
cd react-tutorial-with-angular2
git checkout 5-min-quickstart-ends
npm install

2. gulp + browserify + tsify 対応

依存ライブラリ追加

必要な依存ライブラリを追加します。以下コマンドを実行します。

npm install --save-dev browserify gulp tsify vinyl-source-stream

tsify は Browserify で TypeScript のコンパイルを行うためのプラグインになります。

tsconfig.json の編集

Browserify は モジュールシステムに CommonJS を利用している JavaScript をブラウザで動かせるようにするためのシステムです。

一方 Angular2 の 5 Min Quickstart は モジュールシステムに SystemJS を利用しています。

  • モジュールシステムを CommonJS に変更します。

    "module" : "system""module" : "commonjs" に変更します。

  • TypeScript のコンパイルは tsify を利用して、Browserify と一緒に実行するので保存時のコンパイルを行わないようにします。

    "compileOnSave": false を追加します。

tsconfig.json(一部)
"compilerOptions": {
  ///
  "module": "commonjs",
  ///
},
"compileOnSave": false,

gulpfile.js の追加

以下の gulpfile.js を追加します。

gulpfile.js
var gulp = require('gulp');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var tsify = require('tsify');

//Browserify
gulp.task('browserify', function() {
  browserify('./app/main.ts')
    .add("typings/browser.d.ts")
    .plugin(tsify)
    .bundle()
    .on("error", function (err) {console.log("ERROR: " + err.message);})
    .pipe(source('bundle.js'))
    .pipe(gulp.dest('./'))
});

//Watch Task
gulp.task('watch', function() {
  gulp.watch('./app/**/*.ts', ['browserify'])
});

補足説明します。

  • ./app/main.ts を Browserify の Entry Point とします。
  • add("typings/browser.d.ts") は 現状 tsify が tsconfig の files を参照していないため必要と以下 issue にあり対応しています。TypeScript の定義ファイルを追加しています。
    tsify issue
  • tsify plugin を利用して TypeScript をコンパイルします。
  • Browserify した結果ファイルを ./bundle.js に出力します。
  • ./app 以下の ts ファイルに変更があったら browseriry タスクを実行します。

以下のURLが参考になります。

gulp + browserify + tsify を利用してTypeScript コンパイル環境を作る

package.json の scripts の編集

npm start 等で Browserify して watch がスタートするように package.json の scripts を編集します。

package.json(一部)
"scripts": {
  "start": "gulp browserify && concurrently \"gulp watch\" \"npm run lite\" ",
  "watch": "gulp watch",
  "browserify": "gulp browserify",
  "lite": "lite-server",
  "typings": "typings",
  "postinstall": "typings install"
},

補足説明します。
* && は前後のタスクを直列に実行します。今回の場合は Browserify してから gulp watch と lite-server 起動を並列で実行します。

index.html の編集

Browserify により 不要になった script の記述を削除します。代わりに Browserify によって出力された bundle.js を読み込むようにします。

bundle.js 内で <my-app> タグを参照しているために body タグ内で読み込む必要があります。

angular2-polyfills.js の記述も消したいところなのですが、こちらは現在必要なようです。

index.html
<html>
  <head>
    <title>Angular 2 QuickStart</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">    
    <link rel="stylesheet" href="styles.css">

    <script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
  </head>

  <body>
    <my-app>Loading...</my-app>
    <script src="bundle.js"></script>
  </body>
</html>

.gitignore の作成

.gitignore のファイルがない場合は作成します。

./gitignore
node_modules
typings
app/*.js
app/*.map
bundle.js

実行してみます

npm start

app/app.component.ts の template などを編集し、自動的にページが更新されることを確認します。

これで [ 2. gulp + browserify + tsify 対応 ] の作業は完了です。

3. React Tutorial を行っていくためのサーバー環境の準備

React.js の 公式 Tutorial は 途中で Ajax を利用したサーバー環境が必要になります。

この環境を先に準備しておきます。

この作業完了時点のソースは以下で公開しています。

https://github.com/ma-tu/react-tutorial-with-angular2

の [ start-react-tutorial ] ブランチ

git clone https://github.com/ma-tu/react-tutorial-with-angular2.git
cd react-tutorial-with-angular2
git checkout start-react-tutorial
npm install

server.js および comments.json の配置

React.js の Tutorial レポジトリ の server.js および comments.json を ./ 以下に配置します。

この server.js を exrepss で実行してサーバー環境を起動します。

依存ライブラリ追加

必要な依存ライブラリを追加します。以下コマンドを実行します。

npm install --save-dev express body-parser gulp-nodemon 

補足説明します。
* React Tutorial では express と body-parser を利用しています。

* 自動リロードのため gulp-nodemon を追加しました。

index.html の移動

server.js は ./public フォルダをルートディレクトリとして起動するので ./public フォルダを作成して index.html を ./public 以下に移動します。

gulpfile の修正

./server.js を実行する 'server' タスクを追加します。自動リロードを行うために gulp-nodemon を利用します。

また server.js のルートディレクトリの変更に合わせて Browserify の変換後ファイルの出力先を ./public/bundle.js に変更します。

gulpfile.json(一部)
var nodemon = require('gulp-nodemon');

//Browserify
gulp.task('browserify', function() {
  browserify('./app/main.ts')
    .add("typings/browser.d.ts")
    .plugin(tsify)
    .bundle()
    .on("error", function (err) {console.log("ERROR: " + err.message);})
    .pipe(source('bundle.js'))
    .pipe(gulp.dest('./public'))
});

//Server
gulp.task('server', function () {
  nodemon({ script: './server.js',
            ext: 'html js'})
   .on('restart', function () {
     console.log('restarted!')
   })
});

package.json の start タスクを 修正

npm start で lite-server を利用していた部分を gulp server に変更します。

package.json(一部)
"start": "gulp browserify && concurrently \"gulp watch\" \"gulp server\" ",

angular2-polyfills.js のコピー

node_modules/angular2/bundles/angular2-polyfills.js のファイルを ./public/lib フォルダを作成してコピーします。

また index.html の angular2-polyfills.js の読込部分のパスを合わせて修正します。合わせて index.html の styles.css の読込部分を削除します。

index.html
<html>
  <head>
    <title>Angular 2 QuickStart</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">    

    <script src="lib/angular2-polyfills.js"></script>
  </head>

  <body>
    <my-app>Loading...</my-app>
    <script src="bundle.js"></script>
  </body>
</html>

補足説明します。
* server.js の利用により public フォルダがルートディレクトリになるため node_modules 以下のファイルは参照できなくなるため public/lib 以下に移動しています。
* styles.css は現時点では存在しないためエラーになるので削除しました。

実行してみます

npm start

起動後ブラウザで http://localhost:3000 にアクセスします。

「My First Angular 2 App」が表示されていれば成功です。

これで 3. React Tutorial を行っていくためのサーバー環境の準備 は完了です。

4. React Tutorial を Angular2 を利用して実行します。

ここままでの作業完了時点のソースは以下で公開しています。

https://github.com/ma-tu/react-tutorial-with-angular2

の [ start-react-tutorial ] ブランチ

git clone https://github.com/ma-tu/react-tutorial-with-angular2.git
cd react-tutorial-with-angular2
git checkout start-react-tutorial
npm install

ここからがいよいよ本題です。

以下の React Tutorial を Angular2 を使って実装していきます。React Tutorial を合わせて参照してください。
* React Tutorial公式
* React v0.14 チュートリアル【日本語翻訳】

ここからは React Tutorial のトピック毎に作業を進めていきます。またトピックごとに GitHub の コミットを分けていますので途中で分からなくなった場合に利用ください。
以下のコマンドを実行して作業を開始しましょう。

途中で必要に応じて CTRL + C などで停止し再起動しながら進めてください。Reload がうまくいかない場合なども再起動により改善することがあります。

npm start 

React Tutorial の以下トピックは上記の準備で対応済なので 「Your first component: はじめてのコンポーネント」からスタートとなります。
* Running a server: サーバーを動かす
* Getting started: スタートしよう

Your first component: はじめてのコンポーネント

CommentBoxコンポーネントを作ります。

app/comment-box.component.ts ファイルを作成します。

app/comment-box.component.ts
import {Component} from 'angular2/core';

@Component({
    selector: 'my-comment-box',
    template: 
    `
    <div className="commentBox">
      Hello, world! I am a CommentBox.
    </div>
    `
})

export class CommentBoxComponent {}

補足説明します。
* comment-box.component.ts が最初のコンポーネントのファイルです。
* selector: 'my-comment-box' が 他のコンポーネントの template で CommentBox コンポーネントを利用する時のタグ名になります。


app/app.component.ts に CommentBoxComponent の import を追加して、template を以下のように修正します。また directives に CommentBoxComponent を指定します。

app/app.component.ts
import {Component} from 'angular2/core';
import {CommentBoxComponent} from './comment-box.component'

@Component({
    selector: 'my-app',
    template: '<my-comment-box></my-comment-box>',
    directives: [CommentBoxComponent]
})
export class AppComponent { }

補足説明します。
* main.ts の bootstrap で App コンポーネントを起動していて、この App コンポーネントから CommentBox コンポーネントを起動します。
* app.component.ts の template の <my-comment-box></my-comment-box><my-comment-box/> と書きたいところですが、現時点ではダメなようです。
* directives の CommentBoxComponent を忘れると、template 中のカスタムタグが無視されますので忘れずに記述します。

実行してブラウザ上に「Hello, world! I am a CommentBox.」が表示されていれば成功です。

Composing components: コンポーネントを作る

CommentList と CommentForm のコンポーネントを作成します。

app/comment-list.component.tsapp/comment-form.component.ts ファイルを作成します。

app/comment-list.component.ts
import {Component} from 'angular2/core';

@Component({
    selector: 'my-comment-list',
    template: 
    `
    <div className="commentList">
      Hello, world! I am a CommentList.
    </div>
    `
})

export class CommentListComponent { }
app/comment-form.component.ts
import {Component} from 'angular2/core';

@Component({
    selector: 'my-comment-form',
    template: 
    `
    <div className="commentForm">
      Hello, world! I am a CommentForm.
    </div>
    `
})

export class CommentFormComponent { }

上記のコンポーネントを利用するように CommentBox コンポーネントを修正します。

app/comment-box.component.ts に CommentListComponent と CommentFormComponent の import を追加して、template を以下のように修正します。また directives に CommentListComponent と CommentFormComponent を指定します。

app/comment-box.component.ts
import {Component} from 'angular2/core';
import {CommentListComponent} from './comment-list.component';
import {CommentFormComponent} from './comment-form.component';

@Component({
    selector: 'my-comment-box',
    template: 
    `
    <div className="commentBox">
      <h1>Comments</h1>
      <my-comment-list></my-comment-list>
      <my-comment-form></my-comment-form>
    </div>
    `,
    directives: [CommentListComponent, CommentFormComponent]
})

export class CommentBoxComponent {}

Your first component: はじめてのコンポーネント と同様なので補足説明は省略します。

実行してブラウザに以下が表示されていれば成功です。

Comments
Hello, world! I am a CommentList.
Hello, world! I am a CommentForm.

Using props: propsを使う

Commentコンポーネントを作成します。

app/comment.component.ts ファイルを作成します。

app/comment.component.ts
import {Component, Input} from 'angular2/core';

@Component({
    selector: 'my-comment',
    template: 
    `
    <div className="comment">
      <h2 className="commentAuthor">
        {{author}}
      </h2>
      {{comment}}
    </div>
    `
})

export class CommentComponent {
  @Input() author: string
  @Input() comment: string
}

補足説明します。
* コンポーネントは親コンポーネントから渡されたデータを利用することができます。
* React の props.children に相当するものは存在しないようです。今回は author と同様に comment もプロパティとして利用する想定としています。
* 親コンポーネントから渡されるべき値は CommentComponent クラスで明示的に指定します。
* Angular2 の Tutorial では @Component の inputs で指定していましたが、今回は @Input() を利用しました。

この @Input() を利用するために import で Input を追加しています。

inputs: ['author', 'comment'] の指定と同じ意味です。
* 渡された author と comment を template 中の {{author}} {{comment}}の指定で、データバインディングして表示します。

このタイミングでは CommentComponent は作成しましたが、まだ利用されていないため表示は変更ありません。

Component Properties: コンポーネントのプロパティ

CommentList コンポーネントから Comment コンポーネントを利用するようにします。author と comment のデータを連携します。

CommentComponent を import して、directives に設定したうえで、<my-comment> タグを記述します。

app\comment-list.component.ts
import {Component} from 'angular2/core';
import {CommentComponent} from './comment.component';

@Component({
    selector: 'my-comment-list',
    template: 
    `
    <div className="commentList">
      <my-comment author="Pete Hunt" comment="This is one comment"></my-comment>
      <my-comment author="Jordan Walke" comment="This is *another* comment"></my-comment>
    </div>
    `,
    directives: [CommentComponent]
})

export class CommentListComponent { }

実行してブラウザに以下が表示されていれば成功です。

Comments

Pete Hunt

This is one comment
Jordan Walke

This is *another* comment
Hello, world! I am a CommentForm.

補足説明します。
* my-comment の属性として CommentComponent で指定した author と comment にデータを連携しています。
* comment は React と異なり 「XMLのような子ノードを通して」 というデータの連携方法が見つからなかったので、プロパティとして渡しています。

Adding Markdown: Markdownを追加する

Markdown Parser の marked を追加します。

marked を利用するために必要な依存ライブラリを追加します。以下コマンドを実行します。

npm install --save marked
npm run typings install marked -- --save --ambient

marked の import を追加します。次に comment を marked で変換する rawMarkup()関数を用意し、comment 用の span タグの innerHTML にてセットします。

app/comment.component.ts
import {Component, Input} from 'angular2/core';
import * as marked from 'marked'

@Component({
    selector: 'my-comment',
    template: 
    `
    <div className="comment">
      <h2 className="commentAuthor">
        {{author}}
      </h2>
      <span [innerHTML]="rawMarkup()"></span>
    </div>
    `
})

export class CommentComponent {
  @Input() author: string
  @Input() comment: string

  rawMarkup():string {
    return marked(this.comment, {sanitize: true});
  }
}

実行してブラウザに以下が表示されていて another の部分が斜体になっていれば成功です。

Comments

Pete Hunt

This is one comment
Jordan Walke

This is another comment
Hello, world! I am a CommentForm.

補足説明します。
* Angular2 には dangerouslySetInnerHTML のようなプロパティは存在しないため innerHTML プロパティにセットします。
innerHTML を利用するためクロスサイトスクリプティングには注意が必要です。
* [innerHTML] は [] でプロパティを囲う事により rawMakup() 関数式の評価結果が渡されます。
* 以下のプロパティの評価は次の通りです。

<example attrA="1+1" attrB="{{1+1}}" [attrC]="1+1"></example>

attrA には "1+1" の文字列が渡されます。

attrB には {{}} の中が 式として評価されて、結果の文字列 "2" が渡されます。

attrC には "1+1" の式の評価結果の数字 2 が渡されます。

こちらの記事が詳しいです。

Angular 2 @Inputのアレコレ

Hook up the data model: データモデルをつなぐ

React Tutorial にはありませんが、ここで Comment の interface を作成しておきます。

app/comment.ts ファイルを作成します。

app/comment.ts
export interface Comment {
  id: number
  author: string
  text: string
}

今までは CommentList コンポーネントで my-comment を直接作成していましたが、comments の配列データを元に作成するように修正します。最終的にはサーバーからデータを取得します。

CommentBoxComponent クラスに Comment インターフェースを import して、Comment の配列を指定します。<my-component-list>[comments]="comments" を追加して CommentList コンポーネントに配列値を渡します。

app/comment-box.component.ts
import {Component} from 'angular2/core';
import {CommentListComponent} from './comment-list.component';
import {CommentFormComponent} from './comment-form.component';
import {Comment} from './comment'

@Component({
    selector: 'my-comment-box',
    template: 
    `
    <div className="commentBox">
      <h1>Comments</h1>
      <my-comment-list [comments]="comments"></my-comment-list>
      <my-comment-form></my-comment-form>
    </div>
    `,
    directives: [CommentListComponent, CommentFormComponent]
})

export class CommentBoxComponent {
  public comments: Comment[] = [
    {id: 1, author: "Pete Hunt", text: "This is one comment"},
    {id: 2, author: "Jordan Walke", text: "This is *another* comment"}
  ];
}

補足説明します。
* [comments]="comments" により CommentList コンポーネントの comments に comments 配列が渡されます。


CommentList コンポーネントについて 親コンポーネントから渡された comments の配列データを繰り返し処理して Comment コンポーネントを作成するように修正します。

Comment インターフェイスを追加して、@Input() で comments を親コンポーネントから受けるようにして、*ngFor で繰り返し表示します。

@Input() を利用できるように Input も import します。

app/comment-list.component.ts
import {Component, Input} from 'angular2/core';
import {CommentComponent} from './comment.component';
import {Comment} from './comment'

@Component({
    selector: 'my-comment-list',
    template: 
    `
    <div className="commentList">
      <my-comment *ngFor="#comment of comments" [author]="comment.author" [comment]="comment.text"></my-comment>
    </div>
    `,
    directives: [CommentComponent]
})

export class CommentListComponent {
  @Input() comments: Comment[]
}

実行してブラウザに前回と同じ結果が表示されていれば成功です。

補足説明します。
* CommentBox コンポーネントで comments を 直接配列データで指定します。
* CommentBox コンポーネントから CommentList コンポーネントに comments というプロパティで配列データを渡します。
* CommnetList コンポーネントでは *ngFor="#comment of comments" により comments の件数分繰り返し処理を行います。

*ngFor は繰り返し処理を指示するディレクティブとなります。

comment of comments は comments の配列データを一つづつ取り出して comment 変数に設定することを行います。

* CommnetList コンポーネントの author および comment には 上記で comment 変数に設定された値の author および text の結果をセットします。

今回の場合は 連携されるデータは文字列なため author={{comment.author}} でも結果は同じです。

Fetching from the server: サーバから取ってくる

React Tutorial の下記の3つのセクションを、今回まとめて対応していきます。

* Fetching from the server: サーバから取ってくる
* Reactive state: リアクティブな状態
* Updating state: stateのアップデート

サーバーとの通信の機能は Angular2 の場合は Http モジュールが提供されていますのでこちらを利用します。

Http モジュールは Http クラスのインスタンスを DI して利用します。また Http モジュールを使うだけなら Component で直接 Http モジュールを利用して処理可能なのですが、Angular ではサービスを分離した上で DI して利用する方法が主流となっています。

急に DI という言葉が出てきましたが、以下の記事で詳しく説明されています。
* Angular2のHttpモジュールを眺めてベストプラクティスを考える
* Angular2のDIを知る

DI は私の理解で乱暴に説明すると、Provider という仕組みにしたがって作成したインスタンスを 自分自身または子コンポーネントに提供する機能で、提供されたインスタンスを利用する場合は特定の変数に Inject して利用する仕組みとなります。

まず Http モジュールの Provider を作成します。

HTTP_PROVIDERS を angular2/http から import したうえで、bootstrap の第2引数に指定します。
この bootstrap で指定した Provider は すべてのコンポーネントから探索可能となります。

app/main.ts
import {bootstrap}    from 'angular2/platform/browser'
import {AppComponent} from './app.component'
import {HTTP_PROVIDERS} from 'angular2/http';

bootstrap(AppComponent, [HTTP_PROVIDERS]);

Angular2 の主流に従い、サーバーからコメントを取得したり、サーバーにコメントを送信したりする部分を CommentService として作成します。

comment.service.ts を作成します。

app/comment.service.ts
import {Injectable} from 'angular2/core';
import {Comment} from './comment'
import {Http, Headers} from 'angular2/http';
import {Observable}     from 'rxjs/Observable';
import 'rxjs/Rx';

@Injectable()
export class CommentService {
  constructor(private http: Http) {
  }

  getCommentsObservable(): Observable<Comment[]> {
    return this.http.get('/api/comments').map(res => res.json() as Comment[])
                                         //.delay(2000)
                                         //.repeat()
  }

  saveCommentObservable(comment: Comment): Observable<Comment[]> {
    const headers = new Headers();
    headers.append('Content-Type', 'application/json');
    return this.http.post('/api/comments', JSON.stringify(comment), {headers:headers})
      .map(res => res.json() as Comment[])
  }
}

補足説明します。
* @Injectable() アノテーションの指定と constructor(private http: Http) の記述で 先ほど bootstrap で指定した Http インスタンスを CommentService に Inject します。
* http.get および http.post の戻り値は Observable<Response> となっています。
* Observable<Response> の戻り値を Observable.map 関数 を使って Observable<Comment[]> に変換します。

Observable.map 関数を利用するために import 'rxjs/Rx' の宣言が必要となります。
* delay(2000).repeat() の指定により 2 秒おきに処理が行われるようになります。ただし 後の保存処理とのタイミングでうまくいかない場合があるのでコメントアウトしています。
* http.post の場合は Content-Type の指定が必要なので追加しています。


CommentBox コンポーネントで CommentService を import して providers で指定します。この providers 指定でインスタンス化したうえで、同じく CommentBox クラスの constructor で Inject して利用します。CommentBox コンポーネントで直接配列データを指定していた部分を CommentService で取得して利用するように修正します。

app/comment-box.component.ts
import {Component} from 'angular2/core';
import {CommentListComponent} from './comment-list.component';
import {CommentFormComponent} from './comment-form.component';
import {CommentService} from './comment.service';
import {Comment} from './comment'

@Component({
    selector: 'my-comment-box',
    template: 
    `
    <div className="commentBox">
      <h1>Comments</h1>
      <my-comment-list [comments]="comments"></my-comment-list>
      <my-comment-form></my-comment-form>
    </div>
    `,
    directives: [CommentListComponent, CommentFormComponent],
    providers: [CommentService]
})

export class CommentBoxComponent {
  comments: Comment[];

  constructor(private _commentService: CommentService) { }

  ngOnInit() {
    this._commentService.getCommentsObservable().subscribe(comments => this.comments = comments)
  }
}

補足説明します。
* @Component の providers に CommentService を指定することによりインスタンスが作成されます。
* constructor(private _commentService: CommentService) により _commentSerivce として Inject されます。
* ngOnInit() はインスタンス化されたときに一度だけ呼び出されます。
* ngOnInit() で CommentService の getCommentsObservable() 関数を呼び出します。戻り値の Observable から subscribe して値を取得します。

実行してブラウザに以下が表示されていれば成功です。

Comments
Pete Hunt
Hey there!
Paul O’Shannessy
React is great!
Hello, world! I am a CommentForm.

Adding new comments: 新しいコメントを追加する

次はフォームを追加しましょう。

app/comment-form.component.ts を以下のように修正します。

app/comment-form.component.ts
import {Component} from 'angular2/core';

@Component({
    selector: 'my-comment-form',
    template: 
    `
    <form className="commentForm" (ngSubmit)="handleSubmit()">
      <input [(ngModel)]="author" placeholder="Your name" />
      <input [(ngModel)]="text" placeholder="Say something..." />
      <input type="submit" value="Post" />
    </form>
    `
})

export class CommentFormComponent { 
  public author: string
  public text: string

  handleSubmit(e) {
    if (!this.author || !this.text) {
       return;
     }

    // TODO: send request to the server         
    this.author = ''
    this.text = ''
  }  
}

補足説明します。
* [(ng-model)]="author"により author 変数との 2way データバインディングとなります。
* (ngSubmit) は onSubmit を取り扱うための Angular2 のディレクティブになります。

これにより onSubmit イベント時の処理を記述します。ここでは handleSubmit() 関数を実行します。
* handleSubmit() の this.author 等には 2way データバインディングにより入力値が格納されています。
* server への送信処理は次のセクションで対応します。ここでは入力欄の初期化のみ行っています。

実行してブラウザに input エリアが 2個追加されて、post ボタンが追加されていたら成功です。

Callbacks as props: propsとしてのコールバック

サーバーとの通信は CommentBox が担当します。より正確に表現すると CommentBox コンポーネントの CommentService が担当します。

CommentBox コンポーネントに サーバーへの保存処理を行う handleCommentSubmit 関数を追加して、<my-comment-form (onCommentSubmit)="handleCommentSubmit($event)> にて
ComponentForm コンポーネントの onCommentSubmit イベントに handleCommentSubmit 関数をバインドします。

app/comment-box.component.ts
import {Component} from 'angular2/core';
import {CommentListComponent} from './comment-list.component';
import {CommentFormComponent} from './comment-form.component';
import {CommentService} from './comment.service';
import {Comment} from './comment'

@Component({
    selector: 'my-comment-box',
    template: 
    `
    <div className="commentBox">
      <h1>Comments</h1>
      <my-comment-list [comments]="comments"></my-comment-list>
      <my-comment-form (onCommentSubmit)="handleCommentSubmit($event)"></my-comment-form>
    </div>
    `,
    directives: [CommentListComponent, CommentFormComponent],
    providers: [CommentService]
})

export class CommentBoxComponent {
  comments: Comment[];

  constructor(private _commentService: CommentService) { }

  ngOnInit() {
    this._commentService.getCommentsObservable().subscribe(comments => this.comments = comments)
  }

  handleCommentSubmit(comment) {
    this._commentService.saveCommentObservable(comment).subscribe(comments => this.comments = comments)
  }  
}

補足説明します。
* handleCommentSubmit 関数では CommentService を呼び出して保存処理を行います。保存処理の結果が帰ってくるのでそれを comments に設定します。
* (onCommentSubmit) はイベントのバインディングを指定します。
* $event は Angular のイベントオブジェクトが格納される変数です。


CommentForm コンポーネントで onCommetSubmit を受け取り、handleSubmit() 関数で呼び出すように修正します。

Output と EventEmitter を import して、@Output() を指定して onCommitSubmit を受け取れるようにします。そして handleSubmit() 関数から呼び出します。

app/comment-form.component.ts
import {Component, Output, EventEmitter} from 'angular2/core';

@Component({
    selector: 'my-comment-form',
    template: 
    `
    <form className="commentForm" (ngSubmit)="handleSubmit()">
      <input [(ngModel)]="author" placeholder="Your name" />
      <input [(ngModel)]="text" placeholder="Say something..." />
      <input type="submit" value="Post" />
    </form>
    `
})

export class CommentFormComponent { 
  @Output() onCommentSubmit: EventEmitter<any> = new EventEmitter();

  public author: string
  public text: string

  handleSubmit() {
    if (!this.author || !this.text) {
       return;
     }

    this.onCommentSubmit.emit({author: this.author, text: this.text})
    this.author = ''
    this.text = ''
  }  
}

補足説明します。

* @Output() onCommentSubmit: EventEmitter<any> = new EventEmitter(); で onCommentSubmit を受け取れるように指定します。
* new EventEmitter() は必要です
* @Output() および EventEmitter を利用するために import を追加します。
* this.onCommentSubmit.emit() により onCommentSubmit にバインドされている処理を呼び出します。

Optimization: optimistic updates: 最適化: 快適なアップデート

サーバーとの通信は非同期処理であり、時間がかかる可能性もあるため(今回のケースでは時間はかかりませんが)保存処理を行う前に仮データを comments に追加する処理を行います。

handleCommentSubmit 関数内で追加しようとしている comment オブジェクトの id に仮の値を登録したうえで this.coments に追加します。サーバーに保存処理後、サーバーからの戻値で this.comments は再設定されます。

app/comment-box.component.ts(一部)
export class CommentBoxComponent {
  comments: Comment[];

  constructor(private _commentService: CommentService) { }

  ngOnInit() {
    this._commentService.getCommentsObservable().subscribe(comments => this.comments = comments)
  }

  handleCommentSubmit(comment) {
    comment.id = Date.now();
    this.comments = this.comments.concat([comment]);
    this._commentService.saveCommentObservable(comment).subscribe(comments => this.comments = comments)
  }  
}

これで作業はすべて完了です。

まとめ

すべての作業完了時点のソースは以下で公開しています。

https://github.com/ma-tu/react-tutorial-with-angular2

の [ finish-react-tutorial ] ブランチ

git clone https://github.com/ma-tu/react-tutorial-with-angular2.git
cd react-tutorial-with-angular2
git checkout finish-react-tutorial
npm install

以上です。いかがだったでしょうか?

想像以上に大作(長文)となってしまいましたが、Angular2 を理解するための(または React を理解するための)参考になれば幸いです。

セクションごとに コミットを分けていますので確認いただけますと幸いです。

同記事を作成するにあたり様々な記事を参考にしました。ありがとうございました。