LoginSignup
32
35

More than 5 years have passed since last update.

angular.io Guide: Reactive Forms

Posted at

この記事は、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クラスは、アプリケーションデータモデルを定義します。 heroesstates定数はテストデータを提供します。

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テンプレートは、ReactiveFormsModuleformControlNameディレクティブを使用します。

このサンプルでは、AppModuleHeroDetailComponentを宣言します。したがって、app.module.tsで次の3つのことを行います。

  1. ReactFormsModuleおよびHeroDetailComponentにアクセスするには、JavaScriptのimportステートメントを使用します。
  2. ReactiveFormsModuleAppModuleのインポートリストに追加します。
  3. 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つの具体的なフォームコントロールクラス(FormControlFormGroup、およびFormArray)の抽象基本クラスです。 それは共通の振る舞いと性質を提供し、そのいくつかは観察可能である。
  • FormControlは、個々のフォームコントロールの値と有効性の状態を追跡します。 これは、入力ボックスやセレクタなどのHTMLフォームコントロールに対応します。
  • FormGroupは、AbstractControlインスタンスのグループの値と有効性の状態を追跡します。 グループのプロパティには、子コントロールが含まれます。 コンポーネントの最上位フォームはFormGroupです。
  • FormArrayは、数値的にインデックスされたAbstractControlインスタンスの配列の値と有効性の状態を追跡します。 このガイドでは、これらのクラスの詳細を学習します。

appをスタイリングする

AppComponentHeroDetailComponentの両方のテンプレート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">

これですべてが結ばれたので、ブラウザは次のようなものを表示するはずです:

Single FormControl

FormGroupを追加する

通常、複数のFormControlがある場合、それらを親FormGroup内に登録したいと思うでしょう。これは簡単です。FormGroupを追加するには、hero-detail.component.tsimportsセクションに追加します。

src/app/hero-detail.component.ts

import { Component }              from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

このクラスでは、次のように、FormControlheroFormという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としてレンダリングします。

JSON output

初期名プロパティの値は空の文字列です。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という名前を必須にするには、FormGroupnameプロパティを配列に置き換えます。 最初の項目は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>

ブラウザでは次のように表示されます。

Single FormControl

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プロパティを宣言し、次のようにいくつかのアドレスFormControlsheroFormに追加します。

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コントロールをコンポーネントクラスのAngular FormGroupおよび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では、アドレス関連のFormControldivにラップします。divformGroupNameディレクティブを追加し、それを"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というネストされたアドレスを持つ修正されたフォームモデルが表示されます。

JSON output

すばらしいです!あなたはグループを作ったので、テンプレートとフォームモデルがコミュニケーションしていることがわかります。

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のバリデーション状態。可能な値は、VALIDINVALIDPENDING、またはDISABLEDです。
myControl.pristine ユーザーがUIの値を変更していない場合はtrue。その反対はmyControl.dirtyです。
myControl.untouched ユーザがまだHTMLコントロールにフォーカス当てておらず、blur イベントがトリガされるまでtrueになります。その反対はmyControl.touchedです。

AbstractControl APIリファレンスの他のFormControlプロパティについて学んでください。

FormControlプロパティを確認する一般的な理由の1つは、ユーザーが有効な値を入力したことを確認することです。Angular のフォームバリデーション詳細については、フォームバリデーションガイドを参照してください。

data modelform model

現時点では、フォームは空の値を表示しています。 HeroDetailComponentはヒーローの値を表示する必要があります。ヒーローは、おそらくリモートサーバーから取得したヒーローです。

このアプリでは、HeroDetailComponentHeroListComponentの親からヒーローを取得します

サーバーのヒーローはデータモデルです。 FormControl構造体はフォームモデルです。

コンポーネントは、データモデルのヒーロー値をフォームモデルにコピーする必要があります。 重要な意味合いは2つあります。

  1. 開発者は、データモデルのプロパティをフォームモデルのプロパティにマップする方法を理解する必要があります。
  2. ユーザーの変更は、DOM要素からデータモデルにではなくフォームモデルに流れます。 フォームコントロールはデータモデルを更新しません。

フォームとデータモデルの構造は正確に一致する必要はありません。 特定の画面にデータモデルのサブセットを提示することがよくあります。 しかし、フォームモデルの形状がデータモデルの形状に近い場合は、作業が簡単になります。

このHeroDetailComponentでは、2つのモデルが非常に近いです。

data-model.tsHeroの定義を思い出してください:

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';

setValuepatchValueを使用してフォームモデルを作成する

以前はコントロールを作成し、同時にその値を初期化しました。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を呼び出す必要があります。これは、次の手順で示すように*hero*Inputプロパティが変更されたときに呼び出されます。

まず、hero-detail.component.tsOnChangesInputシンボルをインポートします。

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()
  });
}

HeroListComponentHeroService を作成する

HeroDetailComponentは、マスター/詳細ビューのHeroListComponentのネストされたサブコンポーネントです。これらを組み合わせると下記のように見えます:

HeroListComponent

HeroListComponentはinjectされたHeroServiceを使用してサーバーからヒーローを取り出し、そのヒーローを一連のボタンとしてユーザーに提示します。 HeroServiceはHTTPサービスをエミュレートします。ネットワークレイテンシをシミュレートし、視覚的にアプリケーションの必然的に非同期性を示すために、短い遅延の後に解決するヒーローObservableを返します。

ユーザーがヒーローをクリックすると、コンポーネントはHeroDetailComponentのヒーロー入力プロパティにバインドされているselectedHeroプロパティを設定します。 HeroDetailComponentは変更されたヒーローを検出し、そのヒーローのデータ値でフォームを再設定します。

「Refresh」ボタンはヒーローリストと現在選択されているヒーローをクリアしてから、ヒーローをリフレッシュします。

残りのHeroListComponentHeroServiceの実装の詳細は、リアクティブフォームの理解には関係ありません。関連するテクニックは、ここここのHeroツアーを含むドキュメンテーションの別の場所でカバーされています。

このリアクティブフォームチュートリアルの手順に従ってコーディングする場合は、以下に示すソースコードに基づいて適切なファイルを作成します。hero-list.component.tsObservablefinallyをインポートし、hero.service.tsrxjsからObservableof、およびDelayをインポートする間にインポートされます。その後、ここに戻り、フォーム配列のプロパティについて学びます。

FormArrayを使用してFormGroupの配列を表示する

これまで、FormControlsFormGroupを見てきました。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を操作するには、次の手順を実行します。

  1. 配列に項目(FormControlsまたはFormGroups)を定義します。
  2. データモデル内のデータから作成された項目を使用して配列を初期化します。
  3. ユーザーが必要とするアイテムを追加して削除します。

このガイドでは、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 を初期化する

デフォルトのフォームでは、アドレスのない名前のないヒーローが表示されます。

HeroListComponentHeroListComponent.hero入力プロパティを新しいHeroに設定するたびに、secretLairsに実際のヒーローアドレスを設定する(または再投入する)メソッドが必要です。

次のsetAddressesメソッドは、secretLairs FormArrayheroの住所が格納された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);
}

以前のFormArraysetValueではなく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つの重要なポイントがあります:

  1. 別の折り返し<div>*ngFor<div>の周りに追加し、そのformArrayNameディレクティブを "secretLairs" に設定します。この手順は、内側の繰り返しHTMLテンプレートのフォームコントロールのコンテキストとしてsecretLairs FormArrayを確立します。
  2. 繰り返されるアイテムのソースは、FormArray自体ではなく、FormArray.controlsです。各コントロールはFormGroupというアドレスです。これは以前の(今度は繰り返された)テンプレートのHTMLとまったく同じです。
  3. 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」にはアドレスはありません。

JSON output of addresses array

[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はユーザーの入力をキャプチャしますが、何もしません。 実際のアプリでは、おそらくそれらのヒーローの変更を保存します。 実際のアプリでは、保存していない変更を元に戻して編集を再開することもできます。 このセクションで両方の機能を実装すると、フォームは次のようになります。

Form with save & revert buttons

保存

このサンプルアプリケーションでは、ユーザーがフォームを送信すると、HeroDetailComponentはヒーローデータモデルのインスタンスを、injectされたHeroServicesaveメソッドに渡します。

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.secretLairsaveHero.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> &nbsp;
    <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プロパティの検査。
  • patchValuesetValueでデータを設定する。
  • FormArrayで動的にグループを追加する。
  • FormControlの値に対する変更を観察します。
  • フォームの変更の保存します。

最終バージョンのファイルは次のとおりです。

(省略)

このガイドのすべての手順の完全なソースは、Reactive Forms Demoダウンロードサンプルのライブサンプルからダウンロードできます。

32
35
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
35