Reactive Formにてカスタムバリデーションを実装します。
各種バージョンは以下のとおりです。
- Angular CLI: 11.2.8
- Node: 15.10.0
- Angular Material: 11.2.8
TOC
今回の目標とソースコードについて
今回の目標は、
- 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
- minLength
- maxLength
- pattern
- nullValidator
この中で、最小値を指定する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
の変更点は以下のとおりです。
--- 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
です。
--- 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 ボタンによるフォームの変更やリセット