Angularリアクティブフォームまとめ

# はじめに
ReactiveFormこそAngularの花的な話をどこかで見た気がしたから軽くまとめる。

# リアクティブフォームとは
>リアクティブフォーム は、時間とともに入力値が変わるフォームを扱うためのモデル駆動なアプローチを提供します。
>このガイドでは、シンプルなフォームコントロールの作成と更新から、グループ内の複数コントロールの使用、フォームのバリデーション、高度なフォームの実装の方法を説明します。

[Angular日本語ドキュメント](https://angular.jp/guide/reactive-forms)によると上記の通りらしい。
使った感じ、入力フォームとか動的にNG出したいときとかに使えそう。
[Angular日本語ドキュメント](https://angular.jp/guide/reactive-forms)をなぞりながら簡単にまとめる。

# 前提条件
Angular使えてCLIも使える前提。
自分はmac(10.12.6)でやった。
Anugularは6。

# 準備
とりあえずCLIでプロジェクト生成する。

``` bash
$ ng new reactive-form-test
```

生成された`app.module.ts`に下記を追加する。
やることはReactiveFoms用にmoduleをインポートするだけ。

```javascript
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [
    // other imports ...
    ReactiveFormsModule
  ],
})
export class AppModule { }
```

# 使ってみる
生成されてる`app.component.html`と`app.component.ts`を下記の通りに変える。

```javascript
import { Component } from '@angular/core';
import { FormControl,Validators } from '@angular/forms';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  public control = new FormControl('', [
    Validators.required
  ]);
}
```

```html
入力欄：<input type="text" [formControl]="control" required>
<div *ngIf="control.invalid && (control.dirty || control.touched)">
  <span *ngIf="control.hasError('required')">必須です。</span>
</div>
```

## やってること説明
とりあえず上記サンプルは[ここから](https://angular-hvkrw9.stackblitz.io)動きを確認できる。
やっていることはフォームの必須チェック。
一旦テキストボックスにカーソル入れた後、フォーカスアウトをすると必須のメッセージが表示される。
htmlでdivを入れているのは、フォーカスが当たるまで、メッセージを表示しないようにするため。

>`<div *ngIf="control.invalid && (control.dirty || control.touched)">`

これが無いと、初期表示時未入力 = バリデーションNGということでいきなりメッセージが出てくる。
大抵の場合、それは本意じゃ無いと思うから上を入れておくのが無難だと思う。

### おまけ：ngModelで代用得してみる。
```javascript
public requiredNg = false;
public text = '';

public valid(target: string) {
  this.requiredNg = target.length === 0;
}
```
```html
入力欄：<input type="text" [(ngModel)]="text" (blur)="valid($event.target.value)"><br>
<span *ngIf="requiredNg">必須です。</span>
```

やっていることとしては、テキストボックスの内容を双方向でバインドして、フォーカスアウト時に長さをチェックするということ。
上の様にすればできなくは無いけど、リアクティブフォームを使ったときに比べるとわかりにくいし冗長な気がする。

# ちょっと使いやすくする
リアクティブフォームは使いやすい気がするけど、個人的に使いやすいようにちょっと変える。
理由としてはチェックしたいフォームが複数になるとhtmlが見にくくなっていくから。
おまけとして、formControlをグループにまとめる。
とりあえずエラーメッセージをコンポーネントを作るところから始める。
コンポーネント作成は`ng g component ng-message`でOK。

```html
<div>
  ユーザID：<input type="text" formControlName="userID" required>
  <div *ngIf="userID.invalid && (userID.dirty || userID.touched)">
    <div *ngIf="userID.hasError('required')">
      必須です。
    </div>
    <div *ngIf="userID.hasError('maxlength')">
      最大5文字です。
    </div>
  </div>
</div>
<div>
  パスワード：<input type="text" formControlName="password" required>
    <div *ngIf="password.invalid && (password.dirty || password.touched)">
    <div *ngIf="password.hasError('required')">
      必須です。
    </div>
    <div *ngIf="password.hasError('minlength')">
      最低8文字です。
    </div>
  </div>
</div>
<div>
  e-mail：<input type="text" formControlName="email" required>
    <div *ngIf="email.invalid && (email.dirty || email.touched)">
    <div *ngIf="email.hasError('email')">
      e-mailアドレスのフォーマットにしてください。(xxx@hogehoge.com)
    </div>
  </div>
</div>
```

## 個人的に見やすくした結果
結果から書くと下記の様にした。

**app.component.html**

```html
<form [formGroup]="formGroup">
  <div>
    <div>
      ユーザID：<input type="text" formControlName="userID" required>
      <app-ng-message [form]="userID"></app-ng-message>
    </div>
    <div>
      パスワード：<input type="text" formControlName="password" required>
      <app-ng-message [form]="password"></app-ng-message>
    </div>
    <div>
      e-mail：<input type="text" formControlName="email" required>
      <app-ng-message [form]="email"></app-ng-message>
    </div>
  </div>
</form>
```

**app.component.ts**

```javascript
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators, ValidatorFn, AbstractControl } from '@angular/forms';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
  public formGroup = new FormGroup({
    userID: new FormControl('', [
      Validators.required, //必須
      Validators.maxLength(5) // 最大5文字
    ]),
    password: new FormControl('', [
      Validators.required,
      Validators.minLength(8) // 最低8文字
    ]),
    email: new FormControl('', [
      Validators.email, // e-mailフォーマットチェック
      this.duplicateEmailValidator() // 任意のバリデーション(今回は重複チェック)
    ])
  });

  constructor() { }

  ngOnInit() {
  }

  private duplicateEmailValidator(): ValidatorFn {
    return (control: AbstractControl): {[key: string]: any} | null => {
      const ngList = [
        'hoge@example.com',
        'huga@test.co.jp'
      ];
      const duplicate = ngList.includes(control.value);
      return duplicate ? {'duplicateEmail': {value: control.value}} : null;
    };
  }

  public get userID() {
    return this.formGroup.get('userID');
  }
  public get password() {
    return this.formGroup.get('password');
  }
  public get email() {
    return this.formGroup.get('email');
  }
}
```

**ng-message.component.html**

```html
<div *ngIf="form.invalid && (form.dirty || form.touched)">
  <span *ngIf="userID.hasError('required')">
    必須です。
  </span>
  <span *ngIf="userID.hasError('maxlength')">
    最大5文字です。
  </span>
  <span *ngIf="userID.hasError('minlength')">
    最低8文字です。
  </span>
  <span *ngIf="userID.hasError('email')">
    e-mailアドレスのフォーマットにしてください。(xxx@hogehoge.com)
  </span>
  <span *ngIf="userID.hasError('duplicateEmail')">
    入力頂いたアドレスは重複しています。
  </span>
</div>
```
**ng-message.component.ts**

```javascript
import { Component, OnInit, Input } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-ng-message',
  templateUrl: './ng-message.component.html',
  styleUrls: ['./ng-message.component.css']
})
export class NgMessageComponent implements OnInit {
  @Input() form: FormControl;

  constructor() { }

  ngOnInit() {
  }
}
```

`app.component.ts`の`FormGroup`で`FormCotntrol`をまとめている。
まとめたControlは個別にバリデーションを設定できるので、一応全部バラバラにしてみた。
emailのところでは自作のバリデーションを追加している。
[Angular日本語ドキュメント](https://angular.jp/guide/form-validation#%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%BF)によると(英語のドキュメントも同じだけど)Heroの名前にbobが使えないいじめ仕様だから変えてみた。
＋そもそも公式の例だと`Validators.pattern(/bob/i)`って書けば良いはずなので、もうちょっと有り得そうなパターンにしたかったから。
ちなみに`app.co,ponent.ts`にgetterを作っているのは、
FormGroupから特定のデータを取るには`FormGroup.get('フォーム名')`としなきゃいけなくて、html他で参照するときに使いづらいから。
(＋公式に書いてあるから。)

エラーが有った場合には作成した`ng-message.component`にFormControlをそのまま投げている。
`ng-message.component`で受け取った値に応じたメッセージを表示するという仕組み。
これでメッセージ追加する時はここに足せばいいし、呼び出し側でも長い条件書かなくていいから楽になったのではないかと。

# まとめ
リアクティブフォームについて簡単にまとめて簡単に使ってみた。
今までは双方向バインドでええやんって思ってたけど、これは便利かもしれない。
今後は積極的に使っていきたいところ。