Angular Reactive Forms その3 カスタムバリデーション

Reactive Formにてカスタムバリデーションを実装します。

注意
本連載は、基本的に前回までの作業が終わっていることを前提としています。最初から試したい方は以前の記事も参考にしてください。また、今回から作成するWebページはESPPなどで必要な情報を入れると確定申告に必要な情報をだしてくれるものを目指しますが、確定申告に必要な情報を保証するようなものではないので、了承の上使用ください。
Angular Reactive Form その2 入力内容に合わせて処理を行う

各種バージョンは以下のとおりです。

  • Angular CLI: 11.2.8
  • Node: 15.10.0
  • Angular Material: 11.2.8

今回の目標とソースコードについて

今回の目標は、

  • Angular Materialのフォームにエラーを追加
  • 株数を入力するフォームに正の整数かどうかチェックするバリデーションを追加

です。

大したバリデーションではないですが、ベースを理解していただいくのが今回の目的となります。

参考 今回のソースコード

Angular Materialのフォームにエラーを追加

<mat-form-field>内に<mat-error>を追加します。株数の部分を抜粋すると以下のとおりです。

             <mat-form-field class="espp-form">
                <mat-label>株数</mat-label>
                <input matInput type="number" formControlName="quantity" required/>
                <mat-error>0より大きい整数を入力してください</mat-error>
              </mat-form-field>

この<mat-error>メッセージは該当のフォームをユーザが触り、入力内容がバリデーションに違反したとき表示されます。(touched かつ invalidの状態を示します。)

では、エラー分を表示しましょう。input内でrequiredを指定しているので、フォームから値を削除するとエラー文が以下のように表示されるはずです。

フォームに標準のバリデーションを追加

カスタムバリデーションを追加する前に標準でサポートされているバリデーションを追加します。現在標準でサポートされているバリデーションは以下のとおりです。

  • min
  • max
  • required
  • requiredTrue
  • email
  • minLength
  • maxLength
  • pattern
  • nullValidator
参考 Angular Forms Validator

この中で、最小値を指定するminと必須項目とするrequiredをフォームに追加していきます。

export class MainComponent implements OnInit, OnDestroy {
  constructor(private fb: FormBuilder) {
    this.esppForm = this.fb.group({
      name: ["", Validators.required],
      purchaseDate: ["", Validators.required],
      quantity: this.quantityFormControl,
      marketPrice: [0, [Validators.required, Validators.min(0)]],
      purchasePrice: [0, [Validators.required, Validators.min(0)]],
    });
  }
}

一つのバリデーションを追加するときは、2つ目の引数とし、複数のバリデーションを追加するときはそれらを配列にして指定しています。このバリデーションの結果によって、各項目でエラー文がでるようになります。各フォームのエラー分もhtmlに追加していきます。変更点は以下のとおりです。

              <mat-form-field class="espp-form">
                <mat-label>銘柄</mat-label>
                <input matInput formControlName="name" required/>
                <mat-error>必須項目です</mat-error>
              </mat-form-field>
(snip)
              <mat-form-field class="espp-form">
                <mat-label>購入日</mat-label>
                <input matInput [matDatepicker]="picker" formControlName="purchaseDate" required/>
                <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
                <mat-datepicker #picker></mat-datepicker>
                <mat-error>必須項目です</mat-error>
              </mat-form-field>
(snip)
              <mat-form-field class="espp-form">
                <mat-label>株数</mat-label>
                <input matInput type="number" formControlName="quantity" required/>
                <mat-error>0より大きい整数を入力してください</mat-error>
              </mat-form-field>
(snip)
              <mat-form-field class="espp-form">
                <mat-label>購入日価格</mat-label>
                <input matInput type="number" formControlName="marketPrice" required/>
                <mat-error>0より大きい数を入力してください</mat-error>
              </mat-form-field>
(snip)
              <mat-form-field class="espp-form">
                <mat-label>購入価格</mat-label>
                <input matInput type="number" formControlName="purchasePrice" required/>
                <mat-error>0より大きい数を入力してください</mat-error>
              </mat-form-field>

この変更により各フォームでエラー分が下記のようにでるようになったはずです。

ついでに、フォーム内全ての入力内容がバリデーションに準拠した場合のみボタンが押せるように下記の通り変更を加えました。

          <div class="result-box" fxFlex><button mat-raised-button color="primary" [disabled]="!esppForm.valid">保存</button></div>

全てのバリデーションをパスするとボタンが有効化されます。

カスタムバリデーションを作成

整数かどうかチェックするバリデーションを追加します。バリデーションは関数なので下記のようなコードをコンポーネントに追加します。

function IntegerValidation(c: AbstractControl) {
  if (parseFloat(c.value) == parseInt(c.value) && !isNaN(c.value)) {
    return null;
  }
  return { integerValiation: true };
}

この関数は、引数でFormControlを受け取って、その値が整数ならばnullを返します。それ以外の場合には、{ integerValidation: true }を返します。重要なのは、バリデーションがパスした場合にはnullを返し、それ以外にはエラーとしてオブジェクトを返す必要がある点です。

このバリデーションを株数のフォームに追加します。

export class MainComponent implements OnInit, OnDestroy {
  constructor(private fb: FormBuilder) {
    this.esppForm = this.fb.group({
      name: ["", Validators.required],
      purchaseDate: ["", Validators.required],
      quantity: [0, [Validators.required, IntegerValidation]],
      marketPrice: [0, [Validators.required, Validators.min(0)]],
      purchasePrice: [0, [Validators.required, Validators.min(0)]],
    });
  }
}

これにより、小数を入れるとエラーがでるようになったはずです。

続いて、正の整数かどうかチェックするバリデーションに変更します。また、最小値を引数として指定できるような汎用的なものにします。

function IntegerValidation(min: number): ValidatorFn {
  return (c: AbstractControl): { [key: string]: any } | null => {
    if (parseFloat(c.value) == parseInt(c.value) && !isNaN(c.value) && c.value >= min) {
      return null;
    }
    return { integerValiation: true };
  };
}

先程定義したような関数を返すファクトリー関数に変更することで実現できます。今回は、min以上の値かつ整数であることをチェックしています。この変更に合わせて、フォームの宣言のバリデーションを変更します。

export class MainComponent implements OnInit, OnDestroy {
  constructor(private fb: FormBuilder) {
    this.esppForm = this.fb.group({
      name: ["", Validators.required],
      purchaseDate: ["", Validators.required],
      quantity: [0, [Validators.required, IntegerValidation(1)]],
      marketPrice: [0, [Validators.required, Validators.min(0)]],
      purchasePrice: [0, [Validators.required, Validators.min(0)]],
    });
  }
}

これによって-1などの負の整数や0のときにエラーがでるようになったはずです。

変更点とまとめ

今回アプリケーションに加えた変更点は以下のとおりです。

  • Angular Materialのフォームにエラーメッセージを追加
  • フォームに標準で準備されているバリデーションを追加
  • カスタムバリデーションを作成
  • カスタムバリデーションで引数を指定できるように

まずは、main.component.htmlの変更点は以下のとおりです。

main.component.html
--- a/src/app/main/main.component.html
+++ b/src/app/main/main.component.html
@@ -7,7 +7,8 @@
             <div class="form-box" fxFlex="50">
               <mat-form-field class="espp-form">
                 <mat-label>銘柄</mat-label>
-                <input matInput formControlName="name"/>
+                <input matInput formControlName="name" required/>
+                <mat-error>必須項目です</mat-error>
               </mat-form-field>
             </div>
             <div class="form-box" fxFlex="50">
@@ -18,15 +19,17 @@
             <div class="form-box" fxFlex="50">
               <mat-form-field class="espp-form">
                 <mat-label>購入日</mat-label>
-                <input matInput [matDatepicker]="picker" formControlName="purchaseDate"/>
+                <input matInput [matDatepicker]="picker" formControlName="purchaseDate" required/>
                 <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
                 <mat-datepicker #picker></mat-datepicker>
+                <mat-error>必須項目です</mat-error>
               </mat-form-field>
             </div>
             <div class="form-box" fxFlex="50">
               <mat-form-field class="espp-form">
                 <mat-label>株数</mat-label>
-                <input matInput type="number" formControlName="quantity"/>
+                <input matInput type="number" formControlName="quantity" required/>
+                <mat-error>0より大きい整数を入力してください</mat-error>
               </mat-form-field>
             </div>
           </div>
@@ -34,20 +37,22 @@
             <div class="form-box" fxFlex="50">
               <mat-form-field class="espp-form">
                 <mat-label>購入日価格</mat-label>
-                <input matInput type="number" formControlName="marketPrice"/>
+                <input matInput type="number" formControlName="marketPrice" required/>
+                <mat-error>0より大きい数を入力してください</mat-error>
               </mat-form-field>
             </div>
             <div class="form-box" fxFlex="50">
               <mat-form-field class="espp-form">
                 <mat-label>購入価格</mat-label>
-                <input matInput type="number" formControlName="purchasePrice"/>
+                <input matInput type="number" formControlName="purchasePrice" required/>
+                <mat-error>0より大きい数を入力してください</mat-error>
               </mat-form-field>
             </div>
           </div>
         </div>
         <div class="result-container" fxFlex="40" fxFlex.lt-sm="grow" fxLayout="column">
-          <div class="result-box" fxFlex>為替レート1ドル105円で計算し、利益は{{ profit | currency: 'JPY': '': '0.0-0' }}円です。</div>
-          <div class="result-box" fxFlex><button mat-raised-button color="primary">Primary</button></div>
+          <div class="result-box" fxFlex>為替レート1ドル105円で計算し、利益は{{ profit | currency: "JPY":"":"0.0-0" }}円です。</div>
+          <div class="result-box" fxFlex><button mat-raised-button color="primary" [disabled]="!esppForm.valid">保存</button></div>
         </div>
       </div>
     </form>

エラー文を追加した点と入力内容によるボタンの有効化、無効化が変更点です。利益表示の部分でシングルクォーテーションからダブルクォーテーションに変更していますが、特に意味はありません。


続いてmain.component.tsです。

main.component.ts
--- a/src/app/main/main.component.ts
+++ b/src/app/main/main.component.ts
@@ -1,5 +1,12 @@
 import { Component, OnDestroy, OnInit } from "@angular/core";
-import { FormBuilder, FormGroup, Validators } from "@angular/forms";
+import {
+  AbstractControl,
+  FormBuilder,
+  FormControl,
+  FormGroup,
+  ValidatorFn,
+  Validators,
+} from "@angular/forms";
 import { Subscription } from "rxjs";
 import { debounceTime } from "rxjs/operators";

@@ -23,6 +30,15 @@ const ELEMENT_DATA: PeriodicElement[] = [
   { position: 10, name: "Neon", weight: 20.1797, symbol: "Ne" },
 ];

+function IntegerValidation(min: number): ValidatorFn {
+  return (c: AbstractControl): { [key: string]: any } | null => {
+    if (parseFloat(c.value) == parseInt(c.value) && !isNaN(c.value) && c.value >= min) {
+      return null;
+    }
+    return { integerValiation: true };
+  };
+}
+
 @Component({
   selector: "app-main",
   templateUrl: "./main.component.html",
@@ -37,11 +53,11 @@ export class MainComponent implements OnInit, OnDestroy {

   constructor(private fb: FormBuilder) {
     this.esppForm = this.fb.group({
-      name: "",
+      name: ["", Validators.required],
       purchaseDate: ["", Validators.required],
-      quantity: [0, Validators.required],
-      marketPrice: [0, Validators.required],
-      purchasePrice: [0, Validators.required],
+      quantity: [0, [Validators.required, IntegerValidation(1)]],
+      marketPrice: [0, [Validators.required, Validators.min(0)]],
+      purchasePrice: [0, [Validators.required, Validators.min(0)]],
     });
   }

今回の肝であるバリデーションを追加しました。

最終的なアプリケーションの状態は以下のとおりで、入力内容が正しくないと保存ボタンが押せないようになっています。

参考 今回のソースコード

次回は、保存ボタンを押したときに結果を保存し、フォーム内をリセットするようにします。

Angular Reactive Forms その4 ボタンによるフォームの変更やリセット

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA