Angular2のRouting, Formsの簡単なサンプル。ついでにFlux。(2.0)

**(追記)2.0に対応しました。(2016/09/15)**

[Angular2の開発環境、俺ならこうやる 2016年7月25日版(2.0)](http://qiita.com/ovrmrw/items/56364a4b673c03e20bba)のリポジトリを基本にしつつ、RoutingとFormsを簡単に理解できるようなサンプルを作りました。

今回のGitHubリポジトリ→[ovrmrw/ng2-heroes-editor](https://github.com/ovrmrw/ng2-heroes-editor)

[https://ovrmrw-ng2-heroes-editor-e87d0.firebaseapp.com/](https://ovrmrw-ng2-heroes-editor-e87d0.firebaseapp.com/)は実際にこのリポジトリをデプロイしたものです。

## Routingの設定
ネーミングがわかりにくいのですが、今回のサンプルはPage1はHero List、Page2はAdd Hero/Edit Heroの構成です。
Page2に入るとき、idが渡されればEditing、渡されなければAddingと判断します。
[公式チュートリアルのRoutingの章](https://angular.io/docs/ts/latest/tutorial/toh-pt5.html)を読むと理解が深まります。

```js:src/app/app.routing.ts
// 抜粋
const appRoutes: Routes = [
  {
    path: '',
    redirectTo: '/page1',
    pathMatch: 'full'
  },
  {
    path: 'page1',
    component: Page1Component
  },
  {
    path: 'page2',
    component: Page2Component
  },
  {
    path: 'page2/:id',
    component: Page2Component
  }
];
```

Page2のconstructorでActivatedRouteのparamsを受け取り、EditingかAddingの条件分岐をしています。

```js:src/page2/page2.component.ts
// 抜粋
  constructor(
    public service: Page2Service,
    public route: ActivatedRoute,
    public router: Router,
    public cd: ChangeDetectorRef,
    public el: ElementRef
  ) { }

  ngOnInit() {
    this.route.params.forEach(async (params: Params) => {
      const heroes: Hero[] = await this.service.heroes$.take(1).toPromise();
      if (params['id']) { // Editing Mode
        const id: number = +params['id'];
        const selectedHero: Hero | undefined = heroes.find(hero => hero.id === id);
        if (selectedHero) {
          this.hero = selectedHero;
        } else {
          alert('no hero for the explicit id.');
          this.router.navigate(['/page1']);
        }
      } else { // Adding Mode        
        const newId: number = heroes.length > 0 ? lodash.maxBy(heroes, 'id').id + 1 : 1;
        this.hero = new Hero();
        this.hero.id = newId;
        this.isAdding = true;
      }
      this.cd.markForCheck();
    });
  }
```

## Formsを使う
`@angular/forms` の使い方は僕自身まだよくわかっていませんが、[公式ドキュメント](https://angular.io/docs/ts/latest/guide/forms.html)を一通り読んで書いたみたのがこちら。

```js:src/page2/page2.component.ts
// 抜粋
@Component({
  selector: 'my-page2',
  template: `
    <h3>{{modeName}}</h3>
    <form *ngIf="hero" (ngSubmit)="onSubmit()" #heroForm="ngForm">
      <div class="form-group row">
        <label for="id" class="col-xs-2 col-form-label">Id: </label>
        <div class="col-xs-10">
          <input class="form-control" type="number" id="id" [(ngModel)]="hero.id" name="id" #id="ngModel" required [disabled]="!isAdding" #spy>
          <div [hidden]="id.valid || id.pristine" class="alert alert-danger">
            Id is required
          </div>       
          <pre>className: {{spy.className}}</pre>   
        </div>
      </div>
      <div class="form-group row">
        <label for="name" class="col-xs-2 col-form-label">Name: </label>
        <div class="col-xs-10">
          <input class="form-control" type="text" id="name" [(ngModel)]="hero.name" name="name" #name="ngModel" required #spy>
          <div [hidden]="name.valid || name.pristine" class="alert alert-danger">
            Name is required
          </div>
          <pre>className: {{spy.className}}</pre>          
        </div>
      </div>
      <pre>{{hero | json}}</pre>
      <button type="submit" class="btn btn-outline-primary" [disabled]="!heroForm.form.valid">Submit</button>
      <pre>heroForm.form.valid: {{heroForm.form.valid | json}}</pre>
    </form>    
  `,
  styles: [`
    .ng-valid[required] {
      border-left: 10px solid #42A948; /* green */
    }
    .ng-invalid {
      border-left: 10px solid #a94442; /* red */
    }
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})  
```

Componentがすっきりする代わりにtemplateがごちゃごちゃするのが特徴。ただしちゃんとFormsが使えるとValidationが効くようになるので早い段階で慣れた方が良さそうですね。

## Storeを作ってみる
Angular2に限らずSPAをやるときにはデータを如何に管理するかが課題となります。
そして今回作ったStoreがこちら。

```js:src/store/store.ts
import { Injectable } from '@angular/core';
import { Observable, Subject, BehaviorSubject, ReplaySubject } from 'rxjs/Rx';
import lodash from 'lodash';

import { Hero } from '../types';
import { Action, EditHero, AddHero, DeleteHero } from './actions';
import { logger } from '../helper';

interface AppState {
  heroes: Hero[];
}

export class Dispatcher<T> extends Subject<T> { }

@Injectable()
export class Store {
  private stateSubject$: Subject<AppState>;

  constructor(
    dispatcher$: Dispatcher<Action>
  ) {
    const initState: AppState = {
      heroes: initialHeroes
    };
    this.stateSubject$ = new BehaviorSubject(initState);

    Observable
      .zip(
        heroReducer(initState.heroes, dispatcher$),
        (heroes) => {
          return { heroes } as AppState;
        }
      )
      .subscribe(appState => {
        this.stateSubject$.next(appState);
      });
  }

  private get returner$() { return this.stateSubject$.asObservable().map(state => lodash.cloneDeep(state)); }

  get state$() { return this.returner$; }
  get heroes$() { return this.returner$.map(state => state.heroes); }
}

function heroReducer(initHeroes: Hero[], dispatcher$: Observable<Action>): Observable<Hero[]> {
  return dispatcher$
    .scan((heroes: Hero[], action: Action) => {
      const oldHeroes = heroes;
      if (action instanceof EditHero) {
        const editedHero = action.hero;
        heroes = lodash.uniqBy([editedHero, ...heroes], 'id');
      } else if (action instanceof AddHero) {
        const newHero = action.hero;
        heroes = lodash.uniqBy([newHero, ...heroes], 'id');
      } else if (action instanceof DeleteHero) {
        const deleteId = action.id;
        heroes = lodash.reject(heroes, { id: deleteId });
      }
      return lodash.orderBy(heroes, ['id'], ['asc']);
    }, initHeroes);
}

const initialHeroes: Hero[] = [
  { id: 11, name: 'Mr. Nice' },
  { id: 12, name: 'Narco' },
  { id: 13, name: 'Bombasto' },
  { id: 14, name: 'Celeritas' },
  { id: 15, name: 'Magneta' },
  { id: 16, name: 'RubberMan' },
  { id: 17, name: 'Dynama' },
  { id: 18, name: 'Dr IQ' },
  { id: 19, name: 'Magma' },
  { id: 20, name: 'Tornado' }
];
```

こちらのActionsを使ってStoreを叩きます。

```js:src/store/actions.ts
import { Hero } from '../types';

export class EditHero {
  constructor(public hero: Hero) { }
}

export class AddHero {
  constructor(public hero: Hero) { }
}

export class DeleteHero {
  constructor(public id: number) { }
}

export type Action = EditHero | AddHero | DeleteHero;
```

Page1とPage2のserviceからStoreを叩くのですが、`page2.service.ts` の中身はこんな感じ。

```js:src/page2/page2.service.ts
// 抜粋
  constructor(
    public store: Store,
    public dispatcher$: Dispatcher<Action>
  ) { }

  save(hero: Hero, isAdding: boolean) {
    if (typeof hero.id === 'number' && typeof hero.name === 'string') {
      if (isAdding) {
        this.dispatcher$.next(new AddHero(hero));
      } else {
        this.dispatcher$.next(new EditHero(hero));
      }
    } else {
      console.error(hero);
      console.error('type of hero is not allowed to save.');
    }
  }
```

`this.dispatcher$.next(new AddHero(hero));`から`store.ts`の`heroReducer`関数にrxjsのストリームが流れます。
そして巡り巡ってComponentにデータが戻ってくるという単方向の流れなんですね。

```js:src/page2/page2.service.ts
// 抜粋
  get heroes$() { return this.store.heroes$; }
```

```js:src/page2/page2.component.ts
// 抜粋
  this.service.heroes$.take(1).toPromise().then(heroes => { .....
```

更新されたStoreのデータはこのようにserviceを通じてcomponentで取得しています。

この手法はAngularチームのVictor Savkinが書いた[Tackling State](https://vsavkin.com/managing-state-in-angular-2-applications-caf78d123d02#.i6agdshfi)を参考にしており、僕自身これを理解することでrxjsをきちんと理解することができました。

## Page1(Hero List)を眺めてみる
今までPage2ばかり見てきましたが、実はPage1の方がシンプルで理解しやすいです。

```js:src/page1/page1.component.ts
import { Component, OnInit, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
import { Router } from '@angular/router';

import { Page1Service } from './page1.service';
import { Hero } from '../types';

@Component({
  selector: 'my-page1',
  template: `
    <h3>Hero List</h3>
    
    <div>
      <ul class="list-group">
        <li *ngFor="let hero of heroes | async" class="list-group-item">
          <button class="btn btn-outline-primary btn-sm" (click)="editHero(hero)">Edit</button>
          <button class="btn btn-outline-warning btn-sm" (click)="deleteHero(hero)">Delete</button>
          <span>id: {{hero.id}} / name: {{hero.name}}</span>
        </li>
      </ul>
    </div>
    <div>
      <button class="btn btn-outline-primary" (click)="addHero()">Add Hero</button>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class Page1Component implements OnInit {

  constructor(
    public service: Page1Service,
    public router: Router,
    public cd: ChangeDetectorRef
  ) { }

  ngOnInit() { }

  addHero() {
    this.router.navigate(['/page2']);
  }

  editHero(hero: Hero) {
    this.router.navigate(['/page2', hero.id]);
  }

  deleteHero(hero: Hero) {
    this.service.deleteHero(hero);
  }

  get heroes() { return this.service.heroes$; }
}
```

```js:src/page1/page1.service.ts
import { Injectable } from '@angular/core';

import { Store, Dispatcher, Action, EditHero, AddHero, DeleteHero } from '../store';
import { Hero } from '../types';

@Injectable()
export class Page1Service {

  constructor(
    public store: Store,
    public dispatcher$: Dispatcher<Action>
  ) { }

  deleteHero(hero: Hero) {
    this.dispatcher$.next(new DeleteHero(hero.id));
  }

  get heroes$() { return this.store.heroes$; }
}
```

Serviceを通じてComponentがStoreのheroesを受け取り、templateでAsyncPipeを通してる流れがわかるでしょうか。
AsyncPipe内では自動的に`.subscribe()`されるので、このときにStoreからデータが流れてくることになります。(rxjsは基本的にsubscribeしないとデータが流れない)

## Flux的なデータの流れをわかりやすく
まずComponentからStoreへの流れ。(コードを大分簡略化しています)

```js:src/page2/page2.component.ts
onSubmit() {
  this.service.save(this.hero);
}
```

```js:src/page2/page2.service.ts
save(hero: Hero) {
  this.dispatcher$.next(new AddHero(hero));
}
```

```js:src/store/store.ts
function heroReducer(initHeroes: Hero[], dispatcher$: Observable<Action>): Observable<Hero[]> {
  return dispatcher$
    .scan((heroes: Hero[], action: Action) => {
      if (action instanceof AddHero) {
        const newHero = action.hero;
        heroes = lodash.uniqBy([newHero, ...heroes], 'id');
      }
      return lodash.orderBy(heroes, ['id'], ['asc']);
    }, initHeroes);
}
```

```js:src/store/store.ts
Observable
  .zip(
    heroReducer(initState.heroes, dispatcher$),
    (heroes) => {
      return { heroes } as AppState;
    }
  )
  .subscribe(appState => {
    this.stateSubject$.next(appState);
  });
```

このような流れで最終的にStoreの`stateSubject$`にデータが収まります。

---

次にStoreからComponent(template)への流れ。

```js:src/store/store.ts
private get returner$() { return this.stateSubject$.asObservable().map(state => lodash.cloneDeep(state)); }

get heroes$() { return this.returner$.map(state => state.heroes); }
```

```js:src/page1/page1.service.ts
get heroes$() { return this.store.heroes$; }
```

```js:src/page1/page1.component.ts
get heroes() { return this.service.heroes$; }
```

```html:src/page1/page1.component.ts
<li *ngFor="let hero of heroes | async">
```

このような流れでtemplateにデータが流れます。前述しましたがAsyncPipeの中で`.subscribe()`しています。

Component → Service → Store → Service → Component の流れがしっかり出来ていますね。

## 最後に
rxjsのSubjectを駆使するとAngular2で扱いやすいStoreが簡単に作れます。
rxjsがよくわからないという方には[公式ドキュメント](http://reactivex.io/rxjs/manual/overview.html)を一読するのをお勧めします。