正確性と完全性を確保するために、ユーザー入力を検証することで、データ品質を全体的に向上させることができます。 このページでは、UIからのユーザー入力を検証し、リアクティブフォームとテンプレート駆動フォームの両方で、役立つ検証メッセージを表示する方法について説明します。
テンプレート駆動フォームでの入力検証
テンプレート駆動フォームに検証を追加するには、ネイティブHTMLフォーム検証の場合と同じように、検証属性を追加します。 Angularは、これらの属性をフレームワーク内のバリデーター関数と一致させるためにディレクティブを使用します。
フォームコントロールの値が変更されるたびに、Angularは検証し、検証エラーのリスト(INVALID
ステータスをもたらす)またはnull
(VALID
ステータスをもたらす)を生成します。
その後、ngModel
をローカルテンプレート変数にエクスポートすることで、コントロールの状態を調べることができます。
次の例では、NgModel
をname
という変数にエクスポートします。
template/actor-form-template.component.html (name)
<div> <h2>Template-Driven Form</h2> <form #actorForm="ngForm" appUnambiguousRole> <div [hidden]="actorForm.submitted"> <div class="cross-validation" [class.cross-validation-error]="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)"> <div class="form-group"> <label for="name">Name</label> <input type="text" id="name" name="name" class="form-control" required minlength="4" appForbiddenName="bob" [(ngModel)]="actor.name" #name="ngModel"> <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert"> <div *ngIf="name.hasError('required')"> Name is required. </div> <div *ngIf="name.hasError('minlength')"> Name must be at least 4 characters long. </div> <div *ngIf="name.hasError('forbiddenName')"> Name cannot be Bob. </div> </div> </div> <div class="form-group"> <label for="role">Role</label> <input type="text" id="role" name="role" #role="ngModel" [(ngModel)]="actor.role" [ngModelOptions]="{ updateOn: 'blur' }" appUniqueRole> <div *ngIf="role.pending">Validating...</div> <div *ngIf="role.invalid" class="alert role-errors"> <div *ngIf="role.hasError('uniqueRole')"> Role is already taken. </div> </div> </div> <div *ngIf="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)" class="cross-validation-error-message alert"> Name cannot match role. </div> </div> <div class="form-group"> <label for="skill">Skill</label> <select id="skill" name="skill" required [(ngModel)]="actor.skill" #skill="ngModel"> <option *ngFor="let skill of skills" [value]="skill">{{ skill }}</option> </select> <div *ngIf="skill.errors && skill.touched" class="alert"> <div *ngIf="skill.errors['required']">Power is required.</div> </div> </div> <p>Complete the form to enable the Submit button.</p> <button type="submit" [disabled]="actorForm.invalid">Submit</button> <button type="button" (click)="actorForm.resetForm({})">Reset</button> </div> <div class="submitted-message" *ngIf="actorForm.submitted"> <p>You've submitted your actor, {{ actorForm.value.name }}!</p> <button type="button" (click)="actorForm.resetForm({})">Add new actor</button> </div> </form></div>
例で示されている次の機能に注目してください。
<input>
要素には、HTML検証属性(required
とminlength
)があります。 また、カスタムバリデーターディレクティブforbiddenName
もあります。 詳細については、カスタムバリデーターセクションを参照してください。#name="ngModel"
は、NgModel
をname
というローカル変数にエクスポートします。NgModel
は、基になるFormControl
インスタンスの多くのプロパティをミラーリングしているため、テンプレート内でこれを使用して、valid
やdirty
などのコントロールの状態を確認できます。 コントロールプロパティの完全なリストについては、AbstractControl APIリファレンスを参照してください。<div>
要素の*ngIf
は、入れ子になったメッセージのdiv
のセットを明らかにしますが、name
が無効で、コントロールがdirty
またはtouched
の場合のみです。各入れ子になった
<div>
は、考えられる検証エラーのいずれかについて、カスタムメッセージを表示できます。required
、minlength
、forbiddenName
のメッセージがあります。
HELPFUL: ユーザーがフォームを編集する機会がある前に、バリデーターがエラーを表示しないようにするには、コントロールのdirty
またはtouched
状態のいずれかをチェックする必要があります。
- ユーザーが監視対象のフィールドの値を変更すると、コントロールは「dirty」としてマークされます。
- ユーザーがフォームコントロール要素からフォーカスを外すと、コントロールは「touched」としてマークされます。
リアクティブフォームでの入力検証
リアクティブフォームでは、真実の源はコンポーネントクラスです。 テンプレートで属性を通じてバリデーターを追加する代わりに、コンポーネントクラスのフォームコントロールモデルに直接バリデーター関数を追加します。 その後、Angularは、コントロールの値が変更されるたびにこれらの関数を呼び出します。
バリデーター関数
バリデーター関数は、同期または非同期にすることができます。
バリデーターの種類 | 詳細 |
---|---|
同期バリデーター | コントロールインスタンスを受け取り、検証エラーのセットまたはnull をすぐに返す同期関数。FormControl をインスタンス化する際に、第2引数として渡します。 |
非同期バリデーター | コントロールインスタンスを受け取り、後で検証エラーのセットまたはnull を発行するPromiseまたはObservableを返す非同期関数。FormControl をインスタンス化する際に、第3引数として渡します。 |
パフォーマンス上の理由から、Angularは、すべての同期バリデーターが合格した場合にのみ非同期バリデーターを実行します。 各バリデーターは、エラーが設定される前に完了する必要があります。
組み込みのバリデーター関数
独自のバリデーター関数を作成することも、Angularの組み込みのバリデーターのいくつかを使用することもできます。
required
やminlength
など、テンプレート駆動フォームで属性として使用できるものと同じ組み込みバリデーターはすべて、Validators
クラスから関数として使用できます。
組み込みのバリデーターの完全なリストについては、Validators APIリファレンスを参照してください。
アクターフォームをリアクティブフォームに更新するには、いくつかの組み込みバリデーターを使用します。 今回は、関数形式で、次の例のようにします。
reactive/actor-form-reactive.component.ts (validator functions)
import {Component, OnInit} from '@angular/core';import {FormControl, FormGroup, Validators} from '@angular/forms';import {forbiddenNameValidator} from '../shared/forbidden-name.directive';@Component({ selector: 'app-actor-form-reactive', templateUrl: './actor-form-reactive.component.html', styleUrls: ['./actor-form-reactive.component.css'],})export class HeroFormReactiveComponent implements OnInit { skills = ['Method Acting', 'Singing', 'Dancing', 'Swordfighting']; actor = {name: 'Tom Cruise', role: 'Romeo', skill: this.skills[3]}; actorForm!: FormGroup; ngOnInit(): void { this.actorForm = new FormGroup({ name: new FormControl(this.actor.name, [ Validators.required, Validators.minLength(4), forbiddenNameValidator(/bob/i), // <-- Here's how you pass in the custom validator. ]), role: new FormControl(this.actor.role), skill: new FormControl(this.actor.skill, Validators.required), }); } get name() { return this.actorForm.get('name'); } get skill() { return this.actorForm.get('skill'); }}
この例では、name
コントロールは、2つの組み込みバリデーター(Validators.required
とValidators.minLength(4)
)と、1つのカスタムバリデーターforbiddenNameValidator
を設定しています。
これらすべてのバリデーターは同期であるため、第2引数として渡されます。 関数を配列として渡すことで、複数のバリデーターをサポートできることに注意してください。
この例では、いくつかのゲッターメソッドも追加されています。
リアクティブフォームでは、常に親グループのget
メソッドを通じて任意のフォームコントロールにアクセスできますが、テンプレートの省略形としてゲッターを定義することが便利な場合があります。
name
入力のテンプレートをもう一度見ると、テンプレート駆動の例とかなり似ています。
reactive/actor-form-reactive.component.html (name with error msg)
<div class="container"> <h2>Reactive Form</h2> <form [formGroup]="actorForm" #formDir="ngForm"> <div [hidden]="formDir.submitted"> <div class="cross-validation" [class.cross-validation-error]="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)"> <div class="form-group"> <label for="name">Name</label> <input type="text" id="name" class="form-control" formControlName="name" required> <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert alert-danger"> <div *ngIf="name.hasError('required')"> Name is required. </div> <div *ngIf="name.hasError('minlength')"> Name must be at least 4 characters long. </div> <div *ngIf="name.hasError('forbiddenName')"> Name cannot be Bob. </div> </div> </div> <div class="form-group"> <label for="role">Role</label> <input type="text" id="role" class="form-control" formControlName="role"> <div *ngIf="role.pending">Validating...</div> <div *ngIf="role.invalid" class="alert alert-danger role-errors"> <div *ngIf="role.hasError('uniqueRole')"> Role is already taken. </div> </div> </div> <div *ngIf="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)" class="cross-validation-error-message alert alert-danger"> Name cannot match role or audiences will be confused. </div> </div> <div class="form-group"> <label for="skill">Skill</label> <select id="skill" class="form-control" formControlName="skill" required> <option *ngFor="let skill of skills" [value]="skill">{{ skill }}</option> </select> <div *ngIf="skill.invalid && skill.touched" class="alert alert-danger"> <div *ngIf="skill.hasError('required')">Skill is required.</div> </div> </div> <p>Complete the form to enable the Submit button.</p> <button type="submit" class="btn btn-default" [disabled]="actorForm.invalid">Submit</button> <button type="button" class="btn btn-default" (click)="formDir.resetForm({})">Reset</button> </div> </form> <div class="submitted-message" *ngIf="formDir.submitted"> <p>You've submitted your actor, {{ actorForm.value.name }}!</p> <button type="button" (click)="formDir.resetForm({})">Add new actor</button> </div></div>
このフォームは、テンプレート駆動バージョンとは、ディレクティブをエクスポートしなくなった点が異なります。代わりに、コンポーネントクラスで定義されたname
ゲッターを使用します。
required
属性は、テンプレートにまだ存在することに注意してください。検証には必要ありませんが、アクセシビリティの目的で保持する必要があります。
カスタムバリデーターの定義
組み込みのバリデーターは、アプリケーションのユースケースに常に一致するわけではありません。そのため、カスタムバリデーターを作成する必要がある場合があります。
前の例のforbiddenNameValidator
関数を考えてみてください。
その関数の定義は次のようになります。
shared/forbidden-name.directive.ts (forbiddenNameValidator)
import {Directive, Input} from '@angular/core';import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn,} from '@angular/forms';/** An actor's name can't match the given regular expression */export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const forbidden = nameRe.test(control.value); return forbidden ? {forbiddenName: {value: control.value}} : null; };}@Directive({ selector: '[appForbiddenName]', providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}],})export class ForbiddenValidatorDirective implements Validator { @Input('appForbiddenName') forbiddenName = ''; validate(control: AbstractControl): ValidationErrors | null { return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control) : null; }}
関数は、特定の禁止されている名前を検出するための正規表現を受け取り、バリデーター関数を返すファクトリです。
このサンプルでは、禁止されている名前は「bob」なので、バリデーターは「bob」を含むアクター名をすべて拒否します。 他の場所では、「alice」や、構成された正規表現に一致する名前を拒否することもできます。
forbiddenNameValidator
ファクトリは、構成されたバリデーター関数を返します。
その関数はAngularコントロールオブジェクトを受け取り、コントロール値が有効な場合はnull
を返し、無効な場合は検証エラーオブジェクトを返します。
検証エラーオブジェクトには通常、検証キーの名前である'forbiddenName'
というプロパティと、エラーメッセージに挿入できる任意の値の辞書である{name}
という値を持つプロパティがあります。
カスタム非同期バリデーターは同期バリデーターに似ていますが、代わりに後でnull
または検証エラーオブジェクトを発行するPromiseまたはオブザーバブルを返す必要があります。
オブザーバブルの場合、オブザーバブルは完了する必要があります。その時点で、フォームは最後の発行された値を検証に使用します。
カスタムバリデーターをリアクティブフォームに追加する
リアクティブフォームでは、FormControl
に直接関数を渡すことで、カスタムバリデーターを追加します。
reactive/actor-form-reactive.component.ts (validator functions)
import {Component, OnInit} from '@angular/core';import {FormControl, FormGroup, Validators} from '@angular/forms';import {forbiddenNameValidator} from '../shared/forbidden-name.directive';@Component({ selector: 'app-actor-form-reactive', templateUrl: './actor-form-reactive.component.html', styleUrls: ['./actor-form-reactive.component.css'],})export class HeroFormReactiveComponent implements OnInit { skills = ['Method Acting', 'Singing', 'Dancing', 'Swordfighting']; actor = {name: 'Tom Cruise', role: 'Romeo', skill: this.skills[3]}; actorForm!: FormGroup; ngOnInit(): void { this.actorForm = new FormGroup({ name: new FormControl(this.actor.name, [ Validators.required, Validators.minLength(4), forbiddenNameValidator(/bob/i), // <-- Here's how you pass in the custom validator. ]), role: new FormControl(this.actor.role), skill: new FormControl(this.actor.skill, Validators.required), }); } get name() { return this.actorForm.get('name'); } get skill() { return this.actorForm.get('skill'); }}
カスタムバリデーターをテンプレート駆動フォームに追加する
テンプレート駆動フォームでは、テンプレートにディレクティブを追加します。ディレクティブは、バリデーター関数をラップします。
たとえば、対応するForbiddenValidatorDirective
は、forbiddenNameValidator
のラッパーとして機能します。
Angularは、ディレクティブがNG_VALIDATORS
プロバイダーに自身を登録するため、ディレクティブの検証プロセスにおける役割を認識します。次の例に示すように。
NG_VALIDATORS
は、拡張可能なバリデーターのコレクションを持つ、定義済みのプロバイダーです。
shared/forbidden-name.directive.ts (providers)
import {Directive, Input} from '@angular/core';import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn,} from '@angular/forms';/** An actor's name can't match the given regular expression */export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const forbidden = nameRe.test(control.value); return forbidden ? {forbiddenName: {value: control.value}} : null; };}@Directive({ selector: '[appForbiddenName]', providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}],})export class ForbiddenValidatorDirective implements Validator { @Input('appForbiddenName') forbiddenName = ''; validate(control: AbstractControl): ValidationErrors | null { return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control) : null; }}
その後、ディレクティブクラスはValidator
インターフェースを実装するため、Angularフォームと簡単に統合できます。
以下は、ディレクティブ全体の概要です。
shared/forbidden-name.directive.ts (directive)
import {Directive, Input} from '@angular/core';import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn,} from '@angular/forms';/** An actor's name can't match the given regular expression */export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const forbidden = nameRe.test(control.value); return forbidden ? {forbiddenName: {value: control.value}} : null; };}@Directive({ selector: '[appForbiddenName]', providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}],})export class ForbiddenValidatorDirective implements Validator { @Input('appForbiddenName') forbiddenName = ''; validate(control: AbstractControl): ValidationErrors | null { return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control) : null; }}
ForbiddenValidatorDirective
の準備ができたら、セレクターappForbiddenName
を入力要素に追加して、アクティブ化できます。
たとえば、次のとおりです。
template/actor-form-template.component.html (forbidden-name-input)
<div> <h2>Template-Driven Form</h2> <form #actorForm="ngForm" appUnambiguousRole> <div [hidden]="actorForm.submitted"> <div class="cross-validation" [class.cross-validation-error]="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)"> <div class="form-group"> <label for="name">Name</label> <input type="text" id="name" name="name" class="form-control" required minlength="4" appForbiddenName="bob" [(ngModel)]="actor.name" #name="ngModel"> <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert"> <div *ngIf="name.hasError('required')"> Name is required. </div> <div *ngIf="name.hasError('minlength')"> Name must be at least 4 characters long. </div> <div *ngIf="name.hasError('forbiddenName')"> Name cannot be Bob. </div> </div> </div> <div class="form-group"> <label for="role">Role</label> <input type="text" id="role" name="role" #role="ngModel" [(ngModel)]="actor.role" [ngModelOptions]="{ updateOn: 'blur' }" appUniqueRole> <div *ngIf="role.pending">Validating...</div> <div *ngIf="role.invalid" class="alert role-errors"> <div *ngIf="role.hasError('uniqueRole')"> Role is already taken. </div> </div> </div> <div *ngIf="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)" class="cross-validation-error-message alert"> Name cannot match role. </div> </div> <div class="form-group"> <label for="skill">Skill</label> <select id="skill" name="skill" required [(ngModel)]="actor.skill" #skill="ngModel"> <option *ngFor="let skill of skills" [value]="skill">{{ skill }}</option> </select> <div *ngIf="skill.errors && skill.touched" class="alert"> <div *ngIf="skill.errors['required']">Power is required.</div> </div> </div> <p>Complete the form to enable the Submit button.</p> <button type="submit" [disabled]="actorForm.invalid">Submit</button> <button type="button" (click)="actorForm.resetForm({})">Reset</button> </div> <div class="submitted-message" *ngIf="actorForm.submitted"> <p>You've submitted your actor, {{ actorForm.value.name }}!</p> <button type="button" (click)="actorForm.resetForm({})">Add new actor</button> </div> </form></div>
HELPFUL: カスタム検証ディレクティブがuseExisting
ではなくuseClass
でインスタンス化されていることに注意してください。
登録されたバリデーターは、ForbiddenValidatorDirective
のこのインスタンスである必要があります。フォーム内のインスタンスで、forbiddenName
プロパティが「bob」にバインドされています。
useExisting
をuseClass
に置き換えると、forbiddenName
を持たない新しいクラスインスタンスを登録することになります。
コントロールステータスCSSクラス
Angularは、多くのコントロールプロパティをフォームコントロール要素にCSSクラスとして自動的にミラーリングします。 これらのクラスを使用して、フォームの状態に応じてフォームコントロール要素のスタイルを設定します。 現在サポートされているクラスは次のとおりです。
.ng-valid
.ng-invalid
.ng-pending
.ng-pristine
.ng-dirty
.ng-untouched
.ng-touched
.ng-submitted
(囲んでいるフォーム要素のみ)
次の例では、アクターフォームは.ng-valid
と.ng-invalid
クラスを使用して、
各フォームコントロールの境界線の色を設定しています。
forms.css (status classes)
.ng-valid[required], .ng-valid.required { border-left: 5px solid #42A948; /* green */}.ng-invalid:not(form) { border-left: 5px solid #a94442; /* red */}.alert div { background-color: #fed3d3; color: #820000; padding: 1rem; margin-bottom: 1rem;}.form-group { margin-bottom: 1rem;}label { display: block; margin-bottom: .5rem;}select { width: 100%; padding: .5rem;}
クロスフィールド検証
クロスフィールドバリデーターは、フォーム内の異なるフィールドの値を比較し、組み合わせで受け入れるか拒否するカスタムバリデーターです。 たとえば、互いに非互換なオプションを提供するフォームがある場合、ユーザーはAまたはBを選択できますが、両方は選択できません。 フィールドの値によっては、他の値に依存する場合もあります。ユーザーは、Aを選択した場合にのみBを選択できます。
次のクロス検証の例は、次の方法を示しています。
- 2つの兄弟コントロールの値に基づいて、リアクティブまたはテンプレートベースのフォーム入力を検証する
- ユーザーがフォームとやり取りし、検証に失敗した場合に、説明的なエラーメッセージを表示する
これらの例では、クロス検証を使用して、アクターがアクターフォームに記入することで、役割で同じ名前を再利用しないようにしています。 バリデーターは、アクター名と役割が一致しないことを確認することで、これを実現します。
クロス検証をリアクティブフォームに追加する
フォームは、次の構造になっています。
const actorForm = new FormGroup({ 'name': new FormControl(), 'role': new FormControl(), 'skill': new FormControl()});
name
とrole
は兄弟コントロールであることに注意してください。
1つのカスタムバリデーターで両方のコントロールを評価するには、共通の祖先コントロールであるFormGroup
で検証する必要があります。
子コントロールを取得するためにFormGroup
をクエリして、値を比較します。
FormGroup
にバリデーターを追加するには、作成時に第2引数として新しいバリデーターを渡します。
const actorForm = new FormGroup({ 'name': new FormControl(), 'role': new FormControl(), 'skill': new FormControl()}, { validators: unambiguousRoleValidator });
バリデーターのコードは次のとおりです。
shared/unambiguous-role.directive.ts
import {Directive} from '@angular/core';import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn,} from '@angular/forms';/** An actor's name can't match the actor's role */export const unambiguousRoleValidator: ValidatorFn = ( control: AbstractControl,): ValidationErrors | null => { const name = control.get('name'); const role = control.get('role'); return name && role && name.value === role.value ? {unambiguousRole: true} : null;};@Directive({ selector: '[appUnambiguousRole]', providers: [ {provide: NG_VALIDATORS, useExisting: UnambiguousRoleValidatorDirective, multi: true}, ],})export class UnambiguousRoleValidatorDirective implements Validator { validate(control: AbstractControl): ValidationErrors | null { return unambiguousRoleValidator(control); }}
unambiguousRoleValidator
バリデーターは、ValidatorFn
インターフェースを実装しています。
これはAngularコントロールオブジェクトを引数として受け取り、フォームが有効な場合はnull
を返し、無効な場合はValidationErrors
を返します。
バリデーターは、FormGroup
のgetメソッドを呼び出して子コントロールを取得し、name
コントロールとrole
コントロールの値を比較します。
値が一致しない場合、役割は曖昧ではなく、両方が有効で、バリデーターはnull
を返します。
値が一致する場合、アクターの役割は曖昧で、バリデーターはエラーオブジェクトを返すことでフォームを無効にする必要があります。
より良いユーザーエクスペリエンスを提供するために、フォームが無効な場合、テンプレートに適切なエラーメッセージが表示されます。
reactive/actor-form-template.component.html
<div class="container"> <h2>Reactive Form</h2> <form [formGroup]="actorForm" #formDir="ngForm"> <div [hidden]="formDir.submitted"> <div class="cross-validation" [class.cross-validation-error]="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)"> <div class="form-group"> <label for="name">Name</label> <input type="text" id="name" class="form-control" formControlName="name" required> <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert alert-danger"> <div *ngIf="name.hasError('required')"> Name is required. </div> <div *ngIf="name.hasError('minlength')"> Name must be at least 4 characters long. </div> <div *ngIf="name.hasError('forbiddenName')"> Name cannot be Bob. </div> </div> </div> <div class="form-group"> <label for="role">Role</label> <input type="text" id="role" class="form-control" formControlName="role"> <div *ngIf="role.pending">Validating...</div> <div *ngIf="role.invalid" class="alert alert-danger role-errors"> <div *ngIf="role.hasError('uniqueRole')"> Role is already taken. </div> </div> </div> <div *ngIf="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)" class="cross-validation-error-message alert alert-danger"> Name cannot match role or audiences will be confused. </div> </div> <div class="form-group"> <label for="skill">Skill</label> <select id="skill" class="form-control" formControlName="skill" required> <option *ngFor="let skill of skills" [value]="skill">{{ skill }}</option> </select> <div *ngIf="skill.invalid && skill.touched" class="alert alert-danger"> <div *ngIf="skill.hasError('required')">Skill is required.</div> </div> </div> <p>Complete the form to enable the Submit button.</p> <button type="submit" class="btn btn-default" [disabled]="actorForm.invalid">Submit</button> <button type="button" class="btn btn-default" (click)="formDir.resetForm({})">Reset</button> </div> </form> <div class="submitted-message" *ngIf="formDir.submitted"> <p>You've submitted your actor, {{ actorForm.value.name }}!</p> <button type="button" (click)="formDir.resetForm({})">Add new actor</button> </div></div>
この*ngIf
は、FormGroup
にunambiguousRoleValidator
バリデーターが返したクロス検証エラーがある場合に、エラーを表示しますが、ユーザーがフォームとやり取りを完了した場合のみです。
クロス検証をテンプレート駆動フォームに追加する
テンプレート駆動フォームの場合、バリデーター関数をラップするディレクティブを作成する必要があります。
次の例に示すように、NG_VALIDATORS
トークンを使用して、そのディレクティブをバリデーターとして提供します。
shared/unambiguous-role.directive.ts
import {Directive} from '@angular/core';import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn,} from '@angular/forms';/** An actor's name can't match the actor's role */export const unambiguousRoleValidator: ValidatorFn = ( control: AbstractControl,): ValidationErrors | null => { const name = control.get('name'); const role = control.get('role'); return name && role && name.value === role.value ? {unambiguousRole: true} : null;};@Directive({ selector: '[appUnambiguousRole]', providers: [ {provide: NG_VALIDATORS, useExisting: UnambiguousRoleValidatorDirective, multi: true}, ],})export class UnambiguousRoleValidatorDirective implements Validator { validate(control: AbstractControl): ValidationErrors | null { return unambiguousRoleValidator(control); }}
新しいディレクティブをHTMLテンプレートに追加する必要があります。
バリデーターはフォームの最上位レベルで登録する必要があるため、次のテンプレートはform
タグにディレクティブを配置しています。
template/actor-form-template.component.html
<div> <h2>Template-Driven Form</h2> <form #actorForm="ngForm" appUnambiguousRole> <div [hidden]="actorForm.submitted"> <div class="cross-validation" [class.cross-validation-error]="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)"> <div class="form-group"> <label for="name">Name</label> <input type="text" id="name" name="name" class="form-control" required minlength="4" appForbiddenName="bob" [(ngModel)]="actor.name" #name="ngModel"> <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert"> <div *ngIf="name.hasError('required')"> Name is required. </div> <div *ngIf="name.hasError('minlength')"> Name must be at least 4 characters long. </div> <div *ngIf="name.hasError('forbiddenName')"> Name cannot be Bob. </div> </div> </div> <div class="form-group"> <label for="role">Role</label> <input type="text" id="role" name="role" #role="ngModel" [(ngModel)]="actor.role" [ngModelOptions]="{ updateOn: 'blur' }" appUniqueRole> <div *ngIf="role.pending">Validating...</div> <div *ngIf="role.invalid" class="alert role-errors"> <div *ngIf="role.hasError('uniqueRole')"> Role is already taken. </div> </div> </div> <div *ngIf="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)" class="cross-validation-error-message alert"> Name cannot match role. </div> </div> <div class="form-group"> <label for="skill">Skill</label> <select id="skill" name="skill" required [(ngModel)]="actor.skill" #skill="ngModel"> <option *ngFor="let skill of skills" [value]="skill">{{ skill }}</option> </select> <div *ngIf="skill.errors && skill.touched" class="alert"> <div *ngIf="skill.errors['required']">Power is required.</div> </div> </div> <p>Complete the form to enable the Submit button.</p> <button type="submit" [disabled]="actorForm.invalid">Submit</button> <button type="button" (click)="actorForm.resetForm({})">Reset</button> </div> <div class="submitted-message" *ngIf="actorForm.submitted"> <p>You've submitted your actor, {{ actorForm.value.name }}!</p> <button type="button" (click)="actorForm.resetForm({})">Add new actor</button> </div> </form></div>
より良いユーザーエクスペリエンスを提供するために、フォームが無効な場合、適切なエラーメッセージが表示されます。
template/actor-form-template.component.html
<div> <h2>Template-Driven Form</h2> <form #actorForm="ngForm" appUnambiguousRole> <div [hidden]="actorForm.submitted"> <div class="cross-validation" [class.cross-validation-error]="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)"> <div class="form-group"> <label for="name">Name</label> <input type="text" id="name" name="name" class="form-control" required minlength="4" appForbiddenName="bob" [(ngModel)]="actor.name" #name="ngModel"> <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert"> <div *ngIf="name.hasError('required')"> Name is required. </div> <div *ngIf="name.hasError('minlength')"> Name must be at least 4 characters long. </div> <div *ngIf="name.hasError('forbiddenName')"> Name cannot be Bob. </div> </div> </div> <div class="form-group"> <label for="role">Role</label> <input type="text" id="role" name="role" #role="ngModel" [(ngModel)]="actor.role" [ngModelOptions]="{ updateOn: 'blur' }" appUniqueRole> <div *ngIf="role.pending">Validating...</div> <div *ngIf="role.invalid" class="alert role-errors"> <div *ngIf="role.hasError('uniqueRole')"> Role is already taken. </div> </div> </div> <div *ngIf="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)" class="cross-validation-error-message alert"> Name cannot match role. </div> </div> <div class="form-group"> <label for="skill">Skill</label> <select id="skill" name="skill" required [(ngModel)]="actor.skill" #skill="ngModel"> <option *ngFor="let skill of skills" [value]="skill">{{ skill }}</option> </select> <div *ngIf="skill.errors && skill.touched" class="alert"> <div *ngIf="skill.errors['required']">Power is required.</div> </div> </div> <p>Complete the form to enable the Submit button.</p> <button type="submit" [disabled]="actorForm.invalid">Submit</button> <button type="button" (click)="actorForm.resetForm({})">Reset</button> </div> <div class="submitted-message" *ngIf="actorForm.submitted"> <p>You've submitted your actor, {{ actorForm.value.name }}!</p> <button type="button" (click)="actorForm.resetForm({})">Add new actor</button> </div> </form></div>
これは、テンプレート駆動フォームとリアクティブフォームの両方で同じです。
非同期バリデーターの作成
非同期バリデーターは、AsyncValidatorFn
とAsyncValidator
インターフェースを実装します。
これらは、同期バリデーターと非常に似ており、次の点が異なります。
validate()
関数はPromiseまたはオブザーバブルを返す必要があります。- 返されるオブザーバブルは有限である必要があります。つまり、ある時点で完了する必要があります。
無限のオブザーバブルを有限のオブザーバブルに変換するには、オブザーバブルを
first
、last
、take
、takeUntil
などのフィルタリング演算子でパイプします。
非同期検証は、同期検証の後に実行され、同期検証が成功した場合にのみ実行されます。 このチェックにより、フォームは、基本的な検証方法がすでに無効な入力を検出している場合、潜在的にコストのかかる非同期検証プロセス(HTTPリクエストなど)を回避できます。
非同期検証が開始されると、フォームコントロールはpending
状態になります。
コントロールのpending
プロパティを調べ、それを利用して、進行中の検証操作に関する視覚的なフィードバックを提供します。
一般的なUIパターンは、非同期検証の実行中にスピナーを表示することです。 次の例は、テンプレート駆動フォームでこれを実現する方法を示しています。
<input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator><app-spinner *ngIf="model.pending"></app-spinner>
カスタム非同期バリデーターの実装
次の例では、非同期バリデーターは、アクターがすでに割り当てられている役割にキャストされないようにします。 新しいアクターは常にオーディションを受けており、古いアクターは引退しているため、利用可能な役割のリストを事前に取得はできません。 潜在的な役割のエントリを検証するために、バリデーターは、現在キャストされているすべてのアクターの中央データベースを照会する非同期操作を開始する必要があります。
次のコードは、AsyncValidator
インターフェースを実装するバリデータークラスUniqueRoleValidator
を作成します。
import {Directive, forwardRef, Injectable} from '@angular/core';import { AsyncValidator, AbstractControl, NG_ASYNC_VALIDATORS, ValidationErrors,} from '@angular/forms';import {catchError, map} from 'rxjs/operators';import {ActorsService} from './actors.service';import {Observable, of} from 'rxjs';@Injectable({providedIn: 'root'})export class UniqueRoleValidator implements AsyncValidator { constructor(private actorsService: ActorsService) {} validate(control: AbstractControl): Observable<ValidationErrors | null> { return this.actorsService.isRoleTaken(control.value).pipe( map((isTaken) => (isTaken ? {uniqueRole: true} : null)), catchError(() => of(null)), ); }}@Directive({ selector: '[appUniqueRole]', providers: [ { provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => UniqueRoleValidatorDirective), multi: true, }, ],})export class UniqueRoleValidatorDirective implements AsyncValidator { constructor(private validator: UniqueRoleValidator) {} validate(control: AbstractControl): Observable<ValidationErrors | null> { return this.validator.validate(control); }}
コンストラクターは、次のインターフェースを定義するActorsService
を注入します。
interface ActorsService { isRoleTaken: (role: string) => Observable<boolean>;}
実際のアプリケーションでは、ActorsService
は、アクターデータベースにHTTPリクエストを送信して役割が利用可能かどうかを確認する役割を担います。
バリデーターの観点から、サービスの実際の実装は重要でないため、例ではActorsService
インターフェースに対してのみコードを作成できます。
検証が始まると、UnambiguousRoleValidator
は、現在のコントロール値でActorsService
のisRoleTaken()
メソッドに委任します。
この時点で、コントロールはpending
としてマークされ、validate()
メソッドから返されるObservableチェーンが完了するまで、この状態を維持します。
isRoleTaken()
メソッドは、役割が利用可能かどうかを確認するHTTPリクエストをディスパッチし、結果としてObservable<boolean>
を返します。
validate()
メソッドは、応答をmap
演算子でパイプし、検証結果に変換します。
その後、メソッドは、他のバリデーターと同様に、フォームが有効な場合はnull
を返し、無効な場合はValidationErrors
を返します。
このバリデーターは、catchError
演算子を使用して、潜在的なエラーを処理します。
この場合、バリデーターはisRoleTaken()
エラーを正常な検証として扱います。検証リクエストの実行に失敗したとしても、役割が無効であるとは限りません。
エラーを異なる方法で処理し、代わりにValidationError
オブジェクトを返すこともできます。
しばらくすると、Observableチェーンが完了し、非同期検証が完了します。
pending
フラグはfalse
に設定され、フォームの有効性が更新されます。
非同期バリデーターをリアクティブフォームに追加する
リアクティブフォームで非同期バリデーターを使用するには、最初にバリデーターをコンポーネントクラスのコンストラクターに注入します。
import {Component, OnInit} from '@angular/core';import {FormControl, FormGroup, Validators} from '@angular/forms';import {forbiddenNameValidator} from '../shared/forbidden-name.directive';import {UniqueRoleValidator} from '../shared/role.directive';@Component({ selector: 'app-actor-form-reactive', templateUrl: './actor-form-reactive.component.html', styleUrls: ['./actor-form-reactive.component.css'],})export class HeroFormReactiveComponent implements OnInit { skills = ['Method Acting', 'Singing', 'Dancing', 'Swordfighting']; actor = {name: 'Tom Cruise', role: 'Romeo', skill: this.skills[3]}; actorForm!: FormGroup; ngOnInit(): void { const roleControl = new FormControl('', { asyncValidators: [this.roleValidator.validate.bind(this.roleValidator)], updateOn: 'blur', }); roleControl.setValue(this.actor.role); this.actorForm = new FormGroup({ name: new FormControl(this.actor.name, [ Validators.required, Validators.minLength(4), forbiddenNameValidator(/bob/i), ]), role: roleControl, skill: new FormControl(this.actor.skill, Validators.required), }); } get name() { return this.actorForm.get('name'); } get skill() { return this.actorForm.get('skill'); } get role() { return this.actorForm.get('role'); } constructor(private roleValidator: UniqueRoleValidator) {}}
次に、バリデーター関数をFormControl
に直接渡して、適用します。
次の例では、UnambiguousRoleValidator
のvalidate
関数が、roleControl
に適用されています。この関数をコントロールのasyncValidators
オプションに渡し、ActorFormReactiveComponent
に注入されたUnambiguousRoleValidator
のインスタンスにバインドしています。
asyncValidators
の値は、単一の非同期バリデーター関数、または関数の配列にすることができます。
FormControl
オプションの詳細については、AbstractControlOptions APIリファレンスを参照してください。
import {Component, OnInit} from '@angular/core';import {FormControl, FormGroup, Validators} from '@angular/forms';import {forbiddenNameValidator} from '../shared/forbidden-name.directive';import {UniqueRoleValidator} from '../shared/role.directive';@Component({ selector: 'app-actor-form-reactive', templateUrl: './actor-form-reactive.component.html', styleUrls: ['./actor-form-reactive.component.css'],})export class HeroFormReactiveComponent implements OnInit { skills = ['Method Acting', 'Singing', 'Dancing', 'Swordfighting']; actor = {name: 'Tom Cruise', role: 'Romeo', skill: this.skills[3]}; actorForm!: FormGroup; ngOnInit(): void { const roleControl = new FormControl('', { asyncValidators: [this.roleValidator.validate.bind(this.roleValidator)], updateOn: 'blur', }); roleControl.setValue(this.actor.role); this.actorForm = new FormGroup({ name: new FormControl(this.actor.name, [ Validators.required, Validators.minLength(4), forbiddenNameValidator(/bob/i), ]), role: roleControl, skill: new FormControl(this.actor.skill, Validators.required), }); } get name() { return this.actorForm.get('name'); } get skill() { return this.actorForm.get('skill'); } get role() { return this.actorForm.get('role'); } constructor(private roleValidator: UniqueRoleValidator) {}}
非同期バリデーターをテンプレート駆動フォームに追加する
テンプレート駆動フォームで非同期バリデーターを使用するには、新しいディレクティブを作成し、そのディレクティブにNG_ASYNC_VALIDATORS
プロバイダーを登録します。
次の例では、ディレクティブは、実際の検証ロジックを含むUniqueRoleValidator
クラスを注入し、検証を実行する必要があるときにAngularによってトリガーされるvalidate
関数でそれを呼び出します。
import {Directive, forwardRef, Injectable} from '@angular/core';import { AsyncValidator, AbstractControl, NG_ASYNC_VALIDATORS, ValidationErrors,} from '@angular/forms';import {catchError, map} from 'rxjs/operators';import {ActorsService} from './actors.service';import {Observable, of} from 'rxjs';@Injectable({providedIn: 'root'})export class UniqueRoleValidator implements AsyncValidator { constructor(private actorsService: ActorsService) {} validate(control: AbstractControl): Observable<ValidationErrors | null> { return this.actorsService.isRoleTaken(control.value).pipe( map((isTaken) => (isTaken ? {uniqueRole: true} : null)), catchError(() => of(null)), ); }}@Directive({ selector: '[appUniqueRole]', providers: [ { provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => UniqueRoleValidatorDirective), multi: true, }, ],})export class UniqueRoleValidatorDirective implements AsyncValidator { constructor(private validator: UniqueRoleValidator) {} validate(control: AbstractControl): Observable<ValidationErrors | null> { return this.validator.validate(control); }}
その後、同期バリデーターと同様に、ディレクティブのセレクターを入力に追加して、アクティブ化します。
template/actor-form-template.component.html (unique-unambiguous-role-input)
<div> <h2>Template-Driven Form</h2> <form #actorForm="ngForm" appUnambiguousRole> <div [hidden]="actorForm.submitted"> <div class="cross-validation" [class.cross-validation-error]="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)"> <div class="form-group"> <label for="name">Name</label> <input type="text" id="name" name="name" class="form-control" required minlength="4" appForbiddenName="bob" [(ngModel)]="actor.name" #name="ngModel"> <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert"> <div *ngIf="name.hasError('required')"> Name is required. </div> <div *ngIf="name.hasError('minlength')"> Name must be at least 4 characters long. </div> <div *ngIf="name.hasError('forbiddenName')"> Name cannot be Bob. </div> </div> </div> <div class="form-group"> <label for="role">Role</label> <input type="text" id="role" name="role" #role="ngModel" [(ngModel)]="actor.role" [ngModelOptions]="{ updateOn: 'blur' }" appUniqueRole> <div *ngIf="role.pending">Validating...</div> <div *ngIf="role.invalid" class="alert role-errors"> <div *ngIf="role.hasError('uniqueRole')"> Role is already taken. </div> </div> </div> <div *ngIf="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)" class="cross-validation-error-message alert"> Name cannot match role. </div> </div> <div class="form-group"> <label for="skill">Skill</label> <select id="skill" name="skill" required [(ngModel)]="actor.skill" #skill="ngModel"> <option *ngFor="let skill of skills" [value]="skill">{{ skill }}</option> </select> <div *ngIf="skill.errors && skill.touched" class="alert"> <div *ngIf="skill.errors['required']">Power is required.</div> </div> </div> <p>Complete the form to enable the Submit button.</p> <button type="submit" [disabled]="actorForm.invalid">Submit</button> <button type="button" (click)="actorForm.resetForm({})">Reset</button> </div> <div class="submitted-message" *ngIf="actorForm.submitted"> <p>You've submitted your actor, {{ actorForm.value.name }}!</p> <button type="button" (click)="actorForm.resetForm({})">Add new actor</button> </div> </form></div>
非同期バリデーターのパフォーマンスの最適化
デフォルトでは、すべてのバリデーターは、フォームの値が変更されるたびに実行されます。 同期バリデーターの場合、これは通常、アプリケーションのパフォーマンスに目立った影響を与えません。 ただし、非同期バリデーターは通常、コントロールを検証するために何らかのHTTPリクエストを実行します。 キーストロークごとにHTTPリクエストをディスパッチすると、バックエンドAPIに負担がかかる可能性があり、可能な限り回避する必要があります。
updateOn
プロパティをchange
(デフォルト)からsubmit
またはblur
に変更することで、フォームの有効性の更新を遅らせることができます。
テンプレート駆動フォームでは、テンプレートでプロパティを設定します。
<input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}">
リアクティブフォームでは、FormControl
インスタンスでプロパティを設定します。
new FormControl('', {updateOn: 'blur'});
ネイティブHTMLフォーム検証との相互作用
デフォルトでは、Angularは囲んでいる<form>
にnovalidate
属性を追加することでネイティブHTMLフォーム検証を無効にし、これらの属性をフレームワーク内のバリデーター関数と一致させるためにディレクティブを使用します。
ネイティブ検証を組み合わせてAngularベースの検証を使用したい場合は、ngNativeValidate
ディレクティブを使用して、ネイティブ検証を再び有効にできます。
詳細については、APIドキュメントを参照してください。