1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-03-14

ネットにはドヤ顔で複数項目のバリデーションを解説してくれてるページがたくさんありますが、そうじゃない。その複数フィールドバリデーションが複数セットあった場合のことを書いている記事がないんだ。なかったので書きます。いわゆるフィールドの内容が一致していることを確認するアレが複数セットあるパターンです。かつangular/materialの場合のmat-errorの違うそうじゃない感を取り扱う方法も含めて書きます。

  • Angular11
  • 綺麗に記事書けるほど理解していないので悪しからず
  • 私はAngular初心者なのでまじで何もわかってません。
  • Angularで記事をかける喜びに打ち震えています。(ReactよりAngular好き。今のところね。。)

具体的には
https://stackoverflow.com/a/55574149/5598088
です。
たどりつくまですんごい時間かかりました。2時間ぐらいググらされて死ぬかと思った。
a.gif

では早速

FormGroupの入子

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

  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の表示タイミングが直感的ではないので補正します。

class CrossFieldErrorMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    return control.dirty && form.invalid;
  }
}
export class Page1Component implements OnInit {
  frmGrp: FormGroup;
  errorMatcher = new CrossFieldErrorMatcher(); // <= これ

バリデーター作成関数

確認側のフィールドにエラーをセットしたり外したりします

  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を設定しておくことなどがポイント

  <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

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

<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>
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?