この記事は、Angular の公式ドキュメントのReactive Formsの章を意訳したものです。所々抜け落ち、翻訳モレがありますがあしからずご了承を。
バージョン 4.3.2 のドキュメントをベースにしています。
Reactive Forms
リアクティブフォームは、Reactive スタイルでフォームを作成するためのAngularのテクニックです。
このガイドでは、リアクティブフォームを用いて「Heroの詳細編集」フォームを作る手順を説明していきます。
Reactive Formsライブサンプル/ダウンロードサンプルを試してみてください。
また、Reactive Forms Demo /ダウンロードのサンプル版を実行し、上部の「demo picker」から実装途中のサンプルを選択することもできます。
Reactive Form について
Angularは、リアクティブフォームとテンプレート駆動フォーム、2種類のフォーム構築テクノロジーを提供しています。
2つのテクノロジ―は、@angular/forms
ライブラリに属しており、フォームコントロールクラスの共通セットを共有しています。
ただ、リアクティブ型(ReactiveFormsModule
)とテンプレート駆動型(FormsModule
)では、哲学、プログラミングスタイル、技術において著しく異なります。
Reactive forms(リアクティブフォーム)
Angular リアクティブスタイルは、非UIデータモデル((通常はサーバーから取得される)間のデータフローの明示的な管理に有利なプログラミングの反応スタイルを容易にします。リアクティブフォームは、リアクティブパターン、テスト、およびバリデーションを簡単に適用できます。
リアクティブフォームでは、コンポーネントクラスにAngularフォームコントロールオブジェクトのツリーを作成し、このガイドで説明する方法を用いて、テンプレート側のネイティブフォームコントロール要素にバインドします。
フォームクラスコントロールオブジェクトは、コンポーネントクラスで直接作成と操作ができます。コンポーネントクラスはデータモデルとフォームコントロール構造の両方に対して即座にアクセスできるため、データモデル値をフォームコントロールにプッシュして、ユーザーが変更した値を取得することができます。コンポーネントは、フォーム制御状態の変化を監視(Observe)し、それらの変化に反応することができます。
フォームコントロールオブジェクトを直接操作することの利点の1つは、値とバリデーション状態のアップデートが常に同期して制御できることです。テンプレート駆動フォームで実装した場合に時々悩まされるタイミング問題に遭遇することもありませんし、Reactiveフォームは単体テストをより簡単に書くことができます。
リアクティブパラダイムに沿って、コンポーネントはデータモデルの不変性(immutability)を保存し、元の値を純粋なソースとして扱います。コンポーネントは、データモデルを直接更新するのではなく、ユーザーの変更を抽出して外部コンポーネントまたはサービスに転送します。これらのコンポーネントまたはサービスは、保存されたものなど何かを行い、更新されたモデル状態を反映する新しいデータモデルをコンポーネントに返します。
リアクティブフォームディレクティブを使用する場合、すべてのリアクティブの原理を遵守する必要はありませんが、リアクティブプログラミング手法が容易になります。
Template-driven forms(テンプレート駆動フォーム)
テンプレートガイドで紹介されているテンプレート駆動型は、まったく異なるアプローチです。
コンポーネントテンプレートにHTMLフォームコントロール(<input>
や<select>
など)を配置し、ngModel
などのディレクティブを使用してコンポーネントのデータモデルプロパティにバインドします。
Angularフォームコントロールオブジェクトは作成しません。Angularのディレクティブは、データバインディング内の情報を使用して、ディレクティブを作成します。 データ値をプッシュしたりプルすることはありません。AngularはngModel
でハンドリングします。Angularは、変更されたデータモデルをユーザーの変更に合わせて更新します。
このため、ngModel
ディレクティブはReactiveFormsModule
の一部ではありません。
これはコンポーネントクラスのコード量が少くて済むことを意味しますが、テンプレート駆動フォームは非同期であり、より高度なシナリオ下での開発を複雑にする可能性があります。
Async vs. sync(非同期 vs 同期)
リアクティブフォームは同期的です。テンプレート駆動フォームは非同期です。これは重要な違いです。
リアクティブフォームでは、フォームコントロールツリー全体をコードで作成します。すべてのコントロールが常に利用可能であるため、親フォームの子孫を介して値をすぐに更新したり、ドリルダウンしたりすることができます。
テンプレート駆動フォームは、フォームコントロールの作成をディレクティブに委任します。 “チェックした後に変更されました”というエラーを回避するために、これらのディレクティブはコントロールツリー全体を構築するために複数のサイクルを要します。つまり、コンポーネントクラス内からコントロールを操作する前に、チェックする必要があります。
例えば、フォームコントロールに@ViewChild(NgForm)
クエリを挿入し、それをngAfterViewInit
ライフサイクルフックで調べると、子コントロールがないことがわかります。コントロールから値を抽出したり、バリデーションのステータスをテストしたり、新しい値に設定したりするには、まずsetTimeout
を使用してチェックを待つ必要があります。
テンプレート駆動フォームの非同期性もユニットテストを複雑にします。テストブロックをasync()
またはfakeAsync()
でラップして、まだ存在しないフォーム内の値を探す必要はありません。リアクティブフォームでは、期待どおりのものがすべて利用可能です。
Reactive と template-driven、どっちがいいの?
どちらも「より良い」ものではありません。両者とも長所と短所を持つ、2つの異なるアーキテクチャのパラダイムです。あなたにとって最適なアプローチを選択してください。両方とも同一アプリケーション内で併用することもできます。
このリアクティブフォームガイドのバランスはリアクティブパラダイムを調査し、リアクティブフォームのテクニックのみに焦点を当てて説明します。テンプレート駆動フォームの詳細については、フォームガイドを参照してください。
次のセクションでは、リアクティブフォームのデモ用にプロジェクトを設定します。その後、Angularフォームクラスとそれをリアクティブフォームで実装する方法について学びます。
セットアップ
セットアップガイドの手順にしたがって、QuickStart seedを基にした新しいプロジェクトフォルダ(reactive-forms
と呼ばれる)を作成しましょう。
データモデルを作成する
このガイドの焦点は、Hero
を編集するリアクティブなフォームコンポーネントです。hero
クラスとheroデータが必要です。新しいdata-model.tsファイルをappディレクトリに作成し、下のコンテンツをそのディレクトリにコピーします。
src/app/data-model.ts
export class Hero {
id = 0;
name = '';
addresses: Address[];
}
export class Address {
street = '';
city = '';
state = '';
zip = '';
}
export const heroes: Hero[] = [
{
id: 1,
name: 'Whirlwind',
addresses: [
{street: '123 Main', city: 'Anywhere', state: 'CA', zip: '94801'},
{street: '456 Maple', city: 'Somewhere', state: 'VA', zip: '23226'},
]
},
{
id: 2,
name: 'Bombastic',
addresses: [
{street: '789 Elm', city: 'Smallville', state: 'OH', zip: '04501'},
]
},
{
id: 3,
name: 'Magneta',
addresses: [ ]
},
];
export const states = ['CA', 'MD', 'OH', 'VA'];
このファイルは、2つのクラスと2つの定数をエクスポートします。Address
クラスとHero
クラスは、アプリケーションデータモデルを定義します。 heroes
とstates
定数はテストデータを提供します。
reactive forms コンポーネントを作る
hero-detail.component.ts
という名前の新しいファイルをapp
ディレクトリに作成し、次のシンボルをインポートします。
src/app/hero-detail.component.ts
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
次に、HeroDetailComponent
メタデータを指定する@Component
デコレータを入力します。
src/app/hero-detail.component.ts (抜粋)
@Component({
selector: 'hero-detail',
templateUrl: './hero-detail.component.html'
})
次に、FormControl
を使用して、エクスポートされたHeroDetailComponent
クラスを作成します。 FormControl
は、FormControl
インスタンスを直接作成して管理できるディレクティブです。
src/app/hero-detail.component.ts (抜粋)
export class HeroDetailComponent1 {
name = new FormControl();
}
ここでは、name
というFormControl
を作成しています。テンプレート内では、ヒーロー名用のHTML input
ボックスにバインドされます。
FormControl
コンストラクタは、初期データ値、バリデータの配列、および非同期バリデータの配列の3つのオプションの引数を受け取ります。
このシンプルなコントロールにはデータやバリデータはありません。実際のアプリでは、ほとんどのフォームコントロールは両方を持っています。
このガイドでは、
Validators
について簡単に触れています。 詳細については、フォームバリデーションガイドを参照してください。
テンプレートを作成する
次に、コンポーネントのテンプレート src/app/hero-detail.component.html
を次のマークアップとともに作成します。
src/app/hero-detail.component.html
<h2>Hero Detail</h2>
<h3><i>Just a FormControl</i></h3>
<label class="center-block">Name:
<input class="form-control" [formControl]="name">
</label>
これがクラスのFormControl
という名前に関連付ける入力であることをAngularに知らせるには、<input>
のテンプレートに[formControl]="name"
が必要です。
フォームコントロールで使っているCSSクラス名は気にしなくて構いません。Bootstrap CSSに含まれているクラス名を使用しており、Angularで必要なクラス名ではありません。 このクラス名でフォームのスタイルを定義しますが、フォームのロジックには決して影響を与えません。
ReactiveFormsModule をインポートする
HeroDetailComponent
テンプレートは、ReactiveFormsModule
のformControlName
ディレクティブを使用します。
このサンプルでは、AppModule
でHeroDetailComponent
を宣言します。したがって、app.module.ts
で次の3つのことを行います。
-
ReactFormsModule
およびHeroDetailComponent
にアクセスするには、JavaScriptのimport
ステートメントを使用します。 -
ReactiveFormsModule
をAppModule
のインポートリストに追加します。 -
declarations
配列にHeroDetailComponent
を追加します。
src/app/app.module.ts (抜粋)
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms'; // <-- #1 import module
import { AppComponent } from './app.component';
import { HeroDetailComponent } from './hero-detail.component'; // <-- #1 import component
@NgModule({
imports: [
BrowserModule,
ReactiveFormsModule // <-- #2 add to @NgModule imports
],
declarations: [
AppComponent,
HeroDetailComponent, // <-- #3 declare app component
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
HeroDetailComponent を表示する
AppComponent
テンプレートを修正してHeroDetailComponent
を表示します。
src/app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<div class="container">
<h1>Reactive Forms</h1>
<hero-detail></hero-detail>
</div>`
})
export class AppComponent { }
必須のフォームクラス
コアフォームクラスの簡単な説明を読むと役立つ場合があります。
- *AbstractControl*は、3つの具体的なフォームコントロールクラス(
FormControl
、FormGroup
、およびFormArray
)の抽象基本クラスです。 それは共通の振る舞いと性質を提供し、そのいくつかは観察可能である。 - *FormControl*は、個々のフォームコントロールの値と有効性の状態を追跡します。 これは、入力ボックスやセレクタなどのHTMLフォームコントロールに対応します。
- *FormGroup*は、
AbstractControl
インスタンスのグループの値と有効性の状態を追跡します。 グループのプロパティには、子コントロールが含まれます。 コンポーネントの最上位フォームはFormGroup
です。 - *FormArray*は、数値的にインデックスされた
AbstractControl
インスタンスの配列の値と有効性の状態を追跡します。
このガイドでは、これらのクラスの詳細を学習します。
appをスタイリングする
AppComponent
とHeroDetailComponent
の両方のテンプレートHTMLに Bootstrap の CSSクラスを使用しました。BootstrapのCSSスタイルシートをindex.htmlの先頭に追加します。
index.html
<link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css">
これですべてが結ばれたので、ブラウザは次のようなものを表示するはずです:
FormGroupを追加する
通常、複数のFormControl
がある場合、それらを親FormGroup
内に登録したいと思うでしょう。これは簡単です。FormGroup
を追加するには、hero-detail.component.ts
のimports
セクションに追加します。
src/app/hero-detail.component.ts
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
このクラスでは、次のように、FormControl
をheroForm
というFormGroup
にラップします。
src/app/hero-detail.component.ts
export class HeroDetailComponent2 {
heroForm = new FormGroup ({
name: new FormControl()
});
}
クラスを変更したので、テンプレートに反映する必要があります。hero-detail.component.html
を次のように置き換えて更新してください。
src/app/hero-detail.component.html
<h2>Hero Detail</h2>
<h3><i>FormControl in a FormGroup</i></h3>
<form [formGroup]="heroForm" novalidate>
<div class="form-group">
<label class="center-block">Name:
<input class="form-control" formControlName="name">
</label>
</div>
</form>
ここで、単一の入力がform
要素にあることに注目してください。<form>
要素のnovalidate
属性は、ブラウザ側で実装されているネイティブHTMLバリデーションを実行するのを防ぎます。
formGroup
は、既存のFormGroup
インスタンスを受け取り、それをHTML要素に関連付けるリアクティブフォームディレクティブです。 この場合heroForm
として保存したFormGroup
をフォーム要素に関連付けます。
クラスにFormGroup
が追加されたので、コンポーネントクラスの対応するFormControl
に入力を関連付けるためのテンプレート構文を更新する必要があります。親FormGroup
がなければ、[formControl]="name"
はその指示文が単独で実行できる、つまりFormGroup
に属さずに動作するため、早期に動作しました。親のFormGroup
の場合、クラス内の正しいFormControl
に関連付けられるために、名前入力には構文formControlName="name"
が必要です。 この構文は、Angularが親FormGroup
(この場合はheroForm
)を検索し、そのグループ内でname
という名前のFormControl
を検索するように指示します。
form-group
CSSクラスは無視してください。 Bootstrap CSSライブラリが持っているCSSクラスであり、Angularのものではありません。form-control
クラスと同様に、フォームをスタイルしますが、Angularのロジックには決して影響しません。
フォームは素晴らしいように見えます。しかしそれは動作していますか?ユーザーが名前を入力すると、値はどこに移動しますか?
フォームモデルを見てみる
値は、グループフォームコントロールをバックアップするフォームモデルに入ります。 フォームモデルを表示するには、hero-detail.component.htmlの閉じるフォームタグの後に次の行を追加します。
src/app/hero-detail.component.html
<p>Form value: {{ heroForm.value | json }}</p>
<p>Form status: {{ heroForm.status | json }}</p>
heroForm.value
はフォームモデルを返します。 JsonPipe
にパイプを張り、モデルをブラウザのJSONとしてレンダリングします。
初期名プロパティの値は空の文字列です。name
入力ボックスに入力し、キーストロークがJSONに表示されるのを見てください。
すばらしいです!これでフォームの基本機能を持つことができました!。
実アプリでは、フォームが大幅に高速化します。FormBuilder
はフォームの開発とメンテナンスを容易にします。
FormBuilder について
FormBuilder
クラスは、コントロールの作成の詳細を処理することで、繰り返しや混乱を軽減します。
FormBuilder
を使用するには、hero-detail.component.ts
にインポートする必要があります。
src/app/hero-detail.component.ts (抜粋)
import { Component } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
このプランに沿って、HeroDetailComponent
を読み書きするのが少し簡単なものにリファクタリングするために今すぐ使ってください:
-
heroForm
プロパティの型をFormGroup
に明示的に宣言します。後でそれを初期化します。 -
FormBuilder
をコンストラクタに挿入します。 -
FormBuilder
を使用してheroForm
を定義する新しいメソッドを追加します。createForm
と呼んでください。 - コンストラクタで
createForm
を呼び出します。
修正されたHeroDetailComponent
は次のようになります。
src/app/hero-detail.component.ts (抜粋)
export class HeroDetailComponent3 {
heroForm: FormGroup; // <--- heroForm is of type FormGroup
constructor(private fb: FormBuilder) { // <--- inject FormBuilder
this.createForm();
}
createForm() {
this.heroForm = this.fb.group({
name: '', // <--- the FormControl called "name"
});
}
}
FormBuilder.group
は、FormGroup
を作成するファクトリメソッドです。 FormBuilder.group
は、キーと値がFormControl
の名前とその定義を持つオブジェクトを取ります。 この例ではname
コントロールは初期データ値で定義され、空の文字列です。
1つのオブジェクトにコントロールのグループを定義すると、コンパクトで読みやすいスタイルになります。それはnew FormControl(...)
ステートメントの等価なシリーズを書くのに勝っています。
Validators.required
このガイドではバリデーションに深く関与していませんが、ここではリアクティブ形式で必要とされるValidators.required
を使用する単純さを示す1つの例です。
まず、Validators
シンボルをインポートします。
src/app/hero-detail.component.ts (抜粋)
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
FormControl
という名前を必須にするには、FormGroup
のname
プロパティを配列に置き換えます。 最初の項目はname
の初期値です。2番目は必要なバリデータValidators.required
です。
src/app/hero-detail.component.ts (抜粋)
this.heroForm = this.fb.group({
name: ['', Validators.required ],
});
リアクティブバリデータはシンプルで構成可能な関数です。バリデータをディレクティブでラッパーする必要があるテンプレート駆動フォームでは、バリデーションの設定は難しくなります。
テンプレートの下部にある診断メッセージを更新して、フォームのバリデーションステータスを表示しましょう。
src/app/hero-detail.component.html (抜粋)
<p>Form value: {{ heroForm.value | json }}</p>
<p>Form status: {{ heroForm.status | json }}</p>
ブラウザでは次のように表示されます。
Validators.required
が機能しています。 入力ボックスに値がないため、ステータスはINVALID
です。 入力ボックスに入力すると、ステータスがINVALID
からVALID
に変更されます。
実際のアプリでは、診断メッセージをユーザーフレンドリーなエクスペリエンスに置き換えます。
Validators.required
の使用は、残りのガイドではオプションです。 同じ構成の次の各例にとどまります。
Angularフォームの検証の詳細については、Form Validationガイドを参照してください。
その他のFormControls
hero
は名前以上のものを持っています。 ヒーローには住所、スーパーパワー、時にはサイドキックがあります。
住所には状態プロパティがあります。 ユーザーは<select>
ボックスで状態を選択し、<option>
要素で状態を設定します。したがって、data-model.ts
から状態をインポートしてください。
src/app/hero-detail.component.ts (抜粋)
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { states } from './data-model';
states
プロパティを宣言し、次のようにいくつかのアドレスFormControls
をheroForm
に追加します。
src/app/hero-detail.component.ts (抜粋)
export class HeroDetailComponent4 {
heroForm: FormGroup;
states = states;
constructor(private fb: FormBuilder) {
this.createForm();
}
createForm() {
this.heroForm = this.fb.group({
name: ['', Validators.required ],
street: '',
city: '',
state: '',
zip: '',
power: '',
sidekick: ''
});
}
}
その後、対応するマークアップをフォーム要素内のhero-detail.component.html
に追加します。
<h2>Hero Detail</h2>
<h3><i>A FormGroup with multiple FormControls</i></h3>
<form [formGroup]="heroForm" novalidate>
<div class="form-group">
<label class="center-block">Name:
<input class="form-control" formControlName="name">
</label>
</div>
<div class="form-group">
<label class="center-block">Street:
<input class="form-control" formControlName="street">
</label>
</div>
<div class="form-group">
<label class="center-block">City:
<input class="form-control" formControlName="city">
</label>
</div>
<div class="form-group">
<label class="center-block">State:
<select class="form-control" formControlName="state">
<option *ngFor="let state of states" [value]="state">{{state}}</option>
</select>
</label>
</div>
<div class="form-group">
<label class="center-block">Zip Code:
<input class="form-control" formControlName="zip">
</label>
</div>
<div class="form-group radio">
<h4>Super power:</h4>
<label class="center-block"><input type="radio" formControlName="power" value="flight">Flight</label>
<label class="center-block"><input type="radio" formControlName="power" value="x-ray vision">X-ray vision</label>
<label class="center-block"><input type="radio" formControlName="power" value="strength">Strength</label>
</div>
<div class="checkbox">
<label class="center-block">
<input type="checkbox" formControlName="sidekick">I have a sidekick.
</label>
</div>
</form>
<p>Form value: {{ heroForm.value | json }}</p>
注意:このマークアップでは、
form-group
,form-control
,center-block
,checkbox
の多くを無視します。これらは、Angular自身が無視するBootstrap CSSクラスです。formGroupName
属性とformControlName
属性に注意してください。Angularディレクティブは、HTMLコントロールをコンポーネントクラスのAngularFormGroup
およびFormControl
プロパティにバインドします。
修正されたテンプレートには、より多くのテキスト入力、state
の選択ボックス、power
のラジオボタン、およびsidekick
のチェックボックスが含まれています。
オプションのvalue
プロパティを[value]="state"
でバインドする必要があります。 値をバインドしないと、select
にはデータモデルの最初のオプションが表示されます。
コンポーネントクラスは、テンプレート内の表現に関係なくコントロールプロパティを定義します。state
コントロール、power
コントロール、およびsidekick
コントロールは、name
コントロールを定義したのと同じ方法で定義します。FormControlName
ディレクティブでFormControl
名を指定して、これらのコントロールをテンプレートHTMLエレメントに同じ方法で関連付けます。
ラジオボタン、プルダウンメニュー(select)、およびチェックボックスの詳細については、APIリファレンスを参照してください。
ネストされたFormGroup
このフォームは大きくなって扱いにくいものになっています。関連するFormControl
の一部を入れ子になったFormGroup
にまとめることができます。 通り(Street)、都市、州、および郵便番号は、FormGroup
という住所に適したプロパティです。 グループとコントロールをこのようにネストすると、データモデルの階層構造をミラーリングし、関連する一連のコントロールのバリデーションと状態の追跡に役立ちます。
FormBuilder
を使用して、このコンポーネントのheroForm
という1つのFormGroup
を作成しました。 これを親のFormGroup
としましょう。FormBuilder
を再度使用して、アドレスコントロールをカプセル化する子FormGroup
を作成します。 親FormGroup
の新しいアドレスプロパティに結果を割り当てます。
src/app/hero-detail.component.ts (抜粋)
export class HeroDetailComponent5 {
heroForm: FormGroup;
states = states;
constructor(private fb: FormBuilder) {
this.createForm();
}
createForm() {
this.heroForm = this.fb.group({ // <-- the parent FormGroup
name: ['', Validators.required ],
address: this.fb.group({ // <-- the child FormGroup
street: '',
city: '',
state: '',
zip: ''
}),
power: '',
sidekick: ''
});
}
}
コンポーネントクラスのフォームコントロールの構造を変更しました。コンポーネントテンプレートに対応する調整を行う必要があります。
hero-detail.component.html
では、アドレス関連のFormControl
をdiv
にラップします。div
にformGroupName
ディレクティブを追加し、それを"address"
にバインドします。 これは、親FormGroup
内のheroForm
というアドレスの子FormGroup
のプロパティです。
この変更を視覚的に明白にするには、最上部の<h4>
ヘッダーをテキスト「Secret Lair」で伝えます。 新しいアドレスHTMLは次のようになります。
src/app/hero-detail.component.html (抜粋)
<div formGroupName="address" class="well well-lg">
<h4>Secret Lair</h4>
<div class="form-group">
<label class="center-block">Street:
<input class="form-control" formControlName="street">
</label>
</div>
<div class="form-group">
<label class="center-block">City:
<input class="form-control" formControlName="city">
</label>
</div>
<div class="form-group">
<label class="center-block">State:
<select class="form-control" formControlName="state">
<option *ngFor="let state of states" [value]="state">{{state}}</option>
</select>
</label>
</div>
<div class="form-group">
<label class="center-block">Zip Code:
<input class="form-control" formControlName="zip">
</label>
</div>
</div>
これらの変更の後、ブラウザのJSON出力には、FormGroup
というネストされたアドレスを持つ修正されたフォームモデルが表示されます。
すばらしいです!あなたはグループを作ったので、テンプレートとフォームモデルがコミュニケーションしていることがわかります。
FormControl プロパティを検証する
現時点では、フォームモデル全体をページにダンプしています。 場合によっては、特定のFormControl
の1つの状態だけに興味がある場合もあります。
フォーム内の個々のFormControl
を.get()
メソッドで抽出することで検査できます。 これをコンポーネントクラス内で行うこともできますし、{{form.value | json}}
インターポーレーションを次のように行います。
src/app/hero-detail.component.html
<p>Name value: {{ heroForm.get('name').value }}</p>
FormGroup
内にあるFormControl
の状態を取得するには、ドット表記を使用してコントロールに渡します。
src/app/hero-detail.component.html
<p>Street value: {{ heroForm.get('address.street').value}}</p>
この方法を使用すると、次のいずれかのようなFormControl
のプロパティを表示できます。
プロパティ | 説明 |
---|---|
myControl.value |
FormControl の値 |
myControl.status |
FormControl のバリデーション状態。可能な値は、VALID 、INVALID 、PENDING 、またはDISABLED です。 |
myControl.pristine |
ユーザーがUIの値を変更していない場合はtrue 。その反対はmyControl.dirty です。 |
myControl.untouched |
ユーザがまだHTMLコントロールにフォーカス当てておらず、blur イベントがトリガされるまでtrue になります。その反対はmyControl.touched です。 |
AbstractControl APIリファレンスの他のFormControl
プロパティについて学んでください。
FormControlプロパティを確認する一般的な理由の1つは、ユーザーが有効な値を入力したことを確認することです。Angular のフォームバリデーション詳細については、フォームバリデーションガイドを参照してください。
data modelとform model
現時点では、フォームは空の値を表示しています。 HeroDetailComponent
はヒーローの値を表示する必要があります。ヒーローは、おそらくリモートサーバーから取得したヒーローです。
このアプリでは、HeroDetailComponent
はHeroListComponent
の親からヒーローを取得します
サーバーのヒーローはデータモデルです。 FormControl
構造体はフォームモデルです。
コンポーネントは、データモデルのヒーロー値をフォームモデルにコピーする必要があります。 重要な意味合いは2つあります。
- 開発者は、データモデルのプロパティをフォームモデルのプロパティにマップする方法を理解する必要があります。
- ユーザーの変更は、DOM要素からデータモデルにではなくフォームモデルに流れます。 フォームコントロールはデータモデルを更新しません。
フォームとデータモデルの構造は正確に一致する必要はありません。 特定の画面にデータモデルのサブセットを提示することがよくあります。 しかし、フォームモデルの形状がデータモデルの形状に近い場合は、作業が簡単になります。
このHeroDetailComponent
では、2つのモデルが非常に近いです。
data-model.ts
のHero
の定義を思い出してください:
src/app/data-model.ts (クラス)
export class Hero {
id = 0;
name = '';
addresses: Address[];
}
export class Address {
street = '';
city = '';
state = '';
zip = '';
}
ここでも、コンポーネントのFormGroup
定義があります。
src/app/hero-detail.component.ts (抜粋)
this.heroForm = this.fb.group({
name: ['', Validators.required ],
address: this.fb.group({
street: '',
city: '',
state: '',
zip: ''
}),
power: '',
sidekick: ''
});
ここでも、コンポーネントのFormGroup
定義があります。これらのモデルには2つの重要な違いがあります。
-
Hero
はIDを持っています。フォームモデルは、一般的にユーザーにプライマリキーを表示しないためではありません。 -
Hero
には住所の配列を持っています。このフォームモデルは1つのアドレスしか表示せず、以下で再選択します。
それにもかかわらず、2つのモデルはかなり近似しており、このアライメントが、データモデルのプロパティをpatchValue
メソッドとsetValue
メソッドを使用してフォームモデルにコピーする作業を簡単に見ていきます。
簡潔さと明快さのためにFormGroup
の定義を次のようにリファクタリングしてください。
src/app/hero-detail-7.component.ts
this.heroForm = this.fb.group({
name: ['', Validators.required ],
address: this.fb.group(new Address()), // <-- a FormGroup with a new address
power: '',
sidekick: ''
});
Hero
クラスとAddress
クラスを参照できるように、data-model
からインポートを更新するようにしてください。
src/app/hero-detail-7.component.ts
import { Address, Hero, states } from './data-model';
setValueとpatchValueを使用してフォームモデルを作成する
以前はコントロールを作成し、同時にその値を初期化しました。setValue
メソッドとpatchValue
メソッドで後で値を初期化またはリセットすることもできます。
setValue
setValue
を使用すると、FormGroup
の背後のフォームモデルと完全に一致するプロパティを持つデータオブジェクトを渡すことで、すべてのフォームコントロール値を一度に割り当てることができます。
src/app/hero-detail.component.ts (抜粋)
this.heroForm.setValue({
name: this.hero.name,
address: this.hero.addresses[0] || new Address()
});
setValueメソッドは、フォームコントロールの値を割り当てる前にデータオブジェクトを完全にチェックします。
FormGroup構造と一致しないデータオブジェクトを受け入れることも、グループ内の任意のコントロールの値が欠けていることもあります。 このようにして、タイプミスがあったり、コントロールが正しく入れ子になっていないと、役に立つエラーメッセージを返すことがあります。 patchValueは自動的に失敗します。
一方、setValueはエラーをキャッチし、明確に報告します。
ヒーローは、その形状がコンポーネントのFormGroup構造に似ているので、ほとんどすべてのヒーローをsetValueの引数として使用できます。
あなたはヒーローの最初のアドレスのみを表示することができ、ヒーローには全くアドレスがない可能性を考慮する必要があります。 これは、データオブジェクト引数のアドレスプロパティの条件付き設定を説明しています。
src/app/hero-detail-7.component.ts
address: this.hero.addresses[0] || new Address()
patchValue
patchValue
を使用すると、対象のコントロールだけのキー/値ペアのオブジェクトを指定することで、FormGroup
の特定のコントロールに値を割り当てることができます。
この例では、フォームの名前コントロールのみを設定します。
src/app/hero-detail.component.ts (抜粋)
this.heroForm.patchValue({
name: this.hero.name
});
patchValue
を使用すると、多岐にわたるデータやフォームモデルに柔軟に対応できます。 しかし、setValue
とは異なり、patchValue
は欠けているコントロール値をチェックすることはできず、有用なエラーを投げません。
フォームモデル値(ngOnChanges
)をセットするタイミング
これで、フォームモデルの値を設定する方法が分かりました。 しかし、いつそれらを設定しますか? その答えは、コンポーネントがデータモデル値をいつ取得するかによって異なります。
このリアクティブフォームのHeroDetailComponent
は、マスター/ディテールのHeroListComponent
(以下で説明します)内にネストされています。 HeroListComponent
はヒーロー名をユーザーに表示します。 ユーザーがヒーローをクリックすると、リストコンポーネントはヒーロー入力プロパティにバインドして、選択されたヒーローをHeroDetailComponent
に渡します。
hero-list.component.html (簡略)
<nav>
<a *ngFor="let hero of heroes | async" (click)="select(hero)">{{hero.name}}</a>
</nav>
<div *ngIf="selectedHero">
<hero-detail [hero]="selectedHero"></hero-detail>
</div>
このアプローチでは、ユーザーが新しいヒーローを選択するたびに、HeroDetailComponent
のヒーローの値が変更されます。 ngOnChanges
フックでsetValue
を呼び出す必要があります。これは、次の手順で示すようにheroInputプロパティが変更されたときに呼び出されます。
まず、hero-detail.component.ts
にOnChanges
とInput
シンボルをインポートします。
src/app/hero-detail.component.ts (core imports)
import { Component, Input, OnChanges } from '@angular/core';
hero
Input プロパティを追加します。
src/app/hero-detail-6.component.ts
@Input() hero: Hero;
次のようにngOnChanges
メソッドをクラスに追加します。
src/app/hero-detail.component.ts (ngOnchanges)
ngOnChanges()
this.heroForm.setValue({
name: this.hero.name,
address: this.hero.addresses[0] || new Address()
});
}
フォームフラグをリセットする
ヒーローが変更されたときにフォームをリセットして、前のヒーローのコントロール値がクリアされ、状態フラグが元の状態に復元されるようにする必要があります。 このようにngOnChanges
の先頭でリセットを呼び出すことができます。
src/app/hero-detail-7.component.ts
this.heroForm.reset();
reset
メソッドにはオプションのstate
値があるため、フラグと制御値を同時にリセットできます。 内部的には、reset
は引数をsetValue
に渡します。 少しのリファクタリングとngOnChanges
がこれになります:
src/app/hero-detail.component.ts (ngOnchanges - revised)
ngOnChanges() {
this.heroForm.reset({
name: this.hero.name,
address: this.hero.addresses[0] || new Address()
});
}
HeroListComponent と HeroService を作成する
HeroDetailComponent
は、マスター/詳細ビューのHeroListComponent
のネストされたサブコンポーネントです。これらを組み合わせると下記のように見えます:
HeroListComponent
はinjectされたHeroService
を使用してサーバーからヒーローを取り出し、そのヒーローを一連のボタンとしてユーザーに提示します。 HeroService
はHTTPサービスをエミュレートします。ネットワークレイテンシをシミュレートし、視覚的にアプリケーションの必然的に非同期性を示すために、短い遅延の後に解決するヒーローObservable
を返します。
ユーザーがヒーローをクリックすると、コンポーネントはHeroDetailComponent
のヒーロー入力プロパティにバインドされているselectedHero
プロパティを設定します。 HeroDetailComponent
は変更されたヒーローを検出し、そのヒーローのデータ値でフォームを再設定します。
「Refresh」ボタンはヒーローリストと現在選択されているヒーローをクリアしてから、ヒーローをリフレッシュします。
残りのHeroListComponent
とHeroService
の実装の詳細は、リアクティブフォームの理解には関係ありません。関連するテクニックは、こことここのHeroツアーを含むドキュメンテーションの別の場所でカバーされています。
このリアクティブフォームチュートリアルの手順に従ってコーディングする場合は、以下に示すソースコードに基づいて適切なファイルを作成します。hero-list.component.ts
はObservable
とfinally
をインポートし、hero.service.ts
がrxjs
からObservable
、of
、およびDelay
をインポートする間にインポートされます。その後、ここに戻り、フォーム配列のプロパティについて学びます。
FormArray
を使用してFormGroup
の配列を表示する
これまで、FormControls
とFormGroup
を見てきました。FormGroup
は、プロパティ値がFormControls
および他のFormGroups
である名前付きオブジェクトです。
場合によっては、任意の数のコントロールやグループを表示する必要があります。 たとえば、主人公はゼロ、1つ、または任意の数のアドレスを持つことができます。
Hero.addresses
プロパティは、Address
インスタンスの配列です。 アドレスFormGroup
は1つのアドレスを表示できます。 Angular FormArray
は、FormGroups
のアドレスの配列を表示できます。
FormArray
クラスにアクセスするには、hero-detail.component.ts
にインポートします。
src/app/hero-detail.component.ts (抜粋)
import { Component, Input, OnChanges } from '@angular/core';
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Address, Hero, states } from './data-model';
FormArrayを操作するには、次の手順を実行します。
- 配列に項目(
FormControls
またはFormGroups
)を定義します。 - データモデル内のデータから作成された項目を使用して配列を初期化します。
- ユーザーが必要とするアイテムを追加して削除します。
このガイドでは、Hero.address
用のFormArray
を定義し、ユーザーがアドレスを追加または変更できるようにします(アドレスの削除は宿題とします)。
HeroDetailComponent
コンストラクタでフォームモデルを再定義する必要があります。コンストラクタは現在、FormGroup
のアドレスに最初のヒーローアドレスのみを表示します。
src/app/hero-detail-7.component.ts
this.heroForm = this.fb.group({
name: ['', Validators.required ],
address: this.fb.group(new Address()), // <-- a FormGroup with a new address
power: '',
sidekick: ''
});
addressから*secret lairs(秘密の寝室)*まで
ユーザーの視点から見ると、ヒーローには住所がありません。住所は単なる死人のためのものです。英雄は秘密の寝所を持っています!FormGroup
定義の住所をsecretLairs FormArray
定義に置き換えます。
src/app/hero-detail-8.component.ts
this.heroForm = this.fb.group({
name: ['', Validators.required ],
secretLairs: this.fb.array([]), // <-- secretLairs as an empty FormArray
power: '',
sidekick: ''
});
フォーム制御名を
address
からsecretLairs
に変更することは、重要なポイントです。フォームモデルはデータモデルと一致する必要はありません。
明らかに両者の間には関係がなければなりません。しかし、アプリケーションドメイン内では意味をなさないものになる可能性があります。
プレゼンテーション要件は、データ要件とはしばしば異なります。リアクティブ型のアプローチは、この区別を強調し、容易にします。
"secretLairs" FormArray を初期化する
デフォルトのフォームでは、アドレスのない名前のないヒーローが表示されます。
親HeroListComponent
がHeroListComponent.hero
入力プロパティを新しいHero
に設定するたびに、secretLairsに実際のヒーローアドレスを設定する(または再投入する)メソッドが必要です。
次のsetAddresses
メソッドは、secretLairs FormArray
をheroの住所が格納されたFormGroups
配列で初期化された新しいFormArray
に置き換えます。
src/app/hero-detail-8.component.ts
setAddresses(addresses: Address[]) {
const addressFGs = addresses.map(address => this.fb.group(address));
const addressFormArray = this.fb.array(addressFGs);
this.heroForm.setControl('secretLairs', addressFormArray);
}
以前のFormArray
をsetValue
ではなくFormGroup.setControl
メソッドで置き換えることに注目してください。あなたは、コントロールの値ではなく、コントロールを置き換えています。
また、secretLairs FormArray
にはAddresses
ではなくFormGroups
が含まれています。
FormArray を取得する
HeroDetailComponent
は、secretLairs FormArray
からアイテムを表示、追加、および削除できる必要があります。
FormGroup.get
メソッドを使用して、そのFormArray
への参照を取得します。明快さと再利用のために、式をsecretLairs
コンビニエンスプロパティにラップします。
FormArray を表示する
現在のHTMLテンプレートは単一のアドレスFormGroup
を表示します。ヒーローの住所FormGroups
の0、1つ、またはそれ以上を表示するように修正します。
これは主に、前のテンプレートHTMLを<div>
内のアドレスにラップし、その<div>
を* ngFor
で繰り返すことです。
そのトリックは*ngFor
の書き方を知ることにあります。3つの重要なポイントがあります:
- 別の折り返し
<div>
を*ngFor
で<div>
の周りに追加し、そのformArrayName
ディレクティブを "secretLairs" に設定します。この手順は、内側の繰り返しHTMLテンプレートのフォームコントロールのコンテキストとしてsecretLairsFormArray
を確立します。 - 繰り返されるアイテムのソースは、
FormArray
自体ではなく、FormArray.controls
です。各コントロールはFormGroup
というアドレスです。これは以前の(今度は繰り返された)テンプレートのHTMLとまったく同じです。 -
FormGroup
を繰り返すたびに、FormArray
内のFormGroup
のインデックスでなければならない固有のformGroupName
が必要です。このインデックスを再利用して、各アドレスの一意のラベルを作成します。
HTMLテンプレートのsecret lairsセクションの骨組みは次のとおりです。
*src/app/hero-detail.component.html (ngFor)
<div formArrayName="secretLairs" class="well well-lg">
<div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" >
<!-- The repeated address template -->
</div>
</div>
secret lairsのセクションの完全なテンプレートは次のとおりです。
src/app/hero-detail.component.html (抜粋)
<div formArrayName="secretLairs" class="well well-lg">
<div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" >
<!-- The repeated address template -->
<h4>Address #{{i + 1}}</h4>
<div style="margin-left: 1em;">
<div class="form-group">
<label class="center-block">Street:
<input class="form-control" formControlName="street">
</label>
</div>
<div class="form-group">
<label class="center-block">City:
<input class="form-control" formControlName="city">
</label>
</div>
<div class="form-group">
<label class="center-block">State:
<select class="form-control" formControlName="state">
<option *ngFor="let state of states" [value]="state">{{state}}</option>
</select>
</label>
</div>
<div class="form-group">
<label class="center-block">Zip Code:
<input class="form-control" formControlName="zip">
</label>
</div>
</div>
<br>
<!-- End of the repeated address template -->
</div>
</div>
FormArray に新しい寝所を追加する
addLair
メソッドを追加して、secretLairs FormArray
を取得し、新しいアドレスFormGroup
を追加します。
src/app/hero-detail.component.ts (addLair メソッド)
addLair() {
this.secretLairs.push(this.fb.group(new Address()));
}
フォーム上にボタンを配置すると、ユーザーは新しい秘密の寝所を追加し、それをコンポーネントのaddLair
メソッドに渡すことができます。
src/app/hero-detail.component.html (addLair button)
<button (click)="addLair()" type="button">Add a Secret Lair</button>
type="button"
属性を必ず追加してください。実際にはボタンのtype
を常に指定する必要があります。明示的な型がなければ、ボタンタイプはデフォルトで"submit"になります。 後でフォーム送信アクションを追加すると、すべての「送信」ボタンが送信アクションをトリガーし、現在の変更を保存するようなことが起きる可能性があります。 ユーザーがAdd a Secret Lairボタンをクリックしたときに、変更内容を保存する必要はありません。
Try it!
ブラウザーに戻って、「Magneta」という名の英雄を選択します。 フォームの下部にある診断JSONに表示されているように、「Magneta」にはアドレスはありません。
[Add a Secret Lair]ボタンをクリックします。 新しいアドレスセクションが表示されます。よく頑張りました!
ねぐらを削除する
この例ではアドレスを追加できますが、アドレスは削除できません。さらなる信頼のために、removeLairメソッドを記述し、それを繰り返しアドレスHTMLのボタンにつなぎましょう。
コントロールの変更を監視する
Angularは、ユーザーが親HeroListComponent
のヒーローを選択したときにngOnChanges
を呼び出します。 ヒーローを選ぶと、HeroDetailComponent.hero
Inputプロパティが変更されます。
Angularは、ユーザーがヒーローの名前または秘密の兵士を変更したときにngOnChanges
を呼び出しません。 幸いにも、変更イベントを発生させるフォームコントロールプロパティの1つにsubscribeすることで、そのような変更について知ることができます。
これらは、RxJS Observable
を返し、valueChanges
などのプロパティです。 フォーム制御値を監視するためにRxJS Observable
について多くのことを知る必要はありません。
FormControl
という名前の値の変更を記録する次のメソッドを追加します。
src/app/hero-detail.component.ts (logNameChange)
nameChangeLog: string[] = [];
logNameChange() {
const nameControl = this.heroForm.get('name');
nameControl.valueChanges.forEach(
(value: string) => this.nameChangeLog.push(value)
);
}
フォームを作成した後、コンストラクターで呼び出します。
src/app/hero-detail-8.component.ts
constructor(private fb: FormBuilder) {
this.createForm();
this.logNameChange();
}
logNameChange
メソッドは、名前変更値をnameChangeLog
配列にプッシュします。 この*ngFor
バインディングを使ってコンポーネントテンプレートの一番下に配列を表示します:
src/app/hero-detail.component.html (Name change log)
<h4>Name change log</h4>
<div *ngFor="let name of nameChangeLog">{{name}}</div>
ブラウザに戻り、ヒーロー(たとえば「マゼンタ」)を選択し、名前入力ボックスに入力を開始します。 各キーストローク後にログに新しい名前が表示されます。
それはいつ使うの?
インターポーレーション バインディングは、名前の変更を表示する簡単な方法です。観察可能なフォームコントロールプロパティを購読することは、コンポーネントクラス内のアプリケーションロジックをトリガーするのに便利です。
データを保存する
HeroDetailComponent
はユーザーの入力をキャプチャしますが、何もしません。 実際のアプリでは、おそらくそれらのヒーローの変更を保存します。 実際のアプリでは、保存していない変更を元に戻して編集を再開することもできます。 このセクションで両方の機能を実装すると、フォームは次のようになります。
保存
このサンプルアプリケーションでは、ユーザーがフォームを送信すると、HeroDetailComponent
はヒーローデータモデルのインスタンスを、injectされたHeroService
のsave
メソッドに渡します。
src/app/hero-detail.component.ts (onSubmit)
onSubmit() {
this.hero = this.prepareSaveHero();
this.heroService.updateHero(this.hero).subscribe(/* error handling */);
this.ngOnChanges();
}
このオリジナルのhero
には保存前の値がありました。 ユーザーの変更はまだフォームモデルにあります。したがって、オリジナルのヒーロー値(hero.id
)と、変更されたフォームモデル値の詳細コピー(prepareSaveHero
ヘルパーを使用)の組み合わせから新しいヒーローを作成します。
src/app/hero-detail.component.ts (prepareSaveHero)
prepareSaveHero(): Hero {
const formModel = this.heroForm.value;
// deep copy of form model lairs
const secretLairsDeepCopy: Address[] = formModel.secretLairs.map(
(address: Address) => Object.assign({}, address)
);
// return new `Hero` object containing a combination of original hero value(s)
// and deep copies of changed form model values
const saveHero: Hero = {
id: this.hero.id,
name: formModel.name as string,
// addresses: formModel.secretLairs // <-- bad!
addresses: secretLairsDeepCopy
};
return saveHero;
}
Address deep copy
formModel.secretLair
をsaveHero.addresses
(コメント行を参照)に割り当てていた場合、saveHero.addresses
配列のアドレスはformModel.secretLairs
の椅子と同じオブジェクトになります。その後のユーザーの恋人の通りへの変更は、saveHero
の住所の通りを変更することになります。
prepareSaveHero
メソッドは、フォームモデルのsecretLairs
オブジェクトのコピーを生成しないようにします。
取り消し(変更のキャンセル)
Revertボタンを押すと、ユーザーは変更をキャンセルし、フォームを元の状態に戻します。 元に戻すのは簡単です。オリジナルの変更されていないヒーローデータモデルからフォームモデルを構築したngOnChanges
メソッドを再実行するだけです。
src/app/hero-detail.component.ts (revert)
revert() { this.ngOnChanges(); }
ボタン
コンポーネントのテンプレートの上部にある「Save」ボタンと「Revert」ボタンを追加します。
src/app/hero-detail.component.html (Save and Revert buttons)
<form [formGroup]="heroForm" (ngSubmit)="onSubmit()" novalidate>
<div style="margin-bottom: 1em">
<button type="submit"
[disabled]="heroForm.pristine" class="btn btn-success">Save</button>
<button type="reset" (click)="revert()"
[disabled]="heroForm.pristine" class="btn btn-danger">Revert</button>
</div>
<!-- Hero Detail Controls -->
<div class="form-group radio">
<h4>Super power:</h4>
<label class="center-block"><input type="radio" formControlName="power" value="flight">Flight</label>
<label class="center-block"><input type="radio" formControlName="power" value="x-ray vision">X-ray vision</label>
<label class="center-block"><input type="radio" formControlName="power" value="strength">Strength</label>
</div>
<div class="checkbox">
<label class="center-block">
<input type="checkbox" formControlName="sidekick">I have a sidekick.
</label>
</div>
</form>
フォームコントロール(heroForm.dirty
)のいずれかの値を変更してフォームを「Dirty」にするまで、ボタンは無効になります。
タイプ「submit
」のボタンをクリックすると、コンポーネントのonSubmit
メソッドを呼び出すngSubmit
イベントがトリガされます。Revert ボタンをクリックすると、コンポーネントのrevert
メソッドが呼び出されます。ユーザーは変更を保存または元に戻すことができます。
これはデモの最後のステップです。 Reactive Forms(final)を Plunker で試すか、サンプルをダウンロードしてください。
まとめ
このページでは下記の内容を紹介しました。
- リアクティブフォームコンポーネントとそれに対応するテンプレートを作成する方法。
-
FormBuilder
を使用してリアクティブフォームのコードを簡単にする方法。 -
FormControls
のグループ化。 -
FormControl
プロパティの検査。 -
patchValue
とsetValue
でデータを設定する。 -
FormArray
で動的にグループを追加する。 -
FormControl
の値に対する変更を観察します。 - フォームの変更の保存します。
最終バージョンのファイルは次のとおりです。
(省略)
このガイドのすべての手順の完全なソースは、Reactive Forms Demo/ダウンロードサンプルのライブサンプルからダウンロードできます。