Angular 4フォームの操作:ネストと入力の検証
公開: 2022-03-11Webでは、初期のユーザー入力要素のいくつかは、ボタン、チェックボックス、テキスト入力、およびラジオボタンでした。 HTML標準はその初期の定義から大きく進歩し、今ではあらゆる種類の凝った相互作用を可能にしていますが、今日でもこれらの要素は最新のWebアプリケーションで使用されています。
ユーザー入力の検証は、堅牢なWebアプリケーションの重要な部分です。
Angularアプリケーションのフォームは、そのフォームの下にあるすべての入力の状態を集約し、完全なフォームの検証ステータスのような全体的な状態を提供できます。 これは、各入力を個別にチェックせずに、ユーザー入力を受け入れるか拒否するかを決定するのに非常に便利です。
この記事では、Angularアプリケーションでフォームを操作してフォーム検証を簡単に実行する方法を学習します。
Angular 4では、テンプレート駆動型フォームとリアクティブフォームの2種類のフォームを使用できます。 同じ例を使用して各フォームタイプを調べ、同じものをさまざまな方法で実装する方法を確認します。 後の記事では、ネストされたフォームを設定して操作する方法に関する新しいアプローチについて説明します。
Angular4フォーム
Angular 4では、次の4つのステータスがフォームで一般的に使用されます。
valid –すべてのフォームコントロールの有効性の状態。すべてのコントロールが有効な場合はtrue
無効–
valid
の逆; 一部のコントロールが無効な場合はtrue手付かず–フォームの「クリーンさ」に関するステータスを提供します。 コントロールが変更されていない場合はtrue
汚れた–元の
pristine
; 一部のコントロールが変更された場合はtrue
フォームの基本的な例を見てみましょう。
<form> <div> <label>Name</label> <input type="text" name="name"/> </div> <div> <label>Birth Year</label> <input type="text" name="birthYear"/> </div> <div> <h3>Location</h3> <div> <label>Country</label> <input type="text" name="country"/> </div> <div> <label>City</label> <input type="text" name="city"/> </div> </div> <div> <h3>Phone numbers</h3> <div> <label>Phone number 1</label> <input type="text" name="phoneNumber[1]"/> <button type="button">remove</button> </div> <button type="button">Add phone number</button> </div> <button type="submit">Register</button> <button type="button">Print to console</button> </form>
この例の仕様は次のとおりです。
名前-必須であり、すべての登録ユーザー間で一意です
birthYear-有効な番号である必要があり、ユーザーは18歳以上85歳未満である必要があります
国-必須であり、少し複雑にするために、国がフランスの場合、都市はパリでなければならないことを検証する必要があります(たとえば、私たちのサービスはパリでのみ提供されているとしましょう)
phoneNumber –各電話番号は指定されたパターンに従う必要があり、少なくとも1つの電話番号が必要であり、ユーザーは新しい電話番号を追加したり、既存の電話番号を削除したりできます。
「登録」ボタンは、すべての入力が有効である場合にのみ有効になり、クリックするとフォームが送信されます。
「コンソールに出力」は、クリックされたときにコンソールへのすべての入力の値を出力するだけです。
最終的な目標は、定義された仕様を完全に実装することです。
テンプレート駆動型フォーム
テンプレート駆動型フォームは、AngularJS(またはAngular 1、一部の人が参照している)のフォームと非常によく似ています。 したがって、AngularJSでフォームを操作したことがある人は、フォームを操作するこのアプローチに非常に精通しているでしょう。
Angular 4にモジュールが導入されたことで、特定のタイプのフォームがそれぞれ個別のモジュールに含まれるようになり、適切なモジュールをインポートして、使用するタイプを明示的に定義する必要があります。 テンプレート駆動型フォームのモジュールはFormsModuleです。 そうは言っても、次のようにテンプレート駆動型フォームをアクティブ化できます。
import {FormsModule} from '@angular/forms' import {NgModule} from '@angular/core' import {BrowserModule} from '@angular/platform-browser' import {AppComponent} from 'src/app.component'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent], bootstrap: [ AppComponent ] }) export class AppModule {}
このコードスニペットに示されているように、「ブラウザアプリの起動と実行に不可欠なサービスを提供する」ため、最初にブラウザモジュールをインポートする必要があります。 (Angular 4のドキュメントから)。 次に、必要なFormsModuleをインポートして、テンプレート駆動型フォームをアクティブ化します。 最後に、ルートコンポーネントであるAppComponentの宣言があります。次の手順では、フォームを実装します。
この例と次の例では、 platformBrowserDynamic
メソッドを使用してアプリが適切にブートストラップされていることを確認する必要があることに注意してください。
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {AppModule} from './app.module'; platformBrowserDynamic().bootstrapModule(AppModule);
AppComponent(app.component.ts)は次のようになっていると想定できます。
import {Component} from '@angular/core' @Component({ selector: 'my-app', templateUrl: 'src/app.component.tpl.html' }) export class AppComponent { }
このコンポーネントのテンプレートがapp.component.tpl.htmlにあり、最初のテンプレートをこのファイルにコピーできます。
フォーム内で適切に識別されるように、各入力要素にはname
属性が必要であることに注意してください。 これは単純なHTMLフォームのように見えますが、Angular 4でサポートされているフォームはすでに定義されています(まだ表示されていない可能性があります)。 FormsModuleがインポートされると、Angular 4はform
のHTML要素を自動的に検出し、NgFormコンポーネントをその要素にアタッチします(NgFormコンポーネントのselector
によって)。 これがこの例の場合です。 このAngular4フォームは宣言されていますが、現時点では、Angular4でサポートされている入力を認識していません。 Angular 4は、各input
HTML要素を最も近いform
の祖先に登録するのにそれほど侵襲的ではありません。
入力要素をAngular4要素として認識し、NgFormコンポーネントに登録できるようにするキーは、NgModelディレクティブです。 したがって、app.component.tpl.htmlテンプレートを次のように拡張できます。
<form> .. <input type="text" name="name" ngModel> .. <input type="text" name="birthYear" ngModel > .. <input type="text" name="country" ngModel/> .. <input type="text" name="city" ngModel/> .. <input type="text" name="phoneNumber[1]" ngModel/> </form>
NgModelディレクティブを追加することにより、すべての入力がNgFormコンポーネントに登録されます。 これにより、完全に機能するAngular 4フォームを定義しましたが、これまでのところ非常に優れていますが、NgFormコンポーネントとそれが提供する機能にアクセスする方法がまだありません。 NgFormが提供する2つの主な機能は次のとおりです。
登録されているすべての入力コントロールの値を取得する
すべてのコントロールの全体的な状態を取得する
NgFormを公開するために、<form>要素に以下を追加できます。
<form #myForm="ngForm"> .. </form>
これは、 Component
デコレータのexportAs
プロパティのおかげで可能です。
これが完了すると、すべての入力コントロールの値にアクセスし、テンプレートを次のように拡張できます。
<form #myForm="ngForm"> .. <pre>{{myForm.value | json}}</pre> </form>
myForm.value
を使用して、登録されているすべての入力の値を含むJSONデータにアクセスし、 {{myForm.value | json}}
{{myForm.value | json}}
、値を使用してJSONをきれいに出力しています。
特定のコンテキストからの入力のサブグループをコンテナーにラップし、値JSONでオブジェクトを分離したい場合はどうなりますか?たとえば、国と都市を含む場所、または電話番号はどうなりますか? ストレスを感じないでください。Angular4のテンプレート駆動型フォームでもカバーされています。 これを実現する方法は、 ngModelGroup
ディレクティブを使用することです。
<form #myForm="ngForm"> .. <div ngModelGroup="location"> .. </div> </div ngModelGroup="phoneNumbers"> .. <div> .. </form>
現在不足しているのは、複数の電話番号を追加する方法です。 これを行うための最良の方法は、複数のオブジェクトの反復可能なコンテナーの最良の表現として配列を使用することでしたが、この記事を書いている時点では、その機能はテンプレート駆動型フォームには実装されていません。 したがって、これを機能させるには回避策を適用する必要があります。 電話番号セクションは、次のように更新する必要があります。
<div ngModelGroup="phoneNumbers"> <h3>Phone numbers</h3> <div *ngFor="let phoneId of phoneNumberIds; let i=index;"> <label>Phone number {{i + 1}}</label> <input type="text" name="phoneNumber[{{phoneId}}]" #phoneNumber="ngModel" ngModel/> <button type="button" (click)="remove(i); myForm.control.markAsTouched()">remove</button> </div> <button type="button" (click)="add(); myForm.control.markAsTouched()">Add phone number</button> </div>
myForm.control.markAsTouched()
を使用してフォームをtouched
し、その時点でのエラーを表示できるようにします。 ボタンをクリックしてもこのプロパティはアクティブにならず、入力のみがアクティブになります。 次の例をより明確にするために、 add()
およびremove()
のクリックハンドラーにこの行を追加しません。 そこにあると想像してみてください。 (プランカーに存在します。)
また、次のコードを含むようにAppComponent
を更新する必要があります。
private count:number = 1; phoneNumberIds:number[] = [1]; remove(i:number) { this.phoneNumberIds.splice(i, 1); } add() { this.phoneNumberIds.push(++this.count); }
追加された新しい電話番号ごとに一意のIDを保存する必要があり、 *ngFor
で、電話番号コントロールをIDで追跡します(あまり良くないことは認めますが、Angular 4チームがこの機能を実装するまでは、恐れ入ります、それは私たちができる最善のことです)
さて、これまでのところ、Angular 4でサポートされている入力付きフォームを追加し、入力の特定のグループ(場所と電話番号)を追加して、テンプレート内にフォームを公開しました。 しかし、コンポーネントのメソッドでNgFormオブジェクトにアクセスしたい場合はどうでしょうか。 これを行う2つの方法を見ていきます。
最初の方法として、現在の例でmyForm
とラベル付けされたNgFormを、フォームのonSubmitイベントのハンドラーとして機能する関数への引数として渡すことができます。 統合を改善するために、onSubmitイベントはngSubmit
という名前のAngular 4、NgForm固有のイベントでwrapped
されます。これは、送信時に何らかのアクションを実行する場合の正しい方法です。 したがって、例は次のようになります。
<form #myForm="ngForm" (ngSubmit)="register(myForm)"> … </form>
AppComponentに実装された、対応するメソッドregister
が必要です。 何かのようなもの:
register (myForm: NgForm) { console.log('Successful registration'); console.log(myForm); }
このように、onSubmitイベントを利用することで、送信が実行されたときにのみNgFormコンポーネントにアクセスできます。
2番目の方法は、コンポーネントのプロパティに@ViewChildデコレータを追加してビュークエリを使用することです。
@ViewChild('myForm') private myForm: NgForm;
このアプローチでは、onSubmitイベントが発生したかどうかに関係なく、フォームへのアクセスが許可されます。
すごい! これで、コンポーネント内のフォームにアクセスできる、完全に機能するAngular4フォームができました。 しかし、何か足りないものに気づきましたか? ユーザーが「years」入力に「this-is-not-ayear」のようなものを入力した場合はどうなりますか? ええ、あなたはそれを理解しました、私たちは入力の検証を欠いています、そしてそれを次のセクションでカバーします。
検証
検証は、各アプリケーションにとって非常に重要です。 無効なデータの送信/保存を防ぐために、常にユーザー入力を検証する必要があります(ユーザーを信頼することはできません)。また、ユーザーが有効なデータを入力するように適切にガイドするために、エラーに関する意味のあるメッセージを表示する必要があります。
一部の入力に適用される検証ルールについては、適切なバリデーターをその入力に関連付ける必要があります。 Angular 4は、 required
、 maxLength
、 minLength
などの一般的なバリデーターのセットをすでに提供しています…
では、バリデーターを入力に関連付けるにはどうすればよいでしょうか。 まあ、かなり簡単です。 コントロールにバリデーターディレクティブを追加するだけです。
<input name="name" ngModel required/>
この例では、「名前」の入力が必須になります。 この例のすべての入力にいくつかの検証を追加しましょう。
<form #myForm="ngForm" (ngSubmit)="actionOnSubmit(myForm)" novalidate> <p>Is "myForm" valid? {{myForm.valid}}</p> .. <input type="text" name="name" ngModel required/> .. <input type="text" name="birthYear" ngModel required pattern="\\d{4,4}"/> .. <div ngModelGroup="location"> .. <input type="text" name="country" ngModel required/> .. <input type="text" name="city" ngModel/> </div> <div ngModelGroup="phoneNumbers"> .. <input type="text" name="phoneNumber[{{phoneId}}]" ngModel required/> .. </div> .. </form>
注:
novalidate
は、ブラウザーのネイティブフォーム検証を無効にするために使用されます。
「名前」を必須とし、「年」フィールドは必須であり、数字のみで構成されている必要があります。国の入力と電話番号が必須です。 また、フォームの有効性のステータスを{{myForm.valid}}
で出力します。
この例の改善点は、(全体的な状態を表示するだけでなく)ユーザー入力の何が問題になっているかも表示することです。 追加の検証を追加する前に、提供されたコントロールのすべてのエラーを出力できるようにするヘルパーコンポーネントを実装したいと思います。
// show-errors.component.ts import { Component, Input } from '@angular/core'; import { AbstractControlDirective, AbstractControl } from '@angular/forms'; @Component({ selector: 'show-errors', template: ` <ul *ngIf="shouldShowErrors()"> <li *ngFor="let error of listOfErrors()">{{error}}</li> </ul> `, }) export class ShowErrorsComponent { private static readonly errorMessages = { 'required': () => 'This field is required', 'minlength': (params) => 'The min number of characters is ' + params.requiredLength, 'maxlength': (params) => 'The max allowed number of characters is ' + params.requiredLength, 'pattern': (params) => 'The required pattern is: ' + params.requiredPattern, 'years': (params) => params.message, 'countryCity': (params) => params.message, 'uniqueName': (params) => params.message, 'telephoneNumbers': (params) => params.message, 'telephoneNumber': (params) => params.message }; @Input() private control: AbstractControlDirective | AbstractControl; shouldShowErrors(): boolean { return this.control && this.control.errors && (this.control.dirty || this.control.touched); } listOfErrors(): string[] { return Object.keys(this.control.errors) .map(field => this.getMessage(field, this.control.errors[field])); } private getMessage(type: string, params: any) { return ShowErrorsComponent.errorMessages[type](params); } }
エラーのあるリストは、既存のエラーがいくつかあり、入力がタッチまたはダーティである場合にのみ表示されます。
各エラーのメッセージは、事前定義されたメッセージerrorMessages
のマップで検索されます(すべてのメッセージを前もって追加しました)。
このコンポーネントは次のように使用できます。
<div> <label>Birth Year</label> <input type="text" name="birthYear" #birthYear="ngModel" ngModel required pattern="\\d{4,4}"/> <show-errors [control]="birthYear"></show-errors> </div>
入力ごとにNgModelを公開し、それをすべてのエラーをレンダリングするコンポーネントに渡す必要があります。 この例では、データが数値であるかどうかを確認するためにパターンを使用していることがわかります。 ユーザーが「0000」と入力するとどうなりますか? これは無効な入力になります。 また、一意の名前のバリデーター、国の奇妙な制限(country ='France'の場合、都市は'Paris'である必要があります)、正しい電話番号のパターン、および少なくとも1つの電話番号の検証が欠落しています。存在します。 これは、カスタムバリデーターを確認するのに適切なタイミングです。
Angular 4は、各カスタムバリデーターが実装する必要のあるインターフェースであるValidatorインターフェースを提供します(なんと驚きです!)。 Validatorインターフェースは基本的に次のようになります。
export interface Validator { validate(c: AbstractControl): ValidationErrors | null; registerOnValidatorChange?(fn: () => void): void; }
各具体的な実装は、「validate」メソッドを実装する必要があります。 このvalidate
メソッドは、何を入力として受け取ることができ、何を出力として返す必要があるかについて非常に興味深いものです。 入力はAbstractControlです。これは、引数がAbstractControlを拡張する任意のタイプ(FormGroup、FormControl、およびFormArray)であることを意味します。 ユーザー入力が有効な場合、 validate
メソッドの出力はnull
またはundefined
(出力なし)である必要があります。ユーザー入力が無効な場合は、 ValidationErrors
オブジェクトを返します。 この知識を基に、カスタムのbirthYear
バリデーターを実装します。
import { Directive } from '@angular/core'; import { NG_VALIDATORS, FormControl, Validator, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[birthYear]', providers: [{provide: NG_VALIDATORS, useExisting: BirthYearValidatorDirective, multi: true}] }) export class BirthYearValidatorDirective implements Validator { validate(c: FormControl): ValidationErrors { const numValue = Number(c.value); const currentYear = new Date().getFullYear(); const minYear = currentYear - 85; const maxYear = currentYear - 18; const isValid = !isNaN(numValue) && numValue >= minYear && numValue <= maxYear; const message = { 'years': { 'message': 'The year must be a valid number between ' + minYear + ' and ' + maxYear } }; return isValid ? null : message; } }
ここで説明することがいくつかあります。 まず、Validatorインターフェースを実装したことに気付くかもしれません。 validate
メソッドは、入力された生年までにユーザーが18〜85歳であるかどうかを確認します。 入力が有効な場合はnull
が返され、そうでない場合は検証メッセージを含むオブジェクトが返されます。 そして最後のそして最も重要な部分は、このディレクティブをバリデーターとして宣言することです。 これは、@Directiveデコレータの「providers」パラメータで行われます。 このバリデーターは、マルチプロバイダーNG_VALIDATORSの1つの値として提供されます。 また、NgModuleでこのディレクティブを宣言することを忘れないでください。 これで、このバリデーターを次のように使用できます。
<input type="text" name="birthYear" #year="ngModel" ngModel required birthYear/>
ええ、それと同じくらい簡単です!
電話番号については、次のように電話番号の形式を検証できます。
import { Directive } from '@angular/core'; import { NG_VALIDATORS, Validator, FormControl, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[telephoneNumber]', providers: [{provide: NG_VALIDATORS, useExisting: TelephoneNumberFormatValidatorDirective, multi: true}] }) export class TelephoneNumberFormatValidatorDirective implements Validator { validate(c: FormControl): ValidationErrors { const isValidPhoneNumber = /^\d{3,3}-\d{3,3}-\d{3,3}$/.test(c.value); const message = { 'telephoneNumber': { 'message': 'The phone number must be valid (XXX-XXX-XXX, where X is a digit)' } }; return isValidPhoneNumber ? null : message; } }
次に、国と電話番号の数について、2つの検証を行います。 それらの両方に共通する何かに気づきましたか? どちらも、適切な検証を実行するために複数のコントロールが必要です。 さて、あなたはバリデーターインターフェースを覚えていますか、そしてそれについて私たちが言ったことは何ですか? validate
メソッドの引数はAbstractControlであり、ユーザー入力またはフォーム自体にすることができます。 これにより、複数のコントロールを使用して具体的な検証ステータスを決定するバリデーターを実装する機会が生まれます。
import { Directive } from '@angular/core'; import { NG_VALIDATORS, Validator, FormGroup, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[countryCity]', providers: [{provide: NG_VALIDATORS, useExisting: CountryCityValidatorDirective, multi: true}] }) export class CountryCityValidatorDirective implements Validator { validate(form: FormGroup): ValidationErrors { const countryControl = form.get('location.country'); const cityControl = form.get('location.city'); if (countryControl != null && cityControl != null) { const country = countryControl.value; const city = cityControl.value; let error = null; if (country === 'France' && city !== 'Paris') { error = 'If the country is France, the city must be Paris'; } const message = { 'countryCity': { 'message': error } }; return error ? message : null; } } }
新しいバリデーターであるcountry-cityバリデーターを実装しました。 引数としてvalidateメソッドがFormGroupを受け取り、そのFormGroupから検証に必要な入力を取得できることに気付くでしょう。 残りの部分は、単一の入力バリデーターと非常によく似ています。
電話番号の数のバリデーターは次のようになります。
import { Directive } from '@angular/core'; import { NG_VALIDATORS, Validator, FormGroup, ValidationErrors, FormControl } from '@angular/forms'; @Directive({ selector: '[telephoneNumbers]', providers: [{provide: NG_VALIDATORS, useExisting: TelephoneNumbersValidatorDirective, multi: true}] }) export class TelephoneNumbersValidatorDirective implements Validator { validate(form: FormGroup): ValidationErrors { const message = { 'telephoneNumbers': { 'message': 'At least one telephone number must be entered' } }; const phoneNumbers = <FormGroup> form.get('phoneNumbers'); const hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers.controls).length > 0; return hasPhoneNumbers ? null : message; } }
次のように使用できます。
<form #myForm="ngForm" countryCity telephoneNumbers> .. </form>
入力バリデーターと同じですよね? ちょうど今フォームに適用されます。
ShowErrorsコンポーネントを覚えていますか? AbstractControlDirectiveで動作するように実装しました。つまり、このフォームに直接関連するすべてのエラーを表示するために再利用することもできます。 この時点で、フォームに直接関連付けられている検証ルールは、 Country-city
とTelephone numbers
のみであることに注意してください(他のバリデーターは特定のフォームコントロールに関連付けられています)。 すべてのフォームエラーを印刷するには、次の手順を実行します。
<form #myForm="ngForm" countryCity telephoneNumbers > <show-errors [control]="myForm"></show-errors> .. </form>
最後に残っているのは、一意の名前の検証です。 これは少し異なります。 名前が一意であるかどうかを確認するには、既存のすべての名前を確認するためにバックエンドを呼び出す必要があります。 これは非同期操作として分類されます。 この目的のために、カスタムバリデーターに以前の手法を再利用できます。バリデーターが将来解決されるオブジェクト(promiseまたはvalidate
)を返すようにするだけです。 この場合、promiseを使用します。
import { Directive } from '@angular/core'; import { NG_ASYNC_VALIDATORS, Validator, FormControl, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[uniqueName]', providers: [{provide: NG_ASYNC_VALIDATORS, useExisting: UniqueNameValidatorDirective, multi: true}] }) export class UniqueNameValidatorDirective implements Validator { validate(c: FormControl): ValidationErrors { const message = { 'uniqueName': { 'message': 'The name is not unique' } }; return new Promise(resolve => { setTimeout(() => { resolve(c.value === 'Existing' ? message : null); }, 1000); }); } }
1秒間待ってから、結果を返します。 同期バリデーターと同様に、promiseがnull
で解決された場合、それは検証に合格したことを意味します。 約束が他の何かで解決された場合、検証は失敗しました。 また、このバリデーターが別のマルチプロバイダーであるNG_ASYNC_VALIDATORS
に登録されていることにも注意してください。 非同期バリデーターに関するフォームの便利なプロパティの1つは、 pending
のプロパティです。 次のように使用できます。
<button [disabled]="myForm.pending">Register</button>
非同期バリデーターが解決されるまで、ボタンは無効になります。
これは、完全なAppComponent、ShowErrorsコンポーネント、およびすべてのバリデーターを含むプランカーです。
これらの例では、テンプレート駆動型フォームを操作するためのほとんどのケースについて説明しました。 テンプレート駆動型フォームがAngularJSのフォームに非常に似ていることを示しました(AngularJS開発者が移行するのは非常に簡単です)。 このタイプのフォームを使用すると、Angular 4フォームを最小限のプログラミング、主にHTMLテンプレートでの操作と統合するのは非常に簡単です。
リアクティブフォーム
リアクティブフォームは「モデル駆動型」フォームとも呼ばれていましたが、私はそれらを「プログラマティック」フォームと呼ぶのが好きで、すぐにその理由がわかります。 リアクティブフォームはAngular4フォームのサポートに向けた新しいアプローチであるため、テンプレート駆動型とは異なり、AngularJS開発者はこのタイプに精通していません。
ここから始めましょう。テンプレート駆動型フォームに特別なモジュールがあったことを覚えていますか? また、リアクティブフォームには、ReactiveFormsModuleと呼ばれる独自のモジュールがあり、このタイプのフォームをアクティブ化するにはインポートする必要があります。
import {ReactiveFormsModule} from '@angular/forms' import {NgModule} from '@angular/core' import {BrowserModule} from '@angular/platform-browser' import {AppComponent} from 'src/app.component'; @NgModule({ imports: [ BrowserModule, ReactiveFormsModule ], declarations: [ AppComponent], bootstrap: [ AppComponent ] }) export class AppModule {}
また、アプリケーションをブートストラップすることを忘れないでください。
前のセクションと同じAppComponentとテンプレートから始めることができます。
この時点で、FormsModuleがインポートされていない場合(インポートされていないことを確認してください)、いくつかのフォームコントロールを備えた通常のHTMLフォーム要素があり、Angularマジックはありません。
私がこのアプローチを「プログラマティック」と呼んでいる理由に気付くでしょう。 Angular 4フォームを有効にするには、FormGroupオブジェクトを手動で宣言し、次のようなコントロールを設定する必要があります。
import { FormGroup, FormControl, FormArray, NgForm } from '@angular/forms'; import { Component, OnInit } from '@angular/core'; @Component({ selector: 'my-app', templateUrl: 'src/app.component.html' }) export class AppComponent implements OnInit { private myForm: FormGroup; constructor() { } ngOnInit() { this.myForm = new FormGroup({ 'name': new FormControl(), 'birthYear': new FormControl(), 'location': new FormGroup({ 'country': new FormControl(), 'city': new FormControl() }), 'phoneNumbers': new FormArray([new FormControl('')]) }); } printMyForm() { console.log(this.myForm); } register(myForm: NgForm) { console.log('Registration successful.'); console.log(myForm.value); } }
printForm
メソッドとregister
メソッドは前の例と同じであり、次の手順で使用されます。 ここで使用されるキータイプは、FormGroup、FormControl、およびFormArrayです。 これらの3つのタイプは、有効なFormGroupを作成するために必要なすべてです。 FormGroupは簡単です。 これは、コントロールの単純なコンテナーです。 FormControlも簡単です。 それは任意のコントロール(例えば、入力)です。 そして最後に、FormArrayは、テンプレート駆動型のアプローチでは欠けていたパズルのピースです。 FormArrayを使用すると、各コントロールの具体的なキー、基本的にはコントロールの配列を指定せずに、コントロールのグループを維持できます(電話番号に最適なもののようですよね?)。
これら3つのタイプのいずれかを作成するときは、この3のルールを覚えておいてください。 各タイプのコンストラクターは、コードで定義されている、 value
、バリデーターまたはバリデーターのリスト、および非同期バリデーターまたは非同期バリデーターのリストの3つの引数を受け取ります。
constructor(value: any, validator?: ValidatorFn | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]);
FormGroupの場合、 value
はオブジェクトであり、各キーはコントロールの名前を表し、値はコントロール自体です。

FormArrayの場合、 value
はコントロールの配列です。
FormControlの場合、 value
はコントロールの初期値または初期状態( value
とdisabled
なプロパティを含むオブジェクト)です。
FormGroupオブジェクトを作成しましたが、テンプレートはまだこのオブジェクトを認識していません。 コンポーネント内のFormGroupとテンプレートの間のリンクは、次のように使用されるformGroup
、 formControlName
、 formGroupName
、およびformArrayName
の4つのディレクティブを使用して行われます。
<form [formGroup]="myForm" (ngSubmit)="register(myForm)"> <div> <label>Name</label> <input type="text" name="name" formControlName="name"> </div> <div> <label>Birth Year</label> <input type="text" name="birthYear" formControlName="birthYear"> </div> <div formGroupName="location"> <h3>Location</h3> <div> <label>Country</label> <input type="text" name="country" formControlName="country"> </div> <div> <label>City</label> <input type="text" name="city" formControlName="city"> </div> </div> <div formArrayName="phoneNumbers"> <h3>Phone numbers</h3> <div *ngFor="let phoneNumberControl of myForm.controls.phoneNumbers.controls; let i=index;"> <label>Phone number {{i + 1}}</label> <input type="text" name="phoneNumber[{{phoneId}}]" [formControlName]="i"> <button type="button" (click)="remove(i)">remove</button> </div> <button type="button" (click)="add()">Add phone number</button> </div> <pre>{{myForm.value | json}}</pre> <button type="submit">Register</button> <button type="button" (click)="printMyForm()">Print to console</button> </form>
FormArrayができたので、その構造を使用してすべての電話番号をレンダリングできることがわかります。
次に、(コンポーネント内の)電話番号の追加と削除のサポートを追加します。
remove(i: number) { (<FormArray>this.myForm.get('phoneNumbers')).removeAt(i); } add() { (<FormArray>this.myForm.get('phoneNumbers')).push(new FormControl('')); }
これで、完全に機能するAngular4リアクティブフォームができました。 FormGroupが(テンプレート構造をスキャンすることによって)「テンプレートで作成」されてコンポーネントに渡されたテンプレート駆動型フォームとの違いに注意してください。リアクティブフォームでは、その逆で、完全なFormGroupがで作成されます。コンポーネント、次に「テンプレートに渡され」、対応するコントロールにリンクされます。 ただし、検証にも同じ問題があります。この問題は次のセクションで解決されます。
検証
検証に関しては、リアクティブフォームはテンプレート駆動型フォームよりもはるかに柔軟性があります。 追加の変更なしで、以前に実装されたものと同じバリデーターを再利用できます(テンプレート駆動型の場合)。 したがって、バリデーターディレクティブを追加することで、同じ検証をアクティブ化できます。
<form [formGroup]="myForm" (ngSubmit)="register(myForm)" countryCity telephoneNumbers novalidate> <input type="text" name="name" formControlName="name" required uniqueName> <show-errors [control]="myForm.controls.name"></show-errors> .. <input type="text" name="birthYear" formControlName="birthYear" required birthYear> <show-errors [control]="myForm.controls.birthYear"></show-errors> .. <div formGroupName="location"> .. <input type="text" name="country" formControlName="country" required> <show-errors [control]="myForm.controls.location.controls.country"></show-errors> .. <input type="text" name="city" formControlName="city"> .. </div> <div formArrayName="phoneNumbers"> <h3>Phone numbers</h3> .. <input type="text" name="phoneNumber[{{phoneId}}]" [formControlName]="i" required telephoneNumber> <show-errors [control]="phoneNumberControl"></show-errors> .. </div> .. </form>
ShowErrorsコンポーネントに渡すNgModelディレクティブがないことに注意してください。ただし、完全なFormGroupはすでに構築されており、エラーを取得するための正しいAbstractControlを渡すことができます。
これは、リアクティブフォームのこのタイプの検証を備えた完全に機能するプランカーです。
しかし、バリデーターを再利用するだけでは面白くありませんよね? フォームグループを作成するときにバリデーターを指定する方法を見ていきます。
FormGroup、FormControl、およびFormArrayのコンストラクターについて説明した「3sルール」ルールを覚えていますか? はい、コンストラクターはバリデーター関数を受け取ることができると言いました。 それでは、そのアプローチを試してみましょう。
まず、すべてのバリデーターのvalidate
関数をクラスに抽出して、静的メソッドとして公開する必要があります。
import { FormArray, FormControl, FormGroup, ValidationErrors } from '@angular/forms'; export class CustomValidators { static birthYear(c: FormControl): ValidationErrors { const numValue = Number(c.value); const currentYear = new Date().getFullYear(); const minYear = currentYear - 85; const maxYear = currentYear - 18; const isValid = !isNaN(numValue) && numValue >= minYear && numValue <= maxYear; const message = { 'years': { 'message': 'The year must be a valid number between ' + minYear + ' and ' + maxYear } }; return isValid ? null : message; } static countryCity(form: FormGroup): ValidationErrors { const countryControl = form.get('location.country'); const cityControl = form.get('location.city'); if (countryControl != null && cityControl != null) { const country = countryControl.value; const city = cityControl.value; let error = null; if (country === 'France' && city !== 'Paris') { error = 'If the country is France, the city must be Paris'; } const message = { 'countryCity': { 'message': error } }; return error ? message : null; } } static uniqueName(c: FormControl): Promise<ValidationErrors> { const message = { 'uniqueName': { 'message': 'The name is not unique' } }; return new Promise(resolve => { setTimeout(() => { resolve(c.value === 'Existing' ? message : null); }, 1000); }); } static telephoneNumber(c: FormControl): ValidationErrors { const isValidPhoneNumber = /^\d{3,3}-\d{3,3}-\d{3,3}$/.test(c.value); const message = { 'telephoneNumber': { 'message': 'The phone number must be valid (XXX-XXX-XXX, where X is a digit)' } }; return isValidPhoneNumber ? null : message; } static telephoneNumbers(form: FormGroup): ValidationErrors { const message = { 'telephoneNumbers': { 'message': 'At least one telephone number must be entered' } }; const phoneNumbers = <FormArray>form.get('phoneNumbers'); const hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers.controls).length > 0; return hasPhoneNumbers ? null : message; } }
Now we can change the creation of 'myForm' to:
this.myForm = new FormGroup({ 'name': new FormControl('', Validators.required, CustomValidators.uniqueName), 'birthYear': new FormControl('', [Validators.required, CustomValidators.birthYear]), 'location': new FormGroup({ 'country': new FormControl('', Validators.required), 'city': new FormControl() }), 'phoneNumbers': new FormArray([this.buildPhoneNumberComponent()]) }, Validators.compose([CustomValidators.countryCity, CustomValidators.telephoneNumbers]) );
見る? The rule of “3s,” when defining a FormControl, multiple validators can be declared in an array, and if we want to add multiple validators to a FormGroup they must be “merged” using Validators.compose (also Validators.composeAsync is available). And, that's it, validation should be working completely. There's a Plunker for this example as well.
This goes out to everybody that hates the “new” word. For working with the reactive forms, there's a shortcut provided—a builder, to be more precise. The FormBuilder allows creating the complete FormGroup by using the “builder pattern.” And that can be done by changing the FormGroup construction like this:
constructor(private fb: FormBuilder) { } ngOnInit() { this.myForm = this.fb.group({ 'name': ['', Validators.required, CustomValidators.uniqueName], 'birthYear': ['', [Validators.required, CustomValidators.birthYear]], 'location': this.fb.group({ 'country': ['', Validators.required], 'city': '' }), 'phoneNumbers': this.fb.array([this.buildPhoneNumberComponent()]) }, { validator: Validators.compose([CustomValidators.countryCity, CustomValidators.telephoneNumbers]) } ); }
Not a very big improvement from the instantiation with “new,” but there it is. And, don't worry, there's a Plunker for this also.
In this second section, we had a look at reactive forms in Angular 4. As you may notice, it is a completely new approach towards adding support for forms. Even though it seems verbose, this approach gives the developer total control over the underlying structure that enables forms in Angular 4. Also, since the reactive forms are created manually in the component, they are exposed and provide an easy way to be tested and controlled, while this was not the case with the template-driven forms.
Nesting Forms
Nesting forms is in some cases useful and a required feature, mainly when the state (eg, validity) of a sub-group of controls needs to determined. Think about a tree of components; we might be interested in the validity of a certain component in the middle of that hierarchy. That would be really hard to achieve if we had a single form at the root component. But, oh boy, it is a sensitive manner on a couple of levels. First, nesting real HTML forms, according to the HTML specification, is not allowed. We might try to nest <form> elements. In some browsers it might actually work, but we cannot be sure that it will work on all browsers, since it is not in the HTML spec. In AngularJS, the way to work around this limitation was to use the ngForm
directive, which offered the AngularJS form functionalities (just grouping of the controls, not all form
capabilities like posting to the server) but could be placed on any element. Also, in AngularJS, nesting of forms (when I say forms, I mean NgForm) was available out of the box. Just by declaring a tree of couple of elements with the ngForm
directive, the state of each form was propagated upwards to the root element.
In the next section, we will have a look at a couple options on how to nest forms. I like to point out that we can differentiate two types of nesting: within the same component and across different components.
Nesting within the Same Component
If you take a look at the example that we implemented with the template-driven and the reactive approach, you will notice that we have two inner containers of controls, the “location” and the “phone numbers.” To create that container, to store the values in a separate property object, we used the NgModelGroup, FormGroupName, and the FormArrayName directives. If you have a good look at the definition of each directive, you may notice that each one of them extends the ControlContainer class (directly or indirectly). Well, what do you know, it turns out this is enough to provide the functionality that we require, wrapping up the state of all inner controls and propagating that state to the parent.
For the template-driven form, we need to do the following changes:
<form #myForm="ngForm" (ngSubmit)="register(myForm)" novalidate> .. <div ngModelGroup="location" #location="ngModelGroup" countryCity> .. <show-errors [control]="location"></show-errors> </div> <div ngModelGroup="phoneNumbers" #phoneNumbers="ngModelGroup" telephoneNumbers> .. <show-errors [control]="phoneNumbers"></show-errors> </div> </form>
We added the ShowErrors component to each group, to show the errors directly associated with that group only. Since we moved the countryCity
and telephoneNumbers
validators to a different level, we also need to update them appropriately:
// country-city-validator.directive.ts let countryControl = form.get('country'); let cityControl = form.get('city');
And telephone-numbers-validator.directive.ts to:
let phoneNumbers = form.controls; let hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers).length > 0;
You can try the full example with template-driven forms in this Plunker.
And for the reactive forms, we will need some similar changes:
<form [formGroup]="myForm" (ngSubmit)="register(myForm)" novalidate> .. <div formGroupName="location"> .. <show-errors [control]="myForm.controls.location"></show-errors> </div> <div formArrayName="phoneNumbers"> .. <show-errors [control]="myForm.controls.phoneNumbers"></show-errors> </div> .. </form>
The same changes from country-city-validator.directive.ts
and telephone-numbers-validator.directive.ts
are required for the countryCity
and telephoneNumbers
validators in CustomValidators to properly locate the controls.
And lastly, we need to modify the construction of the FormGroup to:
this.myForm = new FormGroup({ 'name': new FormControl('', Validators.required, CustomValidators.uniqueName), 'birthYear': new FormControl('', [Validators.required, CustomValidators.birthYear]), 'location': new FormGroup({ 'country': new FormControl('', Validators.required), 'city': new FormControl() }, CustomValidators.countryCity), 'phoneNumbers': new FormArray([this.buildPhoneNumberComponent()], CustomValidators.telephoneNumbers) });
And there you have it—we've improved the validation for the reactive forms as well and as expected, the Plunker for this example.
Nesting across Different Components
It may come as a shock to all AngularJS developers, but in Angular 4, nesting of forms across different component doesn't work out of the box. I'm going to be straight honest with you; my opinion is that nesting is not supported for a reason (probably not because the Angular 4 team just forgot about it). Angular4's main enforced principle is a one-way data flow, top to bottom through the tree of components. The whole framework was designed like that, where the vital operation, the change detection, is executed in the same manner, top to bottom. If we follow this principle completely, we should have no issues, and all changes should be resolved within one full detection cycle. 少なくとも、それがアイデアです。 In order to check that one-way data flow is implemented correctly, the nice guys in the Angular 4 team implemented a feature that after each change detection cycle, while in development mode, an additional round of change detection is triggered to check that no binding was changed as a result of reverse data propagation. What this means, let's think about a tree of components (C1, C2, C3, C4) as in Fig. 1, the change detection starts at the C1 component, continues at the C2 component and ends in the C3 component.
If we have some method in C3 with a side effect that changes some binding in C1, that means that we are pushing data upwards, but the change detection for C1 already passed. When working in dev mode, the second round kicks in and notices a change in C1 that came as a result of a method execution in some child component. Then you are in trouble and you'll probably see the “Expression has changed after it was checked” exception. You could just turn off the development mode and there will be no exception, but the problem will not be solved; plus, how would you sleep at night, just sweeping all your problems under the rug like that?
それがわかったら、フォームの状態を集約した場合に何をしているのかを考えてください。 そうです、データはコンポーネントツリーの上方にプッシュされます。 単一レベルのフォームを操作する場合でも、フォームコントロール( ngModel
)とフォーム自体の統合はそれほど優れていません。 これらは、コントロールの値を登録または更新するときに、追加の変更検出サイクルをトリガーします(これは、解決されたPromiseを使用して実行されますが、秘密にしておきます)。 なぜ追加のラウンドが必要なのですか? 同じ理由で、データはコントロールからフォームへと上向きに流れています。 ただし、場合によっては、複数のコンポーネントにまたがるフォームのネストが必須の機能であり、この要件をサポートするためのソリューションを検討する必要があります。
これまでのところ、最初に頭に浮かぶアイデアは、リアクティブフォームを使用し、ルートコンポーネントで完全なフォームツリーを作成してから、子フォームを入力として子コンポーネントに渡すことです。 このようにして、親と子コンポーネントを緊密に結合し、すべての子フォームの作成を処理することでルートコンポーネントのビジネスロジックを乱雑にしました。 さあ、私たちはプロです。フォームを使用して完全に分離されたコンポーネントを作成する方法を見つけ出し、フォームが親である人に状態を伝播する方法を提供できると確信しています。
そうは言っても、Angular 4フォームをネストできるようにするディレクティブは次のとおりです(プロジェクトに必要だったために実装されました)。
import { OnInit, OnDestroy, Directive, SkipSelf, Optional, Attribute, Injector, Input } from '@angular/core'; import { NgForm, FormArray, FormGroup, AbstractControl } from '@angular/forms'; const resolvedPromise = Promise.resolve(null); @Directive({ selector: '[nestableForm]' }) export class NestableFormDirective implements OnInit, OnDestroy { private static readonly FORM_ARRAY_NAME = 'CHILD_FORMS'; private currentForm: FormGroup; @Input() private formGroup: FormGroup; constructor(@SkipSelf() @Optional() private parentForm: NestableFormDirective, private injector: Injector, @Attribute('rootNestableForm') private isRoot) { } ngOnInit() { if (!this.currentForm) { // NOTE: at this point both NgForm and ReactiveFrom should be available this.executePostponed(() => this.resolveAndRegister()); } } ngOnDestroy() { this.executePostponed(() => this.parentForm.removeControl(this.currentForm)); } public registerNestedForm(control: AbstractControl): void { // NOTE: prevent circular reference (adding to itself) if (control === this.currentForm) { throw new Error('Trying to add itself! Nestable form can be added only on parent "NgForm" or "FormGroup".'); } (<FormArray>this.currentForm.get(NestableFormDirective.FORM_ARRAY_NAME)).push(control); } public removeControl(control: AbstractControl): void { const array = (<FormArray>this.currentForm.get(NestableFormDirective.FORM_ARRAY_NAME)); const idx = array.controls.indexOf(control); array.removeAt(idx); } private resolveAndRegister(): void { this.currentForm = this.resolveCurrentForm(); this.currentForm.addControl(NestableFormDirective.FORM_ARRAY_NAME, new FormArray([])); this.registerToParent(); } private resolveCurrentForm(): FormGroup { // NOTE: template-driven or model-driven => determined by the formGroup input return this.formGroup ? this.formGroup : this.injector.get(NgForm).control; } private registerToParent(): void { if (this.parentForm != null && !this.isRoot) { this.parentForm.registerNestedForm(this.currentForm); } } private executePostponed(callback: () => void): void { resolvedPromise.then(() => callback()); } }
次のGIFの例は、 form-1
を含む1つのmain
コンポーネントを示しており、そのフォーム内に、別のネストされたコンポーネント、 component-2
があります。 component-2
には、 form-2
2が含まれています。form-2は、 form-2.1
、 form-2.2
、およびリアクティブフォームのツリーを含むコンポーネント( component-3
)と、フォームを含むコンポーネント( component-4
)をネストしています。他のすべてのフォームから分離されています。 かなり厄介ですが、このディレクティブの機能を示すために、かなり複雑なシナリオを作成したかったのです。
この例は、このプランカーに実装されています。
それが提供する機能は次のとおりです。
nestableFormディレクティブを要素(form、ngForm、[ngForm]、[formGroup])に追加することにより、ネストを有効にします。
テンプレート駆動型およびリアクティブフォームで動作します
複数のコンポーネントにまたがるフォームのツリーを構築できます
rootNestableForm =” true”でフォームのサブツリーを分離します(親のnestableFormには登録されません)
このディレクティブを使用すると、親フォームが同じコンポーネントで宣言されているかどうかに関係なく、子コンポーネントのフォームを最初の親nestableFormに登録できます。 実装の詳細について説明します。
まず、コンストラクターを見てみましょう。 最初の引数は次のとおりです。
@SkipSelf() @Optional() private parentForm: NestableFormDirective
これにより、最初のNestableFormDirective親が検索されます。 @ SkipSelf、それ自体と一致しない場合、および@Optionalは、ルートフォームの場合、親が見つからない可能性があるため。 これで、親のネスト可能なフォームへの参照ができました。
2番目の引数は次のとおりです。
private injector: Injector
インジェクターは、現在のFormGroup provider
(テンプレートまたはリアクティブ)を取得するために使用されます。
そして最後の議論は:
@Attribute('rootNestableForm') private isRoot
このフォームがフォームのツリーから分離されているかどうかを判断する値を取得します。
次に、延期されたアクションとしてのngInit
(逆データフローを覚えていますか?)で、現在のFormGroupが解決され、 CHILD_FORMS
という名前の新しいFormArrayコントロールがこのFormGroup(子フォームが登録される場所)に登録され、最後のアクションとして、現在のFormGroupは、親のネスト可能なフォームの子として登録されます。
ngOnDestroy
アクションは、フォームが破棄されたときに実行されます。 破棄すると、延期されたアクションとして、現在のフォームが親から削除されます(登録解除)。
ネスト可能なフォームのディレクティブは、特定のニーズに合わせてさらにカスタマイズできます。たとえば、リアクティブフォームのサポートを削除したり、各子フォームを特定の名前(配列CHILD_FORMSではない)で登録したりできます。 nestableFormディレクティブのこの実装は、プロジェクトの要件を満たしているため、ここではそのように示しています。 新しいフォームを追加したり、既存のフォームを動的に削除したり(* ngIf)、フォームの状態を親に伝達したりするなど、いくつかの基本的なケースについて説明します。 これは基本的に、1つの変更検出サイクル(延期の有無にかかわらず)内で解決できる操作に要約されます。
2回の変更検出ラウンドを必要とする入力([required] =” someCondition”など)に条件付き検証を追加するなど、より高度なシナリオが必要な場合は、「one-detection-cycle-resolution」ルールのために機能しません。 Angular4によって課せられます。
とにかく、このディレクティブを使用する場合、または他のソリューションを実装する場合は、変更の検出に関連して言及されたことに十分注意してください。 この時点で、これがAngular4の実装方法です。 将来変更される可能性があります—私たちは知ることができません。 この記事で言及されたAngular4の現在の設定と強制された制限は、欠点または利点である可能性があります。 それはまだ分からない。
Angular4でフォームを簡単に
ご覧のとおり、Angularチームは、フォームに関連する多くの機能を提供する上で非常に優れた仕事をしてきました。 この投稿が、Angular 4でさまざまなタイプのフォームを操作するための完全なガイドとして役立ち、フォームのネストや変更検出のプロセスなど、より高度な概念についての洞察を提供することを願っています。
Angular 4フォーム(またはその他のAngular 4の主題)に関連するさまざまな投稿にもかかわらず、私の意見では、最良の出発点は公式のAngular4ドキュメントです。 また、Angularの連中は、コードに優れたドキュメントを持っています。 多くの場合、私は彼らのソースコードとそこにあるドキュメントを見るだけで解決策を見つけました。グーグルなどは何もありません。 前のセクションで説明したフォームのネストについて、Angular 4の学習を開始するAngularJS開発者は、ある時点でこの問題に遭遇すると思います。これは、この投稿を書くための私のインスピレーションのようなものでした。
これまで見てきたように、フォームには2つのタイプがあり、それらを一緒に使用できないという厳密な規則はありません。 コードベースをクリーンで一貫性のある状態に保つのは良いことですが、テンプレート駆動型のフォームを使用すると、より簡単に何かを実行できる場合もあれば、その逆の場合もあります。 したがって、バンドルサイズを少し大きくしてもかまわない場合は、ケースバイケースでより適切と思われるものを使用することをお勧めします。 混乱を招く可能性があるため、同じコンポーネント内でそれらを混合しないでください。
この投稿で使用されているプランカー
テンプレート駆動型フォーム
リアクティブフォーム、テンプレートバリデーター
リアクティブフォーム、コードバリデーター
リアクティブフォーム、フォームビルダー
同じコンポーネント内にネストされたテンプレート駆動型フォーム
同じコンポーネント内にネストされたリアクティブフォーム
コンポーネントのツリーを介したネストされたフォーム