angular

angular.io Guide: Form Validation

この記事は、Angular の公式ドキュメントの Form Validation の章 を意訳したものです。
駆け足で翻訳したので至らない点もありますが、あしからずご承知おきください。

バージョン 4.3.1 のドキュメントをベースにしています。

Form Validation

正確さと完全性のためにユーザー入力をバリデーションすることにより、全体的なデータ品質を向上させましょう。

このページでは、UIでユーザー入力のバリデーションを実施するのにあたり、最初にTemplate-Drivenフォームを用います。続いてReactiveフォームを用いしてvalid時のバリデーションメッセージを表示する方法を示します。最終的なプロジェクトのフォルダ構造は次のようになります。

これらの選択肢の詳細については、フォームおよびReactive形式ガイドを参照してください。

実例を試して、cookbookの完全なソースコードを見てダウンロードしてください。

plunker.png

こちらからダウンロードもできます。

シンプルなTemplate-Drivenフォーム

Template-Drivenフォームでの実装アプローチですが、まずはじめにフォームの要素をコンポーネントのテンプレートに配置します。

Angularは、フォーム機能を実装する対応する内部コントロールモデルを構築するために、Angularフォームディレクティブ(ほとんどがディレクティブの先頭...)を追加します。 Template-Drivenフォームでは、テンプレートに制御モデルが暗黙的に含まれています。

ユーザー入力をバリデーションするために、要素にHTMLバリデーション属性を追加します。 Angularはそれらを同様に解釈し、バリデータ関数をコントロールモデルに追加します。

Angularは、ユーザーがコントロールを「タッチ」したかどうか、コントロールを変更したかどうか、コントロール値が有効かどうかなど、コントロールの状態に関する情報を公開します。

この最初のテンプレートバリデーションの例では、コントロールの状態を読み取り、適切に表示を更新するHTMLを確認します。 ヒストリー名にバインドされた単一の入力コントロールのテンプレートHTMLからの抜粋です:

template/hero-form-template1.component.html (Hero name)

<label for="name">Name</label>

<input type="text" id="name" class="form-control"
       required minlength="4" maxlength="24"
       name="name" [(ngModel)]="hero.name"
       #name="ngModel" >

<div *ngIf="name.errors && (name.dirty || name.touched)"
     class="alert alert-danger">
    <div [hidden]="!name.errors.required">
      Name is required
    </div>
    <div [hidden]="!name.errors.minlength">
      Name must be at least 4 characters long.
    </div>
    <div [hidden]="!name.errors.maxlength">
      Name cannot be more than 24 characters long.
    </div>
</div>

次の点に注意してください。

  • <input>要素には、バリデーションを実施するための属性として、requiredminlengthmaxlengthが含まれています。
  • 入力のname属性は"name"に設定されているので、Angularはこの入力要素を追跡し、内部制御モデルでnameというAngularフォームコントロールに関連付けることができます。
  • [(ngModel)] ディレクティブは、入力ボックスとhero.nameプロパティ間の双方向データバインディングを可能にします。
  • テンプレート変数(#name)の値は"ngModel"(常にngModel)です。これは、このコントロールに関連付けられているAngular NgModelディレクティブへの参照を提供します。このディレクティブは、テンプレートでvaliddirtyといったコントロール状態をチェックするために使用できます。
  • <div>要素の*ngIfは、ネストされたメッセージdivのセットを示しますが、名前エラーがあり、コントロールが汚れているか、またはタッチされている場合のみです。
  • ネストされた各<div>は、考えられるバリデーションエラーの1つに対してカスタムメッセージを表示できます。 requiredminlength、およびmaxlengthのメッセージがあります。

フルテンプレートは、フォーム上の各データ入力コントロールに対してこの種のレイアウトを繰り返します。

なぜ dirty や touched のチェックをするの?

ユーザーが値を編集する前からバリデーションエラーのメッセージが表示されるのは良くないことです。dirty, touched のチェックを行うことで、この早すぎるエラー表示を防ぐのに有効です。

dirty, touched については、Formsのガイドで学ぶことができます。

コンポーネントクラスは、データバインディングで使用されるヒーローモデルとビューをサポートする他のコードを管理します。

template/hero-form-template1.component.ts (class)

export class HeroFormTemplate1Component {

  powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];

  hero = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What');

  submitted = false;

  onSubmit() {
    this.submitted = true;
  }

  addHero() {
    this.hero = new Hero(42, '', '');
  }
}

シンプルな標準バリデーションルールを使用して静的フォームを操作する場合は、このTemplate-Drivenバリデーションメソッドを使用します。

Template-DrivenアプローチのHeroFormTemplateCompononentの最初のバージョンの完全なファイルは次のとおりです。

コードでバリデーションメッセージを含むTemplate-Drivenフォーム

レイアウトは簡単ですが、バリデーションメッセージを処理する方法には明らかな欠点があります。

可能性のあるすべてのエラー条件を表すには多くのHTMLが必要です。 これは多くのコントロールと多くのバリデーションルールがある場合、辛みが増して手を抜きたくなってくるでしょう。

HTMLには多くのJavaScriptロジックがあります。
メッセージは静的な文字列で、テンプレートにハードコードされていますが、コンポーネントクラスで動的メッセージを生成できた方が簡単ですよね。
この例では、テンプレートとコンポーネントを少し変更して、ロジックとメッセージをコンポーネントに移動できます。

元のバージョンの隣に、修正されたテンプレート(テンプレート2)から抜粋したヒーローの名前が再びあります:

(コードは省略)

<input> 要素HTMLはほぼ同じです。 注目すべき違いがあります。

  • ハードコードのエラーメッセージ <divs> はなくなりました。
  • 実際にカスタムバリデーションディレクティブである新しい属性、forbiddenName があります。 ユーザーが<input>の名前に "bob" と入力した場合(コントロールを無効にする)、コントロールは無効になります。 カスタムバリデーションディレクティブの詳細については、このページの後のカスタムバリデーションセクションを参照してください。
  • アプリはこの要素のAngular側のコントロールを参照していないため、#name テンプレート変数はなくなりました。
  • 新しい formErrors.name プロパティにバインドするだけで、すべての名前バリデーションエラーメッセージを表示できます。

Component クラス

テンプレート1の元のコンポーネントコードは同じままでした。 ただし、テンプレート2では、コンポーネントの変更が必要です。 このセクションではAngularのフォームコントロールを取得してエラーメッセージを作成するために、テンプレート2のコンポーネントクラスに必要なコードについて説明します。

最初のステップは、Angularがテンプレートからクエリを作成して作成したフォームコントロールを取得することです。

<form> 要素の #heroForm テンプレート変数で、コンポーネントテンプレートの上部を見てみましょう。

template/hero-form-template1.component.html (form tag)

<form #heroForm="ngForm"  *ngIf="active"  (ngSubmit)="onSubmit()">

heroForm変数は、テンプレートから派生したAngularのコントロールモデルへの参照です。 @ViewChildクエリを使用して、そのモデルをコンポーネントクラスのcurrentFormプロパティに挿入するには、下記のようにします:

emplate/hero-form-template2.component.ts (heroForm)

heroForm: NgForm;
@ViewChild('heroForm') currentForm: NgForm;

ngAfterViewChecked() {
  this.formChanged();
}

formChanged() {
  if (this.currentForm === this.heroForm) { return; }
  this.heroForm = this.currentForm;
  if (this.heroForm) {
    this.heroForm.valueChanges
      .subscribe(data => this.onValueChanged(data));
  }
}

いくつかの観察:

  • Angular@ViewChildは、変数の名前に文字列(この場合は 'heroForm')を渡すとテンプレート変数をクエリします。
  • heroFormオブジェクトは、コンポーネントの使用中に何度か変更されます。特に、新しいヒーローを追加すると顕著になります。 それを定期的に検査することにより、これらの変化が明らかになる。
  • Angularは、ビュー内の何かが変更されたときに、ngAfterViewChecked()ライフサイクルフックメソッドを呼び出します。 それは、新しいheroFormオブジェクトがあるかどうかを確認するのに最適なタイミングです。
  • 新しいheroFormモデルがある場合、formChanged()valueChanges Observableプロパティにサブスクライブします。 onValueChangedハンドラは、すべてのキーストローク後にバリデーションエラーを探します。

template/hero-form-template2.component.ts (handler)

onValueChanged(data?: any) {
  if (!this.heroForm) { return; }
  const form = this.heroForm.form;

  for (const field in this.formErrors) {
    // clear previous error message (if any)
    this.formErrors[field] = '';
    const control = form.get(field);

    if (control && control.dirty && !control.valid) {
      const messages = this.validationMessages[field];
      for (const key in control.errors) {
        this.formErrors[field] += messages[key] + ' ';
      }
    }
  }
}

formErrors = {
  'name': '',
  'power': ''
};

onValueChangedハンドラは、ユーザデータエントリを解釈します。 ハンドラに渡されるデータオブジェクトには、現在の要素値が含まれます。 ハンドラはそれらを無視します。 代わりに、コンポーネントのformErrorsオブジェクトのフィールドを反復処理します。

formErrorsは、バリデーションルールと現在のエラーメッセージを持つヒーローフィールドの辞書のようなものです。 2つのヒーロープロパティのみがバリデーションルール、名前、パワーを持っています。 ヒーローデータが有効な場合、メッセージは空の文字列です。

各フィールドについて、onValueChangedハンドラは次の処理を行います。

  • 以前のエラーメッセージがあればそれをクリアします。
  • フィールドの対応するAngular formsコントロールを取得します。
  • そのようなコントロールが存在し、そのコントロールが変更されている("dirty")場合、そのコントロールは無効です。ハンドラーは、コントロールのすべてのエラーの統合エラーメッセージを作成します。

次に、コンポーネントはいくつかのエラーメッセージを必要とします。バリデーションメッセージごとに1つのメッセージを持つ、バリデーションされた各プロパティのセットです。

template/hero-form-template2.component.ts (messages)

validationMessages = {
  'name': {
    'required':      'Name is required.',
    'minlength':     'Name must be at least 4 characters long.',
    'maxlength':     'Name cannot be more than 24 characters long.',
    'forbiddenName': 'Someone named "Bob" cannot be a hero.'
  },
  'power': {
    'required': 'Power is required.'
  }
};

ユーザーが変更を行うたびに、onValueChangedハンドラはバリデーションエラーをチェックし、それに応じてメッセージを生成します。

コード内のメッセージの利点

明らかに、テンプレートは実質的に小さくなり、コンポーネントコードは実質的に大きくなりました。フィールドが3つしかなく、そのうち2つだけにバリデーションルールを適用するだけでは、メリットを見いだすのは容易ではないかもしれません。

バリデーションされたフィールドとルールの数が増えるにつれて何が起こるかを考えてください。一般に、HTMLはコード側(=JavaScript)よりも読解とメンテナンスが難しいです。最初のテンプレートはすでに大規模であり、バリデーションメッセージ<div>要素を追加すると一気に複雑化する可能性があります。

バリデーションメッセージをコンポーネントに移動した結果、テンプレートはより緩やかに比例して大きくなります。各フィールドには、バリデーションルールの数に関係なく、ほぼ同じ数の行があります。このコンポーネントは、有効なフィールドごとに1行、バリデーションメッセージごとに1行の割合で比例して増加します。

バリデーションロジックがコード側に移ったので、柔軟性が増し、より効率的にメッセージを作成できます。コンポーネントからメッセージをリファクタリングすることができます。おそらく、サーバからメッセージを取得するサービスクラスにメッセージをアサインしてリファクタリングすることもできます。

要するに、テキストとロジックがテンプレートからコードに移行した今、メッセージ処理を改善する機会が増えたのではないでしょうか。

FormModuleとTemplate-Drivenフォーム

Angularにはフォーム開発の2つのアプローチに対応するFormsModuleReactiveFormsModuleという2つの異なるフォームモジュールがあります。どちらのモジュールも同じ @anglar/forms ライブラリパッケージから提供されています。

これからのセクションでは、まずはじめにFormsModuleを必要とするTemplate-Driven フォーム アプローチを検討してみます。これをHeroFormTemplateModuleにインポートした方法は次のとおりです。

template/hero-form-template.module.ts

import { NgModule }     from '@angular/core';
import { FormsModule }  from '@angular/forms';

import { SharedModule }               from '../shared/shared.module';
import { HeroFormTemplate1Component } from './hero-form-template1.component';
import { HeroFormTemplate2Component } from './hero-form-template2.component';

@NgModule({
  imports:      [ SharedModule, FormsModule ],
  declarations: [ HeroFormTemplate1Component, HeroFormTemplate2Component ],
  exports:      [ HeroFormTemplate1Component, HeroFormTemplate2Component ]
})
export class HeroFormTemplateModule { }

このガイドでは、このCookbookの各フォームテンプレートの下部に表示されるSharedModuleまたはSubmittedComponentについては説明していません。

彼らはバリデーションチェックの話には密接な関係がありません。あなたが興味を持っているなら、ライブの例を見てください。

コード内でバリデーションを行えるReactiveフォーム

Template-Driven型アプローチでは、Angular FormsModuleのフォーム要素、バリデーション属性、およびng...ディレクティブをテンプレートにマークアップします。実行時に、Angularはテンプレートを解釈し、フォーム制御モデルを導出します。

Reactive Formsは異なるアプローチを採用しています。(Componentの)コード側でフォームコントロールモデルを作成します。Angular ReactiveFormsModuleからフォーム要素とフォーム...ディレクティブを含むテンプレートを作成します。実行時に、Angularは指示に基づいてテンプレート要素をコントロールモデルにバインドします。

これにより、次のことが可能になります。

  • バリデーション機能のすぐに追加、変更、削除できます。
  • コンポーネント内からコントロールモデルを動的に操作できます。
  • バリデーションおよびコントロールロジックの独立した単体テストができるようになります。

次のサンプルでは、​​ヒーローフォームをReactiveフォームスタイルで書き直しています。

ReactiveFormsModule に切り替える

Reactive Formsクラスとディレクティブは、Angular ReactiveFormsModuleから提供されているため、FormsModuleは使わなくなります。このサンプルのReactiveフォーム機能のアプリケーションモジュールは、次のようになります。

src/app/reactive/hero-form-reactive.module.ts

import { NgModule }            from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';

import { SharedModule }              from '../shared/shared.module';
import { HeroFormReactiveComponent } from './hero-form-reactive.component';

@NgModule({
  imports:      [ SharedModule, ReactiveFormsModule ],
  declarations: [ HeroFormReactiveComponent ],
  exports:      [ HeroFormReactiveComponent ]
})
export class HeroFormReactiveModule { }

Reactive Forms feature モジュールとコンポーネントは、src/app/reactiveフォルダにあります。HeroFormReactiveComponentにそのテンプレートから開始します。

Component テンプレート

まず、<form> タグを変更して、テンプレートのAngular formGroup ディレクティブをコンポーネントクラスの heroForm プロパティにバインドします。heroForm は、コンポーネントクラスが構築し維持するコントロールモデルです。

form-validation/src/app/reactive/hero-form-reactive.component.html

<form [formGroup]="heroForm" *ngIf="active" (ngSubmit)="onSubmit()">

次に、テンプレートHTML要素をReactive Formsスタイルに合わせて変更します。テンプレートの「名前」部分は、Reactiveフォームで改訂され、Template Drivenバージョンと比較されます。

hero-form-reactive.component.html (name #3)

<label for="name">Name</label>

<input type="text" id="name" class="form-control"
       formControlName="name" required >

<div *ngIf="formErrors.name" class="alert alert-danger">
  {{ formErrors.name }}
</div>

hero-form-template1.component.html (name #2)

<label for="name">Name</label>

<input type="text" id="name" class="form-control"
       required minlength="4" maxlength="24" forbiddenName="bob"
       name="name" [(ngModel)]="hero.name" >

<div *ngIf="formErrors.name" class="alert alert-danger">
  {{ formErrors.name }}
</div>

主な変更点は次のとおりです。

コードでバリデーションが行われるため、バリデーション属性はなくなります(必須ではありません)。
バリデーションの目的ではなく、(コード内にある)CSSのスタイリングとアクセシビリティのために残しています。

現在のところ、Reactive Formsは、コントロールに必要なバリデータ関数がある場合、必須または必須のHTMLバリデーション属性をDOM要素に追加しません。

それまでは、必要な属性を適用して、次に示すように、Validator.required関数をコントロールモデルに追加します。

  • formControlNamename属性を置き換えます。入力とAngular formの制御を関連付けるのと同じ目的を果たします。
  • 双方向の [(ngModel)] バインディングはなくなりました。 Reactiveアプローチでは、データをフォームコントロールの内外に移動するためのデータバインディングは使用されません。これがすべてのコードなのです。

Component クラス

コンポーネントクラスは、フォームコントロールモデルの定義と管理を担当するようになりました。

Angularは、もはやテンプレートからコントロールモデルを派生しないため、もはやそれを問い合わせることができません。 Angularフォームコントロールモデルは、FormBuilderクラスを使用して明示的に作成できます。

このプロセスに捧げられたコードのセクションは、それが置き換えられるTemplate Drivenコードとペアになっています:

reactive/hero-form-reactive.component.ts (FormBuilder)

heroForm: FormGroup;
constructor(private fb: FormBuilder) { }

ngOnInit(): void {
  this.buildForm();
}

buildForm(): void {
  this.heroForm = this.fb.group({
    'name': [this.hero.name, [
        Validators.required,
        Validators.minLength(4),
        Validators.maxLength(24),
        forbiddenNameValidator(/bob/i)
      ]
    ],
    'alterEgo': [this.hero.alterEgo],
    'power':    [this.hero.power, Validators.required]
  });

  this.heroForm.valueChanges
    .subscribe(data => this.onValueChanged(data));

  this.onValueChanged(); // (re)set validation messages now
}

template/hero-form-template2.component.ts (ViewChild)

heroForm: NgForm;
@ViewChild('heroForm') currentForm: NgForm;

ngAfterViewChecked() {
  this.formChanged();
}

formChanged() {
  if (this.currentForm === this.heroForm) { return; }
  this.heroForm = this.currentForm;
  if (this.heroForm) {
    this.heroForm.valueChanges
      .subscribe(data => this.onValueChanged(data));
  }
}

FormBuilderをコンストラクタに挿入します。

ngOnInitライフサイクルフックメソッドでbuildFormメソッドを呼び出すのは、それがヒーローデータを持つためです。addHeroメソッドで再度呼び出します。

実際のアプリケーションは、ngOnInitフックで最もよく実行されるタスクである、データサービスからヒーローを非同期的に取得します。

buildFormメソッドは、FormBuilder fbを使用してフォームコントロールモデルを宣言します。 次に、フォームのvalueChangesイベントに同じonValueChangedハンドラ(1行の違いがあります)を付加し、直ちに呼び出して新しいコントロールモデルのエラーメッセージを設定します。

Built-in validators

Angularには、フォーム内の一般的なユーザー入力を確認するのに役立つ、多数の組み込みバリデータ関数が含まれています。 minlengthmaxlength、およびrequiredに含まれる組み込みバリデーターに加えて、Reactive Formsのemailpatternなどのものもあります。組み込みバリデータの完全なリストについては、Validators APIリファレンスを参照してください。

FormBuilder 宣言(declaration)

FormBuilder declaration オブジェクトは、サンプルのヒーローフォームの3つのコントロールを指定します。

各制御仕様は、配列値を持つ制御名です。最初の配列要素は、対応するヒーローフィールドの現在の値です。オプションの第2の値は、バリデータ関数またはバリデータ関数の配列です。

バリデータ関数のほとんどは、Validatorsクラスの静的メソッドとしてAngularによって提供されるストックバリデータです。 Angularには、標準のHTMLバリデーション属性に対応する在庫バリデータがあります。

"name"コントロールのforbiddenNameバリデーターはカスタムバリデーターです。これについては以下の別のセクションで説明します。

Reactive Formsガイドの「FormBuilder入門」セクションで、FormBuilderの詳細をご覧ください。

hero value の変更をコミットする

双方向データバインディングでは、ユーザーの変更はコントロールから自動的にデータモデルのプロパティに反映されます。Reactiveフォームコンポーネントは、データモデルのプロパティを自動的に更新するためにデータバインディングを使用するべきではありません。開発者はコントロール値からデータモデルへいつどのようにアップデートするかを決める必要があります。

このサンプルは、モデルを2回更新します。

  1. ユーザーがフォームを送信するとき。
  2. ユーザーが新しいヒーローを追加したとき。

onSubmit() メソッドは、ヒーローオブジェクトを単純にフォームの値でマージして置き換えます。

form-validation/src/app/reactive/hero-form-reactive.component.ts

onSubmit() {
  this.submitted = true;
  this.hero = this.heroForm.value;
}

addHero() メソッドは保留中の変更を破棄し、新しいヒーローモデルオブジェクトを作成します。

form-validation/src/app/reactive/hero-form-reactive.component.ts

addHero() {
  this.hero = new Hero(42, '', '');
  this.buildForm();
}

次に、以前のheroFormコントロールモデルを新しいものに置き換えるbuildForm()をもう一度呼び出します。

タグの[formGroup]バインディングは、新しいコントロールモデルでページをリフレッシュします。

2つのTemplate-Driven型コンポーネントファイルと比較して、完全なReactiveコンポーネントファイルがあります。

(ソースは省略)

ライブサンプルを実行して、Reactive型フォームの動作を確認し、このサンプル内のすべてのファイルを比較してみましょう。

カスタム バリデーション

このCookbookサンプルには、Template DrivenフォームとReactiveフォームコントロールの両方に適用されるカスタムのforbiddenNameValidator() 関数があります。これは src/app/shared フォルダにあり、SharedModule で宣言されています。

以下は forbiddenNameValidator() 関数です:

shared/forbidden-name.directive.ts (forbiddenNameValidator)

/** A hero's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
  return (control: AbstractControl): {[key: string]: any} => {
    const name = control.value;
    const no = nameRe.test(name);
    return no ? {'forbiddenName': {name}} : null;
  };
}

この関数は実際には、特定の禁止された名前を検出する正規表現を取り、バリデータ関数を返すファクトリです。

このサンプルでは、禁止された名前は"bob"です。 バリデーターは "bob"を含んだヒーロー名を拒否します。他の場所では、 "alice"または正規表現を構成する名前を拒否することができます。

forbiddenNameValidatorファクトリは、コンフィグレーションされたバリデータ関数を返します。 この関数はAngularコントロールオブジェクトをとり、コントロール値が有効な場合はnullまたはバリデーションエラーオブジェクトを返します。 バリデーションエラーオブジェクトは通常、バリデーションキーで 'forbiddenName'という名前を持ち、値がエラーメッセージ({name})に挿入できる値の任意の辞書であるプロパティを持っています。

カスタム バリデーション ディレクティブ

Reactive Formsコンポーネントでは、'name'コントロールのバリデーター関数リストの下部にforbiddenNameValidatorがあります。

reactive/hero-form-reactive.component.ts (name validators)

'name': [this.hero.name, [
    Validators.required,
    Validators.minLength(4),
    Validators.maxLength(24),
    forbiddenNameValidator(/bob/i)
  ]
],

Template Drivenの例では、<input> にはカスタム属性ディレクティブのセレクタ(forbiddenName)があり、これは "bob"を拒否します。

template/hero-form-template2.component.html (name input)

<input type="text" id="name" class="form-control"
       required minlength="4" maxlength="24" forbiddenName="bob"
       name="name" [(ngModel)]="hero.name" >

対応するForbiddenValidatorDirectiveは、forbiddenNameValidatorのラッパーです。

Angularのformsは、ディレクティブが拡張ディレクティブの拡張可能なコレクションを持つプロバイダであるNG_VALIDATORSプロバイダに登録されるため、ディレクティブのバリデーションプロセスにおける役割を認識します。

shared/forbidden-name.directive.ts (providers)

providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]

どのようにすべてが一緒に来るかのアイデアを得るのに役立つディレクティブの残りの部分は次のとおりです。

shared/forbidden-name.directive.ts (directive)

@Directive({
  selector: '[forbiddenName]',
  providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
})
export class ForbiddenValidatorDirective implements Validator, OnChanges {
  @Input() forbiddenName: string;
  private valFn = Validators.nullValidator;

  ngOnChanges(changes: SimpleChanges): void {
    const change = changes['forbiddenName'];
    if (change) {
      const val: string | RegExp = change.currentValue;
      const re = val instanceof RegExp ? val : new RegExp(val, 'i');
      this.valFn = forbiddenNameValidator(re);
    } else {
      this.valFn = Validators.nullValidator;
    }
  }

  validate(control: AbstractControl): {[key: string]: any} {
    return this.valFn(control);
  }
}

Angularバリデーションに熟知している方は、カスタムバリデーションディレクティブがuseClassではなくuseExistingでインスタンス化されていることに気づいたかもしれません。
登録されているバリデータは、ForbiddenValidatorDirectiveのこのインスタンスでなければなりません。つまり、フォームのインスタンスで、forbiddenNameプロパティが "bob"にバインドされている必要があります。useExistinguseClassに置き換えた場合は、forbiddenNameはありません。

この動作を確認するには、サンプルを実行し、Hero Form 2の名前に「bob」と入力します。妥当性検査エラーが発生します。
useExistingからuseClassに変更して、やり直してください。 今回は、「bob」と入力すると、「bob」というエラーメッセージは表示されません。

 

要素への動作の関連付けの詳細については、「属性ディレクティブ」を参照してください。

テストの考慮事項

Reactiveフォームでは、バリデーションと制御ロジックの分離された単体テストを記述することができます。

分離された単体テストは、テンプレート、DOM、その他の依存関係、またはAngular自体との相互作用とは無関係に、コンポーネントクラスを直接調べます。

このようなテストは、セットアップが最小限で、手早く書くことができ、メンテナンスが容易です。Angular TestBedや非同期テストの習慣は必要ありません。

これはTemplate-Drivenフォームでは不可能です。Template-Drivenフォームのアプローチでは、Angularを使用してコントロールモデルを生成し、HTMLバリデーション属性からバリデーションルールを導き出します。Angular TestBedを使用してコンポーネントテストインスタンスを作成し、非同期テストを作成し、DOMとコミュニケーションをとる必要があります。

テストコードを書くこと自体は困難ではありませんが、より多くの時間、作業、スキルが必要です。なによりテストコードのカバレッジを低下させる要因になるでしょう。