Angular2のFormについて（その２）

## 概要
第２部では，[第１部](http://qiita.com/atsukita/items/a1d58781327886257192)で紹介したバリデーションに加え，カスタムバリデーション，変更があったときのWatch，双方向データバインディングを例示し，ionic2に組み込むところまでを見ていきます．

## Custom Validations
独自のバリデーションを作るにはどうすれば良いか，Angular core sourceにあるvalidators.requiredを見てみましょう．

```ts
export class Validators {  
  static required(c: FormControl): StringMap<string, boolean> {  
    return isBlank(c.value) || c.value == "" ? {"required": true} : null;  
  }
```

validatorはFormControlをインプットで受け取り，エラーコードとStringMap\<string, boolean\>を返します．

```ts
function skuValidator(control: FormControl): { [s: string]: boolean } {  
  if (!control.value.match(/^123/)) {  
    return {invalidSku: true};  
  }
}
```

例えば，skuに123という値が必要な時，上記のような関数になります．
このvalidatorは入力値のcontrol.valueの最初の値が123ではなかった場合，invalidSkuというエラーコードを返します．

## 変更をWatchする
formの値はonSubmitが呼ばれて初めてようやく取得できるのが普通ですが，値に変更があったらすぐにチェックしたいですよね．FormGroupもFormControlも，どちらも変更を監視する事ができるEventEmitterを持っています．

変更を監視するためには，まずEventEmitterにアクセスするためにcontrol.valueChangesを呼び，.observer関数でオブザーバー（監視役）を追加します．

```ts
  constructor(fb: FormBuilder) {  
    this.myForm = fb.group({  
      'sku':  ['', Validators.required]  
    });
 
    this.sku = this.myForm.controls['sku'];
 
    this.sku.valueChanges.subscribe(  
      (value: string) => {  
        console.log('sku changed to:', value);  
      }
    );
 
    this.myForm.valueChanges.subscribe(  
      (form: any) => {  
        console.log('form changed to:', form);  
      }
    );
  }

```

ここでは，skuフィールドだけでなく，form全体の変更も監視しています．skuに変更があった場合，文字列のvalueが返ってきますが，フォームの変更点はkey-valueがペアになったオブジェクトが返ってきます．

## ngModel
NgModelはmodelをformにバインドする特別なディレクティブです．ngModelは双方向データバインディングを行うために使われます．一方方向のバインディングと比べて双方向のデータバインディングは複雑で難しくなります．Angular2は通常一方通行のデータフローを持つように設計されていますが，ことフォームに関しては，双方向のバインディングが便利なときがあります．

**ただ，Angular1でng-modelを使っていたからということだけでngModelを使うのは避けましょう．双方向のバインディングを使わない方がいいというのは，それなりの訳があってのことなので，Angular1と同じように多用する必要はありません．**

フォームを少し変更してproductNameというのを追加してみましょう．

```ts
export class DemoFormNgModel {  
  myForm: FormGroup;  
  productName: string;
 
  constructor(fb: FormBuilder) {  
    this.myForm = fb.group({  
      'productName':  ['', Validators.required]  
    });  
  }
 
  onSubmit(value: string): void {  
    console.log('you submitted value: ', value);  
  }
}
```

productNameがただのstringインスタンス変数になっているのがポイントです．

次にngModelをインプットタグに埋め込んでみます．

```ts
        <label for="productNameInput">Product Name</label>  
        <input type="text"  
               id="productNameInput"  
               placeholder="Product Name"  
               [formControl]="myForm.get('productName')"  
               [(ngModel)]="productName">
```

[ ]と( )の両方を使ってngModel属性のまわりを囲っているため，ngModelの表記が奇妙なことになっていますね．これはinputを示す[ ]とoutputを示す( )を両方使って，双方向データバインディングということを明示しているのです．

もうひとつ，formControlもまだ使っていることに気づきます．これはngModelは入力をインスタンス変数にバインディングしているだけで，FormControlとは完全に別物なのですが，その値自体は正しいか否かバリデートしたいため，formControlディレクティブを残しているのです．

```html
<div class="ui info message">  
  The product name is: {{productName}}  
</div>
```

ここまでが，元サイトの記載になります．最後にこれらを使ってionic2のプロジェクトに組み込んでみましょう．

## ionic2のフォームへ組み込む

前提として，Node.jsがインストールされていて，ionic2のプロジェクトをサイトからDLしてあり，ionic serveで動かせるというところまで出来ていれば，組み込むと言っても特に難しいことはありません．
なお，バージョンは以下の通り

- Node.jsのバージョン：v6.7.0
- ionicのバージョン：2.1.0

ionic2のプロジェクトをDLすると通常ならタブレイアウトのサンプルが表示されます．appフォルダ内のページを１つ選び，そのビューとコントローラを以下のように編集します．

### ビューのソース
ビューには３つの入力フォーム（タイトル，本文，コメント）と，２つのボタン（送信ボタン，キャンセルボタン）を用意しました．タイトルにはカスタムバリデーションを加え，titleという文字を入れないとvalidになりません．本文は通常のバリデーションの例になります．
また，普通こんな使い方しませんが，双方向データバインディングの例としてコメントを用意しました．タイトルを入力するとその値でコメントの入力欄が上書きされます．


```thml
<ion-header>
  <ion-navbar>
    <ion-title>
      フォームサンプル
    </ion-title>
  </ion-navbar>
</ion-header>
<ion-content>

  <form [formGroup]="myForm" (ngSubmit)="submitItem(myForm.value)">
    <ion-item>
      <ion-label>タイトル</ion-label>
      <ion-input type="text" id="title" placeholder="title" [formControl]="myForm.controls['title']"></ion-input>
    </ion-item>

    <ion-item>
      <div *ngIf="myForm.controls['title'].hasError('invalidNum')">title must begin with "title"</div>
      <div *ngIf="!myForm.controls['title'].valid">title is invalid</div>
      <div *ngIf="myForm.controls['title'].hasError('required')">title is required</div>
    </ion-item>

    <ion-item>
      <ion-label>本文</ion-label>
      <ion-input type="text" id="description" placeholder="description" [formControl]="myForm.controls['description']"></ion-input>
    </ion-item>
    <ion-item>
      <div *ngIf="!myForm.controls['description'].valid">description is invalid</div>
      <div *ngIf="myForm.controls['description'].hasError('required')">description is required</div>
    </ion-item>

    <ion-item>
      <ion-label>コメント</ion-label>
      <ion-input type="text" id="comment" placeholder="comment" [formControl]="myForm.controls['comment']" [(ngModel)]="comment"></ion-input>
    </ion-item>
    <ion-item>
      <div *ngIf="!myForm.controls['comment'].valid">comment is invalid</div>
      <div *ngIf="myForm.controls['comment'].hasError('required')">comment is required</div>
    </ion-item>

    <ion-item>
      <button ion-button block type="submit" [disabled]="!myForm.valid">送信</button>
      <button ion-button block (click)="cancelItem($event)">キャンセル</button>
    </ion-item>

  </form>
</ion-content>
```

### コントローラのソース
コントローラ側では，必要なコントローラなどをimportし，各ボタンが押されたときに呼ばれるファンクションの定義，カスタムバリデーションのファンクションの定義に加え，フォームの値に変更があった時のWatchを記載しています．送信ボタンが押されるとアラートビューが表示され，記載の内容がポップアップ表示されます．キャンセルボタンを押すと，フォームをリセットするようにしました．
またstring型のcommentには，ビューとコントローラの双方向から値を入力出来るので，双方向データバインディングがどのようなものか分かると思います．

```ts
import { Component } from '@angular/core';
import { NavController, AlertController } from 'ionic-angular';
import { FormBuilder, FormGroup, Validators, FormControl, AbstractControl } from '@angular/forms';

// カスタムバリデーション
function customValidator(control: FormControl): { [s: string]: boolean } {
  if (!control.value.match(/^title/)) {
    return { invalidNum: true };
  }
}
// コンポーネント
@Component({
  selector: 'page-about',
  templateUrl: 'about.html'
})

export class AboutPage {
  myForm: FormGroup;
  // commentは双方向データバインディングするため、string型
  comment: string;

  constructor(public fb: FormBuilder, public alertCtrl: AlertController) {
    this.createForm();
  }

  // フォームビルダーを使ってフォーム作成
  createForm() {
    this.myForm = this.fb.group({
      'title': ['', Validators.compose([Validators.required, customValidator])],
      'description': ['', Validators.required],
      'comment': ['', Validators.required],
    });

    // titleをwatch
    this.myForm.controls['title'].valueChanges.subscribe(
      (value: string) => {
        console.log('title changed to:', value);
        // titleで入力した値をcommentに入れる
        this.comment = value;
      }
    );

    // descriptionをwatch
    this.myForm.controls['description'].valueChanges.subscribe(
      (value: string) => {
        console.log('description changed to:', value);
      }
    );

    // commentをwatch
    this.myForm.controls['comment'].valueChanges.subscribe(
      (value: string) => {
        console.log('comment changed to:', value);
      }
    );
        // フォーム全体の変更をwatch
    this.myForm.valueChanges.subscribe(
      (form: any) => {
        console.log('form changed to:', form);
      }
    );
  }

  // キャンセルを押したら値をクリアするためフォームを再作成
  cancelItem(_event) {
    this.createForm();
    // submitItemが呼ばれるのを防止
    _event.preventDefault();
  }

  // 送信ボタンを押し、アラートビューを表示
  submitItem(_data) {
    let alert = this.alertCtrl.create({
      title: _data.title,
      subTitle: '本文：' + _data.description + '<br>コメント：' + _data.comment,
      buttons: [{
        text: 'OK',
        handler: data => {
          this.createForm();
          console.log('OK clicked');
        }
      }]
    });
    alert.present();
  }
}
```