角度でマルチコントロールカスタムバリデータを作成する


角の反応形式ライブラリのカスタムバリデータは、開発者がより良いフォームUI/UXを作成しなければならない最も強力な(そして、私の意見で見落とされる)ツールのうちの1つです.カスタムバリデータは、単一のコントロールに限られていません.グループ全体を評価するのは簡単です.これは、複数のコントロールを比較するために最適です.この記事では、2つのフィールドを検証するマルチコントロールカスタムバリデータを作成します.
私がprevious article about custom validatorsで述べたように、組み込みのバリデータがそうでないカスタムロジックを処理し、1つの場所に妥当性検査エラーメッセージを作成することができます.これは、カスタムバリデータを強力かつ非常に再利用可能になります.

マルチコントロールカスタムバリデータの作成



マルチコントロールカスタムバリデータの作成は、単一のコントロールの作成に非常に似ています.バリデータは、 AbstractControl パラメーターで渡される必要があります.単一の制御バリデータでは、通常、制御は FormControl です.しかし、マルチコントロールバリデータの場合は、コントロールとして親 FormGroup に渡す必要があります.これを行うと、FormGroupの内部のすべての子供たちのコントロールの範囲を私に与えます.このバリデータを再利用可能にするために、私は比較したいコントロールの名前も渡します.私はまた、エラーメッセージをより動的にするために比較している値の種類の名前を渡すことができます.
フォームコントロールから値の変数を作成します.一度私はそれらを持って、私はいくつかの簡単な条件を設定します.FormGroupとしてAbstractControlとして通過したので、FormControlにエラーを設定したいならば、 FormControls を特定のコントロールに呼び出す必要があります.さもなければ、私が setErrors() を返すならば、彼らはValidationErrorsに適用されます.
export class MatchFieldValidator {
  static validFieldMatch(
    controlName: string,
    confirmControlName: string,
    fieldName: string = 'Password',
  ): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      const controlValue: unknown | null = control.get(controlName)?.value;
      const confirmControlValue: unknown | null = control.get(
        confirmControlName,
      )?.value;

      if (!confirmControlValue) {
        control.get(confirmControlName)?.setErrors({
          confirmFieldRequired: `Confirm ${fieldName} is required.`,
        });
      }

      if (controlValue !== confirmControlValue) {
        control
          .get(confirmControlName)
          ?.setErrors({ fieldsMismatched: `${fieldName} fields do not match.` });
      }

      if (controlValue && controlValue === confirmControlValue) {
        control.get(confirmControlName)?.setErrors(null);
      }

      return null;
    };
  }
}
作業バリデーターを持っているので、コンポーネントに配線する必要があります.複数のFormGroupと対話したいので、親242479142にバリデータを付ける必要があります. FormControls は、コントロールの設定の後にオプション引数を受け取ります.私はマッチフィールドバリデータを、コントロールの名前と比較して、比較するフィールドの種類を追加します.私は以下のコードを単純に関連するものに集中させました.
private createForm(): FormGroup {
  const form = this.fb.group({
    password: [
      '',
      Validators.compose([PasswordValidator.validPassword(true)]),
    ],
    confirmPassword: [''],
  },
  {
    validators: Validators.compose([
      MatchFieldValidator.validFieldMatch('password', 'confirmPassword', 'Password'),
    ]),
  });

  return form;
}
今の検証を行っているので、エラーをテンプレートにバインドできます.私はまだ簡単にFormGroupを介してエラーオブジェクトを介してループを使用しています.
<div class="field-group">
  <mat-form-field>
    <input
      name="password"
      id="password"
      type="password"
      matInput
      placeholder="Password"
      formControlName="password"
    />
    <mat-error *ngIf="form.get('password')?.errors">
      <ng-container *ngFor="let error of form.get('password')?.errors | keyvalue">
        <div *ngIf="error.key !== 'required'">{{ error.value }}</div>
      </ng-container>
    </mat-error>
  </mat-form-field>
  <mat-form-field>
    <input
      name="confirmPassword"
      id="confirmPassword"
      type="password"
      matInput
      placeholder="Confirm Password"
      formControlName="confirmPassword"
      required
    />
    <mat-error *ngIf="form.get('confirmPassword')?.errors">
      <ng-container *ngFor="let error of form.get('confirmPassword')?.errors | keyvalue">
        <div *ngIf="error.key !== 'required'">{{ error.value }}</div>
      </ng-container>
    </mat-error>
  </mat-form-field>
</div>

バリデータのテスト


他のカスタムバリデータと同様に、マルチコントロールカスタムバリデータをテストするのは簡単です.このバリデータのためのライティング・ユニットテストは、私が最初に処理していなかったエッジ・ケースを見つけて、扱うのを助けました.以下に例を示します.
  describe('validFieldMatch() default field name', () => {
    const matchFieldValidator = MatchFieldValidator.validFieldMatch(
      'controlName',
      'confirmControlName',
    );
    const form = new FormGroup({
      controlName: new FormControl(''),
      confirmControlName: new FormControl(''),
    });
    const controlName = form.get('controlName');
    const confirmControlName = form.get('confirmControlName');

    it(`should set control error as { confirmFieldRequired: 'Confirm Password is required.' } when value is an empty string`, () => {
      controlName?.setValue('');
      confirmControlName?.setValue('');
      matchFieldValidator(form);
      const expectedValue = {
        confirmFieldRequired: 'Confirm Password is required.',
      };
      expect(confirmControlName?.errors).toEqual(expectedValue);
    });

    it(`should set control error as { fieldsMismatched: 'Password fields do not match.' } when values do not match`, () => {
      controlName?.setValue('password123!');
      confirmControlName?.setValue('password123');
      matchFieldValidator(form);
      const expectedValue = {
        fieldsMismatched: 'Password fields do not match.',
      };
      expect(confirmControlName?.errors).toEqual(expectedValue);
    });

    it(`should set control error as null when values match`, () => {
      controlName?.setValue('password123!');
      confirmControlName?.setValue('password123!');
      matchFieldValidator(form);
      expect(controlName?.errors).toEqual(null);
      expect(confirmControlName?.errors).toEqual(null);
    });

    it(`should set control error as null when control matches confirm after not matching`, () => {
      controlName?.setValue('password123!');
      confirmControlName?.setValue('password123!');
      matchFieldValidator(form);
      controlName?.setValue('password123');
      matchFieldValidator(form);
      controlName?.setValue('password123!');
      matchFieldValidator(form);
      expect(controlName?.errors).toEqual(null);
      expect(confirmControlName?.errors).toEqual(null);
    });

    it(`should set control error as null when confirm matches control after not matching`, () => {
      controlName?.setValue('password123!');
      confirmControlName?.setValue('password123!');
      matchFieldValidator(form);
      controlName?.setValue('password123');
      matchFieldValidator(form);
      confirmControlName?.setValue('password123');
      matchFieldValidator(form);
      expect(controlName?.errors).toEqual(null);
      expect(confirmControlName?.errors).toEqual(null);
    });
  });
カスタムバリデータを作成し、非常に強力です.彼らは任意のレベルの反応形式で行うことができますので、複数のコントロールと対話することができますこのようなマルチコントロールカスタムバリデータを作成することが可能です.これは、開発者は非常に反応性のUI/UXのユーザーをクラフトすることができます.

資源


リポジトリは、所望の動作をダイヤルするのに役立つunit tests for the validatorを含んでいます.HereはGitthubのリポジトリです、そして、hereはStackblitzの上でコードの作業デモです.角の私のポストの全てはタグ付けされて、hereを集めました.
Creating a Multi-Control Custom Validator in Angular年には、Hapax Legomenonが初めて登場した.