まえおき
こちらの記事 で validation のエラー情報を管理する際に、そのトリガーとして (keyup)
を使用する方法を記載したが、これだと期待通りに動作しないケースがあった。
具体的には次のとおり。
前提
- 複数の入力項目で発生したエラー情報を管理し、最後に発生したエラーを表示したい
- そのために
(keyup)
をトリガーにエラー情報を管理している
(keyup)
でうまくいかないケース
- 操作
- 全角入力で1文字入力 -> Enterで確定する
- 結果
- 未入力を示す validation エラーとなる
- 2文字目の入力を行うとパターン不整合による validation エラーとなる
- 期待値
- 1文字目の入力でパターン不整合による validation エラーとなる
対策
上記で発生した問題は statusChanges を利用することで解決、期待値に示す結果となった。
で、statusChanges
を利用するには ReactiveFormsModule
を使用してフォームをコンポーネントで作成する必要がある。
本記事では以下の内容で statusChanges
と ReactiveFormsModule
を使った方法について紹介する。
更新情報
2021/01/10
- 記事内で扱ったコードを Angular
v11.0.5
で確認しました
作業環境
環境 | バージョン | 備考 |
---|---|---|
Angular CLI |
|
$ ng --version |
Angular |
|
同上 |
TypeScript | v4.0.2 | 同上 |
Node.js |
|
$ node --version |
npm |
|
$ npm --version |
ng version の結果
$ ng version
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI: 11.0.5
Node: 12.18.3
OS: darwin x64
Angular: 11.0.5
... animations, cli, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router
Ivy Workspace: Yes
Package Version
---------------------------------------------------------
@angular-devkit/architect 0.1100.5
@angular-devkit/build-angular 0.1100.5
@angular-devkit/core 11.0.5
@angular-devkit/schematics 11.0.5
@schematics/angular 11.0.5
@schematics/update 0.1100.5
rxjs 6.6.0
typescript 4.0.2
ReactiveFormsModule を使用したフォームを作成する
ReactiveFormsModule
でフォームを作るには次の作業を行う。
- 前準備
-
app.module.ts
にReactiveFormsModule
をimport
して@NgModule
のimports
に登録する - フォームを作るコンポーネントで
FormGroup
,FormControl
をimport
する - validation も行いたい場合は
Validators
もimport
する - 実装
-
FormGroup
から テンプレートで使用するフォームのインスタンスを生成する - このとき、テンプレートのフォームで設定したい項目のインスタンスを
FormControl
を指定して生成する - validation を行う場合は、
FromControl
のインスタンスを生成する際に、コンストラクタの引数に指定することで設定する - 生成した
FormGourp
のインスタンスをセットしたプロパティはテンプレートの<form>
タグで[formGroup]="form"
と指定する
以下、具体的な内容をコードでみていく。
前準備
-
app.module.ts
の編集app.module.ts// 省略 import { ReactiveFormsModule } from '@angular/forms'; // <-- ここを追加 // 省略 @NgModule({ // 省略 imports: [ // 省略 ReactiveFormsModule // <-- ここを追加 ], // 省略 }) export class AppModule { }
-
コンポーネントの編集
use-directive.component.ts// 省略 import { FormGroup, FormControl, Validators } from '@angular/forms'; // <-- ここを追加 // 省略 export class UseDirectiveComponent implements OnInit, OnDestroy { // 省略 }
実装
-
Form の生成
コンポーネントの編集例.ts// 省略 import { FormGroup, FormControl, Validators } from '@angular/forms'; import { Subscription } from 'rxjs'; import { stringify } from 'querystring'; // 省略 export class exampleComponent implements OnInit, OnDestroy { public form!: FormGroup; // テンプレートで使用するフォームを宣言 public ngOnInit() { // フォームの生成 this.form = new FormGroup({ // フォーム内で使用する項目の生成 numberControl: new FormControl( '', [ Validators.required, // 必須入力をチェック Validators.minLength(7), // 最少入力桁数をチェック Validators.maxLength(15), // 最大入力桁数をチェック Validators.pattern('^[0-9]+$') // 入力パターンをチェック ] ) }); } public onClickOK($event) { const inputValue: any = { // 入力項目へのアクセスは FormGroup のインスタンスの controls から行う // このとき FormControl を生成したときの名前を指定する input: this.form.controls['numberControl'].value }; alert('input value: ' + JSON.stringify(inputValue)); } }
-
テンプレートへの登録
テンプレートの編集例.html<!-- [formGroup]="form" でコンポーネントで生成した FormGroup をセット --> <form #formCheck="ngForm" [formGroup]="form" class="form-check"> <div class="form-area"> <div class="input-network-address"> <label class="label-common">IPアドレス:</label> <!-- [formControl]="form.controls['numberControl']" でコンポーネントで生成した FormControl をセット --> <input type="text" class="input-text" name="input-ip-address" formControlName="numberControl" > </div> <div class="validation-error-information"> <!-- *ngIf=form.controls['numberControl'].errors で FormControl のエラー有無をチェックしてエラーが発生していれば表示する --> <div class="caution" *ngIf="form.controls['numberControl'].errors"> <div [hidden]="!form.controls['numberControl'].errors?.required">※ 項目が未入力です</div> <div [hidden]="!form.controls['numberControl'].errors?.minlength">※ 入力した内容が短すぎます</div> <div [hidden]="!form.controls['numberControl'].errors?.maxlength">※ 入力した内容が長すぎます</div> <div [hidden]="!form.controls['numberControl'].errors?.pattern">※ 入力した内容に誤りがあります</div> </div> </div> </div> <div class="button-area"> <!-- form タグの属性にセットしていた #formCheck="ngForm" から formCheck.invalid を判定することでボタンの有効/無効を制御 --> <input type="button" class="button-ok" name="button-ok" value="OK" (click)="onClickOK($event)" [disabled]="formCheck.invalid"> </div> </form>
小まとめ
以上で ReactiveFormsModule
を使用したフォームの作成となる。
ただここまでの内容ではテンプレートで validation を実現する方法とやっていることは変わりなく、ただ入力項目に対する validation チェックを行ってエラーを出しているだけである。
まえおき で記した要件を実現するための実装を以降に記載する。
statusChanges を使用して validation の状態を監視する
本家のリファレンス をみると statusChanges
の型は Observable<any> | null
となっている。
ということで、statusChanges の subscribe
を利用すれば、 validation の状態を監視 できる。
コード例を挙げると次のような感じになる。
statusChanges を利用したコード例
-
validation の状態を監視するサンプル
コンポーネントの編集例.ts// 省略 export class exampleComponent implements OnInit, OnDestroy { public form!: FormGroup; // テンプレートで使用する form を宣言 private statusChangesSubscription!: Subscription; // statusChanges イベントを保持する subscription public ngOnInit() { // form の生成 this.form = new FormGroup({ // form 内で使用する項目の生成 numberControl: new FormControl( '', [ Validators.required, // 必須入力をチェック Validators.minLength(7), // 最少入力桁数をチェック Validators.maxLength(15), // 最大入力桁数をチェック Validators.pattern('^[0-9]+$') // 入力パターンをチェック ] ) }); // -------------------------------------------// // 入力フォームの validation の状態を監視する // // -------------------------------------------// this.statusChangesSubscription = this.networkForm.controls['ipControl'].statusChanges.subscribe((data) => { // エラー情報に対して行いたい処理... // なおエラー情報は // form Group のインスタンス.controls['FormControl を生成したときの名前'].errors // で取得できる }); // 省略 } public ngOnDestroy() { // イベント情報が残り続けるのを防ぐためにコンポーネントの終了処理で破棄する this.statusChangesSubscription.unsubscribe(); } }
statusChanges を利用してエラー情報を管理する
要件は まえおき に記載したとおり 「複数の入力項目で発生したエラー情報を管理し、最後に発生したエラーを表示したい」 となる。
エラー管理の実装内容は、これも まえおき で挙げた参照記事と変わらない。
変更点は
- 状態の監視に
statusChanges
を利用する - それに合わせて
(keyup)
の利用を廃止する
といったところ。
では、実際のコードを示す。
( 上記までで挙げたサンプルコードとはプロパティ名やメソッド名が異なるので注意 )
-
コンポーネント
reactive-form.component.tsimport { Component, OnInit, OnDestroy } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; import { Subscription } from 'rxjs'; import { stringify } from 'querystring'; @Component({ selector: 'app-reactive-form', templateUrl: './reactive-form.component.html', styleUrls: ['./reactive-form.component.css'] }) export class ReactiveFormComponent implements OnInit, OnDestroy { public networkForm!: FormGroup; // ビューで定義する Form の本体 // validation の属性 public readonly minNetworkAddressLength: number = 7; public readonly maxNetworkAddressLength: number = 15; public readonly networkAddressPattern: string = '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])[¥.]){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$'; public validationError: any; // 入力エラー情報を画面に表示するためのプロパティ private validationErrorList: any = []; // 入力エラー情報を管理するためのリスト // 入力 form で発生した statusChanges イベントを保持する subscription private ipControlSubscription!: Subscription; private subnetmaskSubscription!: Subscription; constructor() { } public ngOnInit() { // 複数の入力項目を設置する form を生成する this.networkForm = new FormGroup({ // IPアドレスの入力項目 ipControl: new FormControl( '', [ Validators.required, // 必須入力をチェック Validators.minLength(this.minNetworkAddressLength), // 最少入力桁数をチェック Validators.maxLength(this.maxNetworkAddressLength), // 最大入力桁数をチェック Validators.pattern(this.networkAddressPattern) // 入力パターンをチェック ] ), // サブネットマスクの入力項目 subnetmaskControl: new FormControl( '', [ Validators.required, // 必須入力をチェック Validators.minLength(this.minNetworkAddressLength), // 最少入力桁数をチェック Validators.maxLength(this.maxNetworkAddressLength), // 最大入力桁数をチェック Validators.pattern(this.networkAddressPattern) // 入力パターンをチェック ] ), }); // form 生成時に指定した入力項目( FormControl ) 分、validation の状態を監視する // 状態が変化したらイベントをキャッチするので、それをトリガーにエラー情報の管理を行う this.ipControlSubscription = this.networkForm.controls['ipControl'].statusChanges.subscribe((data) => { this.manageValidationError('ipControl', this.networkForm.controls['ipControl'].errors); }); this.subnetmaskSubscription = this.networkForm.controls['subnetmaskControl'].statusChanges.subscribe((data) => { this.manageValidationError('subnetmaskControl', this.networkForm.controls['subnetmaskControl'].errors); }); // おまけ: 入力 form の有効/無効を制御する this.changeFormEnabledDisabled(); } public ngOnDestroy() { // イベント情報が残り続けるのを防ぐためにコンポーネントの終了処理で破棄する this.ipControlSubscription.unsubscribe(); this.subnetmaskSubscription.unsubscribe(); } private changeFormEnabledDisabled() { // この例では常に「有効」となる // 有効にしたい場合 this.networkForm.controls['ipControl'].enable(); this.networkForm.controls['subnetmaskControl'].enable(); // 無効にしたい場合 // this.networkForm.controls['ipControl'].disable(); // this.networkForm.controls['subnetmaskControl'].disable(); } private manageValidationError(validationKey, errorInformation) { // validation のエラー情報を管理する // 常に最後に発生したエラー情報をビューに表示するため、リストから最後の要素を取得して // テンプレートから参照されるプロパティにセットしている for (const target in this.validationErrorList) { if (this.validationErrorList.hasOwnProperty(target) && this.validationErrorList[target].key === validationKey) { this.validationErrorList.splice(target, 1); break; } } if (errorInformation) { this.validationErrorList.push({ key: validationKey, error: errorInformation }); } const errorData: any = this.validationErrorList[this.validationErrorList.length - 1]; this.validationError = errorData ? errorData.error : undefined; } }
-
テンプレート
reactive-form.component.html<!-- [formGroup]="networkForm" でコンポーネントで生成した FormGroup をセット --> <form #formCheck="ngForm" [formGroup]="networkForm" class="form-check"> <div class="form-area"> <div class="input-network-address"> <label class="label-common">IPアドレス:</label> <!-- [formControl]="networkForm.controls['ipControl']" でコンポーネントで生成した FormControl をセット --> <input type="text" class="input-text" name="input-ip-address" formControlName="ipControl" > </div> <div class="input-network-address"> <label class="label-common">サブネットマスク:</label> <!-- [formControl]="networkForm.controls['subnetmaskControl']" でコンポーネントで生成した FormControl をセット --> <input type="text" class="input-text" name="input-subnet-mask" formControlName="subnetmaskControl" > </div> <div class="validation-error-information"> <!-- *ngIf=validationError で validation のエラー有無をチェックしてエラーが発生していれば表示する (networkForm.dirty || networkForm.touched) は一回でも入力があったかを判断する --> <div class="caution" *ngIf="validationError && (networkForm.dirty || networkForm.touched)"> <div [hidden]="!validationError?.required">※ 項目が未入力です</div> <div [hidden]="!validationError?.minlength">※ 入力した内容が短すぎます</div> <div [hidden]="!validationError?.maxlength">※ 入力した内容が長すぎます</div> <div [hidden]="!validationError?.pattern">※ 入力した内容に誤りがあります</div> </div> </div> </div> <div class="button-area"> <!-- form タグの属性にセットしていた #formCheck="ngForm" から formCheck.invalid を判定することでボタンの有効/無効を制御 --> <input type="button" class="button-ok" name="button-ok" value="OK" (click)="onClickOK($event)" [disabled]="formCheck.invalid"> </div> </form>
実行結果
ここでは まえおき の要件の確認ということで、全角文字1文字の確認結果のみを示す。
まとめ
本記事の内容を簡単にまとめると
-
statusChanges
で validation の状態を監視することができる -
statusChanges
を使うにはReactiveFormsModule
でフォームを作成する必要がある - フォームを作る際は
FormGroup
とFromControl
のインスタンスを生成してテンプレートにセットする - 生成したフォームに対して validation の状態を監視するには、
FromControl
に対してstatusChanges
をsubscribe
してイベント登録する - フォームの有効/無効を切り替える際は
enable()
とdisable()
を使用する
となる。
ソースコード
今回の記事で作成したコードは こちら にアップしてあるのでご参考まで。
謝辞
validation のエラーハンドリングについて こちら で質問をしたところ、 @Quramy 様に statusChanges をご教示いただきました。
あらためて御礼申し上げます。
ありがとうございました。