Angular , Material,  Cross Field, 複数フィールド, Validation ,複数セット, multi set

ネットにはドヤ顔で複数項目のバリデーションを解説してくれてるページがたくさんありますが、そうじゃない。その複数フィールドバリデーションが複数セットあった場合のことを書いている記事がないんだ。なかったので書きます。いわゆるフィールドの内容が一致していることを確認するアレが複数セットあるパターンです。かつangular/materialの場合のmat-errorの違うそうじゃない感を取り扱う方法も含めて書きます。
* Angular11
* 綺麗に記事書けるほど理解していないので悪しからず
* 私はAngular初心者なのでまじで何もわかってません。
* Angularで記事をかける喜びに打ち震えています。(ReactよりAngular好き。今のところね。。)

具体的には
https://stackoverflow.com/a/55574149/5598088
です。
たどりつくまですんごい時間かかりました。2時間ぐらいググらされて死ぬかと思った。
![a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13106/14cf8d7c-3046-020c-d21d-7f435cfbb01f.gif)

では早速

## FormGroupの入子
TSにはFormGroupを入子にするのがポイントです。入れ子にしないと一方が満たせても他の方がnvalidで今入力しているのがエラーのまんまになってしまいます。

```typescript
  constructor(private fb: FormBuilder) {
    this.frmGrp = this.fb.group({
      name: ['', Validators.required],
      set1: this.fb.group(
        {
          password: ['', Validators.required],
          password_confirm: [''],
        },
        {
          validator: this.getMultiValidator('password', 'password_confirm'),
        } as AbstractControlOptions
      ),
      set2: this.fb.group(
        {
          mail: ['', Validators.required],
          mail_confirm: [''],
        },
        {
          validator: this.getMultiValidator('mail', 'mail_confirm'),
        } as AbstractControlOptions
      ),
    });
  }
```

### angular/materialのmat-errorを公式に近い形で補正するコードをつくってインスタンスを作っておきます
ng-errorの表示タイミングが直感的ではないので補正します。

```typescript
class CrossFieldErrorMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    return control.dirty && form.invalid;
  }
}
```

```typescript
export class Page1Component implements OnInit {
  frmGrp: FormGroup;
  errorMatcher = new CrossFieldErrorMatcher(); // <= これ
```


### バリデーター作成関数
確認側のフィールドにエラーをセットしたり外したりします

```typescript
  getMultiValidator(a: string, b: string) {
    return (form: FormGroup) => {
      const condition = form.get(a).value !== form.get(b).value;
      if (condition) {
        form.get(b).setErrors({ multi: true });
      } else {
        form.get(b).setErrors(null);
      }
      return null;
    };
  }
```

### htmlをフォームの入子にします。

入れ子にすることとerrorStateMatcherを設定しておくことなどがポイント

```html
  <div [formGroup]="frmGrp.get('set1')">
    <mat-form-field>
      <input type="password" matInput placeholder="password" formControlName="password" />
      <mat-error *ngIf="errorHandling('password', 'required', 'set1')">required.</mat-error>
    </mat-form-field>
    <mat-form-field>
      <input type="password" matInput placeholder="Verify Password" formControlName="password_confirm" [errorStateMatcher]="errorMatcher" />
      <mat-error *ngIf="errorHandling('password_confirm', 'multi', 'set1')">not mutch.</mat-error>
    </mat-form-field>
  </div>

  <div [formGroup]="frmGrp.get('set2')">
    <mat-form-field>
      <input type="mail" matInput placeholder="test@example.com" formControlName="mail" />
      <mat-error *ngIf="errorHandling('mail', 'required', 'set2')">required.</mat-error>
    </mat-form-field>
    <mat-form-field>
      <input type="mail" matInput placeholder="verify" formControlName="mail_confirm" [errorStateMatcher]="errorMatcher" />
      <mat-error *ngIf="errorHandling('mail_confirm', 'multi', 'set2')">not mutch.</mat-error>
    </mat-form-field>
  </div>
```

# 全体
## TS

```typescript
import { ErrorStateMatcher } from '@angular/material/core';
import { FormBuilder, FormGroup, FormControl, FormGroupDirective, NgForm, Validators, AbstractControlOptions } from '@angular/forms';

import { Component, OnInit } from '@angular/core';

class CrossFieldErrorMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    return control.dirty && form.invalid;
  }
}
@Component({
  selector: 'app-page1',
  templateUrl: './page1.component.html',
  styleUrls: ['./page1.component.css'],
})
export class Page1Component implements OnInit {
  frmGrp: FormGroup;
  errorMatcher = new CrossFieldErrorMatcher();
  constructor(private fb: FormBuilder) {
    this.frmGrp = this.fb.group({
      name: ['', Validators.required],
      set1: this.fb.group(
        {
          password: ['', Validators.required],
          password_confirm: [''],
        },
        {
          validator: this.getMultiValidator('password', 'password_confirm'),
        } as AbstractControlOptions
      ),
      set2: this.fb.group(
        {
          mail: ['', Validators.required],
          mail_confirm: [''],
        },
        {
          validator: this.getMultiValidator('mail', 'mail_confirm'),
        } as AbstractControlOptions
      ),
    });
  }

  ngOnInit(): void {}

  onSubmit(): void {
    console.log(this.frmGrp.value);
  }

  getMultiValidator(a: string, b: string) {
    return (form: FormGroup) => {
      const condition = form.get(a).value !== form.get(b).value;
      if (condition) {
        form.get(b).setErrors({ multi: true });
      } else {
        form.get(b).setErrors(null);
      }
      return null;
    };
  }

  public errorHandling = (control: string, error: string, parent?: string) => {
    if (parent) {
      return this.frmGrp.get(parent).get(control).hasError(error);
    } else {
      return this.frmGrp.get(control).hasError(error);
    }
  }
}
```

## html

```html
<form [formGroup]="frmGrp" (ngSubmit)="onSubmit()">
  <mat-form-field>
    <input type="text" matInput placeholder="name" formControlName="name" autocomplete="on" />
    <mat-error *ngIf="errorHandling('name', 'required')">required.</mat-error>
  </mat-form-field>

  <div [formGroup]="frmGrp.get('set1')">
    <mat-form-field>
      <input type="password" matInput placeholder="password" formControlName="password" autocomplete="on" />
      <mat-error *ngIf="errorHandling('password', 'required', 'set1')">required.</mat-error>
    </mat-form-field>
    <mat-form-field>
      <input
        type="password"
        matInput
        placeholder="Verify Password"
        formControlName="password_confirm"
        [errorStateMatcher]="errorMatcher"
        autocomplete="on"
      />
      <mat-error *ngIf="errorHandling('password_confirm', 'multi', 'set1')">not mutch.</mat-error>
    </mat-form-field>
  </div>

  <div [formGroup]="frmGrp.get('set2')">
    <mat-form-field>
      <input type="mail" matInput placeholder="test@example.com" formControlName="mail" autocomplete="on" />
      <mat-error *ngIf="errorHandling('mail', 'required', 'set2')">required.</mat-error>
    </mat-form-field>
    <mat-form-field>
      <input type="mail" matInput placeholder="verify" formControlName="mail_confirm" [errorStateMatcher]="errorMatcher" autocomplete="on" />
      <mat-error *ngIf="errorHandling('mail_confirm', 'multi', 'set2')">not mutch.</mat-error>
    </mat-form-field>
  </div>

  <div class="state">
    Do fields match?<strong>{{ !frmGrp.invalid }}</strong>
    <div *ngIf="!frmGrp.invalid">
      <button type="submit" mat-flat-button color="primary">OK Submit</button>
    </div>
  </div>
</form>
```