詳細ガイド
フォーム

フォーム入力の検証

正確性と完全性を確保するために、ユーザー入力を検証することで、データ品質を全体的に向上させることができます。 このページでは、UIからのユーザー入力を検証し、リアクティブフォームとテンプレート駆動フォームの両方で、役立つ検証メッセージを表示する方法について説明します。

テンプレート駆動フォームでの入力検証

テンプレート駆動フォームに検証を追加するには、ネイティブHTMLフォーム検証の場合と同じように、検証属性を追加します。 Angularは、これらの属性をフレームワーク内のバリデーター関数と一致させるためにディレクティブを使用します。

フォームコントロールの値が変更されるたびに、Angularは検証し、検証エラーのリスト(INVALIDステータスをもたらす)またはnullVALIDステータスをもたらす)を生成します。

その後、ngModelをローカルテンプレート変数にエクスポートすることで、コントロールの状態を調べることができます。 次の例では、NgModelnameという変数にエクスポートします。

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検証属性(requiredminlength)があります。 また、カスタムバリデーターディレクティブforbiddenNameもあります。 詳細については、カスタムバリデーターセクションを参照してください。

  • #name="ngModel"は、NgModelnameというローカル変数にエクスポートします。 NgModelは、基になるFormControlインスタンスの多くのプロパティをミラーリングしているため、テンプレート内でこれを使用して、validdirtyなどのコントロールの状態を確認できます。 コントロールプロパティの完全なリストについては、AbstractControl APIリファレンスを参照してください。

    • <div>要素の*ngIfは、入れ子になったメッセージのdivのセットを明らかにしますが、nameが無効で、コントロールがdirtyまたはtouchedの場合のみです。

    • 各入れ子になった<div>は、考えられる検証エラーのいずれかについて、カスタムメッセージを表示できます。 requiredminlengthforbiddenNameのメッセージがあります。

HELPFUL: ユーザーがフォームを編集する機会がある前に、バリデーターがエラーを表示しないようにするには、コントロールのdirtyまたはtouched状態のいずれかをチェックする必要があります。

  • ユーザーが監視対象のフィールドの値を変更すると、コントロールは「dirty」としてマークされます。
  • ユーザーがフォームコントロール要素からフォーカスを外すと、コントロールは「touched」としてマークされます。

リアクティブフォームでの入力検証

リアクティブフォームでは、真実の源はコンポーネントクラスです。 テンプレートで属性を通じてバリデーターを追加する代わりに、コンポーネントクラスのフォームコントロールモデルに直接バリデーター関数を追加します。 その後、Angularは、コントロールの値が変更されるたびにこれらの関数を呼び出します。

バリデーター関数

バリデーター関数は、同期または非同期にすることができます。

バリデーターの種類 詳細
同期バリデーター コントロールインスタンスを受け取り、検証エラーのセットまたはnullをすぐに返す同期関数。FormControlをインスタンス化する際に、第2引数として渡します。
非同期バリデーター コントロールインスタンスを受け取り、後で検証エラーのセットまたはnullを発行するPromiseまたはObservableを返す非同期関数。FormControlをインスタンス化する際に、第3引数として渡します。

パフォーマンス上の理由から、Angularは、すべての同期バリデーターが合格した場合にのみ非同期バリデーターを実行します。 各バリデーターは、エラーが設定される前に完了する必要があります。

組み込みのバリデーター関数

独自のバリデーター関数を作成することも、Angularの組み込みのバリデーターのいくつかを使用することもできます。

requiredminlengthなど、テンプレート駆動フォームで属性として使用できるものと同じ組み込みバリデーターはすべて、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.requiredValidators.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」にバインドされています。

useExistinguseClassに置き換えると、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()});

nameroleは兄弟コントロールであることに注意してください。 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を返します。

バリデーターは、FormGroupgetメソッドを呼び出して子コントロールを取得し、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は、FormGroupunambiguousRoleValidatorバリデーターが返したクロス検証エラーがある場合に、エラーを表示しますが、ユーザーがフォームとやり取りを完了した場合のみです。

クロス検証をテンプレート駆動フォームに追加する

テンプレート駆動フォームの場合、バリデーター関数をラップするディレクティブを作成する必要があります。 次の例に示すように、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>

これは、テンプレート駆動フォームとリアクティブフォームの両方で同じです。

非同期バリデーターの作成

非同期バリデーターは、AsyncValidatorFnAsyncValidatorインターフェースを実装します。 これらは、同期バリデーターと非常に似ており、次の点が異なります。

  • validate()関数はPromiseまたはオブザーバブルを返す必要があります。
  • 返されるオブザーバブルは有限である必要があります。つまり、ある時点で完了する必要があります。 無限のオブザーバブルを有限のオブザーバブルに変換するには、オブザーバブルをfirstlasttaketakeUntilなどのフィルタリング演算子でパイプします。

非同期検証は、同期検証の後に実行され、同期検証が成功した場合にのみ実行されます。 このチェックにより、フォームは、基本的な検証方法がすでに無効な入力を検出している場合、潜在的にコストのかかる非同期検証プロセス(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は、現在のコントロール値でActorsServiceisRoleTaken()メソッドに委任します。 この時点で、コントロールは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に直接渡して、適用します。

次の例では、UnambiguousRoleValidatorvalidate関数が、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ドキュメントを参照してください。