[Reactive Forms]バリデーションの実装方法をまとめてみた

Angularのリアクティブフォームにおけるバリデーションの実装をまとめました。
フォームを作る上で、既存のバリデーションだけでは不十分なケースは多々ありますよね。後半ではカスタムバリデーションの実装例もご紹介したいと思います。

#1. 組み込みバリデータを使う

まずは基本中の基本から。
Angularでは組み込みバリデータが`Validators`クラスにまとまっています。FormGroupを作る時に、FormControlの２つ目の引数として設定することができます。

```form.component.ts
export class FormComponent implements OnInit {

  form: FormGroup;

  constructor(private fb: FormBuilder) {
  }

  ngOnInit(): void {
    this.form = this.createForm()
  }

  createForm(): FormGroup {
    return this.fb.group({
      name: ['', [Validators.required, Validators.maxLength(10)]
    });
  }

  get name(): FormControl {
    return this.form.get('name') as FormControl
  }
}
```

ここでは、入力必須と最大文字数のバリデーションをかけています。もしユーザーが文字数制限を超えてしまった場合、何かしらのエラーメッセージを出力したいというケースがあると思います。公式ガイドでは下記のように`name`がエラーだった場合に`*ngIf`で表示する方法が紹介されています。

```from.component.html
<div>名前：<input formControlName="name"></div>

<div *ngIf="name.errors.maxlength">
  １０文字以内で入力してください
</div>
```
バリデーションを実装した場合、最大文字数を上回ると`{maxlength: {requiredLength: 10, actualLength:11}}`というkeyとvalueが返ります。なければ`null`です。`name.errors.maxlength`はtrueとなるため、その場合だけ上記のエラーメッセージは出力されます。

# 2. カスタムバリデータを実装する

次にプロジェクト独自のバリデーションをかけたい時の実装例です。簡単な方法としてクラス内部に関数を定義する方法をご紹介します。

**例：半角や全角のみの入力を許容しないバリデーション**

```form.component.ts
export class FormComponent implements OnInit {

  ...

  createForm(): FormGroup {
    return this.fb.group({
      name: ['', [
        Validators.required,
        Validators.maxLength(5),
        //組み込みバリデータと同じように設定できる
        this.forbiddenOnlyWhiteSpace]],
      color: ['']
    }, {
      validators: CustomValidator.requiredSelectForm
    })
  }

  forbiddenOnlyWhiteSpace(): ValidatorFn {
    return (control: AbstractControl): { [key: string]: any } | null => {
      const originalValue = String(control.value);
      const replacedValue = originalValue.replace(/\s+/g, '');
      if (originalValue.length > 0 && replacedValue.length == 0) {
        return {'forbiddenOnlyWhiteSpace': control.value}
      } else {
        return null
      }
    }
  }
}
```

厳密には`ValidatorFn`で返さなくても実装できます。組み込みのバリデータと同じように、バリデーションがかかった場合には`name.errors.forbiddenOnlyWhiteSpace`がtrueになるため、エラーハンドリングすることができます。

# 3. クロスフィールドバリデーションを実装する

あるフォームの状態によって、別のフォームのバリデーションをかけたい場合もあると思います。例えば、資格の有無のチェックボックスがtrueの時だけ、資格名を必須で入力させるケースなどです。

カスタムバリデータを`.group`の2番目の引数として設定することで、この関数はFormGroupを引数に受け取れるようになります。後は`.get(FormControl名)`で必要な値を取り出して、バリデーションをセットします。

```form.component.ts
export class FormComponent implements OnInit {

  ...

  createForm(): FormGroup {
    return this.fb.group({
      name: ['', [
        Validators.required,
        Validators.maxLength(5),
        CustomValidator.forbiddenOnlyBlank]],
      color: [''],
      hasSkill: ['false', [Validators.required]],
      skillName: ['']
    }, {
      //.groupの第２引数に関数をセットすることでFormGroupを受け取れる
      validators: [this.skillNameRequired]
    })
  }

  skillNameRequired(control: FormGroup) {
    const hasSkill = control.get('hasSkill');
    const skillName = control.get('skillName');

    return hasSkill ? skillName.setValidators(Validators.required) : skillName.clearValidators();
  }
}
```

# まとめ
いかがだったでしょうか？
Angularでアプリケーションを開発するにあたり、実装することが多い箇所なので公式ドキュメントを一度じっくり読んでみるのをオススメします！

[Angular 日本語ドキュメンテーション: フォームバリデーション](https://angular.jp/guide/form-validation)