Help us understand the problem. What is going on with this article?

Angular2のFormについて(その1)

More than 3 years have passed since last update.

概要

Angular2とionic2でformを使った際に,バリデーションが少し面倒だったので,Angular2のformについて調べてみました.この記事は The Ultimate Guide to Forms in Angular2 を元にしています.元の記事は少々長いため,以下のように二部に分けて記事にします.うまく訳せていないところもあるので,元の記事はご自身で確認いただければと思います.
なおionic2に関しては第2部の最後に,フォームのサンプルを書いてみたいと思います.

  • 第1部

    • FormControl
    • FormGroup
    • FormBuilder
    • バリデーション
  • 第2部

    • カスタムバリデーション
    • 変更をWatchする
    • ngModelによる双方向データバインディング
    • ionic2に組み込んでみる

第1部ではAngular2が用意するツールを用いてバリデーションを実装するまでを見ていきたいと思います.
フォームを使うとユーザが入力したデータをサーバに送ることが出来ます.しかしユーザは自由に入力できるので,データの正否を評価する必要がありますよね.そしてユーザに対しては,入力した値が期待通りのものなのか表示してあげるのが親切です.また,依存関係のあるフォームを作るには複雑なロジックが必要でとても手間です.
ありがたいことにAngualr2ではこれらをやってくれるツールが用意されています.

FormControl

FormControlはひとつの入力フィールドに相当するものでAngular formの最も小さいユニットです.FormControlは入力フィールドの値をカプセル化し,その値がvalid(正しい)なのか,dirty(変更された)のか,あるいはエラーがあるのか示してくれます.

// 新しいFormControl を用意し "Nate" という値を入れます
let nameControl = new FormControl("Nate");
let name = nameControl.value; // -> Nate

// nameControlに対して以下のように状態を問い合わせることが出来ます
nameControl.errors // -> StringMap<string, any> of errors
nameControl.dirty  // -> false
nameControl.valid  // -> true

formを作る際,私たちはまずFormControl(もしくはFormControlの集まり)を作り,その上でメタデータやロジックを追加していきます.
他のAngularで行われていることと同じように,ここではFormControlというクラスがあり,DOMに追加することが出来ます.以下のようにすると新しいFormControlオブジェクトをformの中に作ることが出来ます.

<!-- 大きなフォームの中の一部分のinput -->
<input type="text" [formControl]="name" />

FormGroup

多くの場合formはひとつだけのフィールドということはなく,複数のフィールドがあり,それらのFormControlをうまく管理する必要があります.しかしフォームの値が正しいかチェックするために,FormControlの配列を繰り返しチェックするのは手間ですよね.FormGroupは,そうした面倒なことを解決してくれます.

let personInfo = new FormGroup({
    firstName: new FormControl("Angular"),
    lastName: new FormControl("Ionic"),
    zip: new FormControl("222-2222")
})

FormGroupとFormControlは共通の祖先(AbstractControl)を持っています.どういうことかというと,上記の例で言うと単体のFormControlと同じように,簡単にpersonalInfoというFormGroupの状態や値をチェックすることが出来るのです.

personInfo.value;
// -> {
//   firstName: "Nate",
//   lastName: "Murray",
//   zip: "90210"
// }

// FormControlの子要素の値に依存した実際の値について,
// コントロールグループに対して問い合わせることが出来ます.
personInfo.errors // -> StringMap<string, any> of errors
personInfo.dirty  // -> false
personInfo.valid  // -> true
// etc.

FormGroupから値を取ろうとするとキーと値がペアになったオブジェクトを受け取っていますよね.これは個別のFormControlから繰り返し値を取らずに,すべての情報を取れるのでとても便利です.

formを作ってみる

はじめの一歩としてフォームを作ってみましょう.まずはひとつのinputとsubmitボタンを使った最もシンプルなフォームです.
formのComponentについて説明すると,componentを定義するのには3つのステップがありましたね.

  • @Component()アノテーションを設置する
  • テンプレートを作る
  • コンポーネントで宣言したクラスにカスタムファンクションを埋め込む

これらを順に見ていきましょう.

FormsModuleの読み込み

新しいフォームライブラリを使うためには,まずNgModuleにフォームのライブラリをインポートする必要があります.Angularでフォームを使うには,FormsModuleを用いる方法と,ReactiveFormsModuleを用いる二通りの方法があります.ここでは両方共説明するため,どちらもモジュールにインポートしておきましょう.例えばapp.tsであればこのような感じになります.

import {  
  FormsModule,  
  ReactiveFormsModule  
} from '@angular/forms';

// 下の方に行き,,,
@NgModule({  
  declarations: [  
    FormsDemoApp,  
    DemoFormSku,  
    // 他の宣言  
  ],  
  imports: [  
    BrowserModule,  
    FormsModule,         // <-- 追加
    ReactiveFormsModule  // <-- 追加
  ],  
  bootstrap: [ FormsDemoApp ]  
})  
class FormsDemoAppModule {}

こうすることで,ビューの中でフォームディレクティブを使うことが出来るようになります.FormsModuleは以下のようなディテクティブも提供してくれます.

  • ngModel
  • NgForm

一方,ReactiveFormsModuleは以下のディレクティブを提供します.

  • formControl
  • ngFormGroup

本当はこれ以外のもあるのですが,ここではざっくりとFormsModuleとReactiveFormsModuleを自分のNgModuleにインポートするということは,フォームに関連するすべてのディレクティブを使えるようになり,自分のコンポーネントにそれらの各プロバイダを注入することが出来ると理解してください.

簡単なSKU Form: @Component Annotation

まずコンポーネントを用意します.

import { Component } from '@angular/core';

@Component({  
  selector: 'demo-form-sku',

コンポーネントはselectorとtemplateがセットになっており,ここでdemo-form-skuというselectorを定義することで,Angularにこのコンポーネントはなんの要素をバインドするのかを伝えます.これでビューでdemo-form-skuというタグを書くと,templateの中身が表示されるようになります.
次にテンプレートを見てみましょう.SKUを送信する簡単なフォームです.(※SKUとはStock Keeping Unitの略です.ここでは単純に商品数というような理解で構いません.)

  template: `  
  <div class="ui raised segment">  
    <h2 class="ui header">Demo Form: Sku</h2>  
    <form #f="ngForm"  
          (ngSubmit)="onSubmit(f.value)"  
          class="ui form">
      <div class="field">  
        <label for="skuInput">SKU</label>  
        <input type="text"  
               id="skuInput"  
               placeholder="SKU"  
               name="sku" ngModel>  
      </div>
      <button type="submit" class="ui button">Submit</button>  
    </form>  
  </div>  
  `

formとNgForm

少し面白いことに気が付きましたか?ここではすでに,NgFormを使えるようにするためのFormsModuleをインポートしています.ビューの中でディレクティブを使えるようにすると,同じ名前のセレクタとセットで使われるのが普通ですよね.
NgFormは確かに便利ですがちょっと分かりづらいのです.NgFormはformタグ(しかもこのタグは明示的にngFormという属性を追加する必要があります)をセレクタに含んでいます.どういうことかというと,FormsModulesをインポートすると自動的にビューにあるすべての<form>タグにNgFormが追加されるということです.
これはとても便利なのですが,意識せずともそのようになるのでちょっと注意が必要です.
さて,NgFormには以下の2つの重要な機能があります.

  1. ngFormという名前のFormGroup
  2. (ngSubmit)

どちらも<from>タグの中で使われていますね.
まず#f="ngForm"を見てみましょう.#v=hogehogeという表記はこのビューのローカル変数を作ることですね.ngFormを#fという変数にいれています.ngFormが急に出てきていますが,どこから来たのでしょうか?それはNgFromディレクティブからです.ngFormの型は?FormGroup型です.つまり私たちはfをFormGroupとして使うことが出来るということです.そしてこれは(ngSubmit)でやっていることも全く同じです.

鋭い方は,はじめにNgFormは(NgFormのデフォルトセレクタである)<form>タグに自動的に追加されると書いているということは,NgFormを使うのにngForm属性を追加する必要ないんじゃないの?それでもngFormを属性(変数)タグとして追加してるけどこれは間違い?と気づくかもしれないですね.
これは間違いではありません.もしngFormが属性のキーだとしたら,それはその属性でNgFormを使いたいとAngularに伝えてしまう事になります.でも今回は,参照を割り当てているとき,ngFormをその属性として使っています.これは,評価される式の値であるngFormはローカルテンプレートの変数であるfに割り当てられるべきなのです.
ngFormはすでにこのエレメントであり,どこからでも参照できるように,このFormGroupをエクスポートしたと思って構いません.
(斜体のところよく分からず.)

ngSubmitは以下のように追加されています.

(ngSubmit)="onSubmit(f.value)"

(ngSubmit)はNgFormからきています.onSubmit( )は実行されるであろうファンクションです.f.valueのfはFormGroup,.valueはFormGroupのキーと値のペアを返します.

ようするに,formをサブミットしたらコンポーネントのインスタンスにあるonSubmit()を呼んで引数としてformの値を渡す,ということをこの一行でやっています.

inputとNgModel

NgModelについて説明する前に,ここで例示しているinputタグについて簡単に触れておきます.

   <form #f="ngForm"  
          (ngSubmit)="onSubmit(f.value)"  
          class="ui form">

      <div class="field">  
        <label for="skuInput">SKU</label>  
        <input type="text"  
               id="skuInput"  
               placeholder="SKU"  
               name="sku" ngModel>  
      </div>

class = "ui form"とclass = "field"はただ見た目を良くするためのオプションです.CSS framework Semantic UIを利用しています.labelのfor属性とinputのid属性は説明不要でしょう.placeholder属性は,フォームが空のときに表示しユーザにそのフォームに何を入力すべきか表示するためのものですね.

さて,NgModelディテクティブはngModelのセレクタを特定します.これはngModel = "hogehoge"みたいな属性をinputタグに追加できるということです.今回の場合はngModelにはなんの属性値も持たせていません.
このテンプレートの中でngModelを特定する方法はいくつかあり,何も属性値を与えない今回のやり方もそのうちのひとつになります.何も属性値を与えず使うということは,以下の2つのことを意味しています.

1.ワンウェイデータバインディング
2.このフォームに,sku(inputタグのname属性にskuとあるから)という名前でFormControlを作る

NgModelは新しいFormControlを作り,自動的に親のFormGroup(この場合,form)に追加されます.そしてDOMエレメントをその新しいFormControlにバインドします.これは,inputタグとFormControlを名前,この場合skuで連結させるということです.(なのでngModelを加えるときはinputタグの属性値としてnameは必須になります.)

FormBuilder

ngFormとngControlを使ってFormControlとFormGroupを作るのは確かに便利だけど,細かくカスタマイズは出来ません.もっと柔軟かつ一般的な方法でフォームを使うには,FormBuilderを利用します.
FormBuilderとは,フォームを作るのにはぴったりな名前の補助的なクラスですね.すでに説明したようにformはFormControlとFormGroupから構成されており,FormBuilderはそれらを作るのを手助けしてくれます.

先の例にFormBuilderを追加してみましょう.

  • どうやってcomponentクラスの中でFormBuilderを使うのか
  • どうやってformの中でカスタマイズしたFormGroupを使うのか

リアクティブなフォームとFormBuilder

componentでformGroupとformControlディテクティブを使えるようにするには,適切なクラスをインポートする必要があります.

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

@Component({  
  selector: 'demo-form-sku-builder',

FormBuilderを使ってみる

コンストラクタの引数にFormBuilderを入れます.

export class DemoFormSkuBuilder {  
  myForm: FormGroup;

  constructor(fb: FormBuilder) {  
    this.myForm = fb.group({  
      'sku': ['ABC123']  
    });  
  }
  onSubmit(value: string): void {  
    console.log('送信された値:', value);  
  }
}

FormBuilderのインスタンスが作られるので,fbという変数に入れましょう.FormBuilderでは,主に2つのファンクションを使うことになります.

  • control: 新しいFormControlを生成します
  • group: 新しいFormGroupを生成します

さて,myFormという新しいインスタンス変数が用意されていると思います.myFormはFormGroup型で宣言されています.そしてfb.group()でFormGroupを作ることが出来ます.groupはそのグループ内のキーと値がセットになったオブジェクトを持ちます.

ここではskuというひとつのcontrolを作成します.値は["ABC123"]としています.これはデフォルトの値がABC123であるということです.配列になっていることに気づいたと思いますが,後から他のコンフィグレーションを追加していきます.これでビュー側で使うことの出来るmyFormを用意できました.

ビューでmyFormを使う

<form>でmyFormを使えるようにしましょう.先程FormsModuleを使うとngFormは自動的に適応される,ngFormは自身のFormGroupを作る,と説明したと思います.ただこのケースではFormGroupの外側を使いたいわけではありません.しかし,FormBuilderからつくったインスタンス変数であるmyFormを使いたいのですが,どうすればよいのでしょう.

Angularは,FormGroupがあるとき,それを使うためのディテクティブを用意しています.それはformGroupと呼ばれ,以下のように利用します.

    <h2 class="ui header">Demo Form: Sku with Builder</h2>  
    <form [formGroup]="myForm"

こうするとAngularに「このformでmyFormをFormGroupとして使いたい」ということを伝えることが出来ます.
なお最初のほうで,FormsModuleを使うと,NgFormが自動的にすべての<form>の要素に適応されると説明したと思うのですが,例外があり,NgFormはformGroupを持っている<form>には適応されません.
NgFormのセレクタを見てみると以下のようになっているため,ngNoForm属性を使うとNgFormを適応させないformを使うことも出来ます.

form:not([ngNoForm]):not([formGroup]),ngForm,[ngForm]

そして,myFormを使うためにはonSubmitも変更する必要があります.というのもこれまで使っていたfではなく,myFormの要素,値を持っているからです.

あともうひとつ,myFormを使えるようにするために,FormControlをinputタグに埋め込む必要があります.ngControlは新しいFormControlオブジェクトを生成するのは覚えていますか.そして親のFormGroupに追加すると.しかし今回はFormControlを生成するのにFormBuilderを利用します.

すでにあるinputタグにFormControlを加えるにはformControlを利用します.

   <label for="skuInput">SKU</label>  
        <input type="text"  
               id="skuInput"  
               placeholder="SKU"
               [formControl]="myForm.controls['sku']">

このようにすると,formControlディレクティブにmyForm.controlsとskuというFormControlを使ってこのinputタグを監視するよう指示できます.

全部まとめるとこんな感じになります.

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

@Component({  
  selector: 'demo-form-sku-builder',  
  template: `  
  <div class="ui raised segment">  
    <h2 class="ui header">Demo Form: Sku with Builder</h2>  
    <form [formGroup]="myForm"  
          (ngSubmit)="onSubmit(myForm.value)"  
          class="ui form">

      <div class="field">  
        <label for="skuInput">SKU</label>
        <input type="text"  
               id="skuInput"  
               placeholder="SKU"  
               [formControl]="myForm.controls['sku']">  
      </div>

    <button type="submit" class="ui button">Submit</button>  
    </form>
  </div>
  `
})  
export class DemoFormSkuBuilder {  
  myForm: FormGroup;

  constructor(fb: FormBuilder) {  
    this.myForm = fb.group({  
      'sku': ['ABC123']  
    });  
  }

  onSubmit(value: string): void {  
    console.log('you submitted value: ', value);  
  }
}

バリデーション

間違ったフォーマットのデータを入れた際にフィードバックを渡し,送信できないようにするには,validatorsを使います.ValidatorsはValidators moduleから提供され,最もシンプルなvalidatorはValidators.requiredで,指定のフィールドが必須なのか,FormControlによってinvalidと判別されているのかを通知します.

validatorsを使うには,以下の作業が必要になります.

  1. validatorをFormControlに割り当てる
  2. ビューの中でvalidatorのステータスをチェックし,それによってアクションを起こす

まず,FormControlにvalidatorを割り当てるにはFormControlコンストラクタの2つ目の引数にvalidatorを渡します.

let control = new FormControl('sku', Validators.required);

もしくは,FormBuilderを使っているのであれば,以下のようにすることも出来ます.

 constructor(fb: FormBuilder) {  
    this.myForm = fb.group({  
      'sku':  ['', Validators.required]  
    });

    this.sku = this.myForm.controls['sku'];  
  }

次に,ビューの中でvalidationを使う方法を説明します.ビューの中でvalidationの値にアクセスするには2種類の方法があります.

1.明確にFormControl skuをclassのインスタンス変数に割り当てる方法:冗長だがビューの中で簡単にFormControlにアクセスできる

2.ビューの中のFormControl skuをmyFormから調べる方法:この方がコンポーネントの定義クラス内でより簡単にできるのですが,ビューの中では若干ですがより冗長になります.

2つの違いを比べながら確認してみましょう.

sku FormControlを明示的にインスタンス変数として割り当てる方法

ビューの中の各FormControlsを扱うのに最もフレキシブルな方法は,各FormControlをコンポーネント定義クラスの中でインスタンス変数としてセットするものです.
具体的には,以下のようにクラスにskuをセット出来ます.

export class DemoFormWithValidationsExplicit {  
  myForm: FormGroup;  
  sku: AbstractControl;

  constructor(fb: FormBuilder) {  
    this.myForm = fb.group({  
      'sku':  ['', Validators.required]  
    });
    this.sku = this.myForm.controls['sku'];  
  }

  onSubmit(value: string): void {  
    console.log('you submitted value: ', value);  
  }
}

流れは以下のようになります.
1. sku: AbstractControlをクラスの先頭にセットし,
2. FormBuilderとmyFormを作った後,this.skuを割り当てる

これが便利なのは,コンポーネントのビュー内であればどこからでもskuを参照できる点です.一方,面倒なのはformのすべてのフィールドにインスタンス変数を設定しないといけないため,フィールドが多くなるとそれなりに大変ということです.

これでskuに対してバリデーションを割り当てることができるようになりました.ビューの中で使える4つの使い方を見ていきましょう.

  1. 全フォームについてチェックし,メッセージを表示する
  2. 各フィールドについてチェックし,メッセージを表示する
  3. 各フィールドについてチェックし,もしおかしかったらそのフィールドを赤くする
  4. 各フィールドの特定の要求をチェックし,メッセージを表示する

フォーム全体をチェックするにはmyForm.validをみます.

<div *ngIf="!sku.valid"  
           class="ui error message">SKU is invalid</div>

myFormはFormGroupでしたね.そしてFormGroupはすべての子要素のFormControlが正しければ正しくなります.
ある特定のフィールドのFormControlが間違っているときに,メッセージを表示することも出来ます.

<div *ngIf="!sku.valid"
           class="ui error message">SKU is invalid</div>
<div *ngIf="sku.hasError('required')"  
           class="ui error message">SKU is required</div>

フィールドの色を変えるには,セマンティックUI CSSフレームワークのCSSのクラスである.errorを使います.<div class = "field">にerrorクラスを追加すると,インプットタグに赤いボーダーを表示してくれます.どのようにするかというと,条件付きクラスをセットするためにプロバティシンタックスを用います.

<div class="field"  
          [class.error]="!sku.valid && sku.touched">

.errorクラスに条件を2つセットしていますね.これは!sku.validとsku.touchedをチェックしてます.これはエラーを,ユーザーがそのフォームを編集("touched")しようとして,その内容が間違っていたときのみエラーを表示したいためです.

formのフィールドは色々な理由でinvalid(不正)とすることが出来ます.入力された内容によって違うエラーメッセージを表示したいことがあると思います.
特定の入力間違いを検出するにはhasErrorメソッドを利用します.

 <div *ngIf="sku.hasError('required')"  
           class="ui error message">SKU is required</div>

hasErrorはFormControlとFormGroup両方に定義されています.これは,FormGroupの特定のフィールドを見るためにpathの2つ目の引数を渡すことができます.例えば,先程の例を次のように書くことが出来ます.

<div *ngIf="myForm.hasError('required', 'sku')"  
       class="error">SKU is required</div>

コード全体を書くと以下のようになります.

/* tslint:disable:no-string-literal */  
import { Component } from '@angular/core';  
import {  
  FormBuilder,  
  FormGroup,  
  Validators,  
  AbstractControl  
} from '@angular/forms';

@Component({  
  selector: 'demo-form-with-validations-explicit',  
  template: `  
  <div class="ui raised segment">  
    <h2 class="ui header">Demo Form: with validations (explicit)</h2>  
    <form [formGroup]="myForm"  
          (ngSubmit)="onSubmit(myForm.value)"  
          class="ui form">

      <div class="field"  
          [class.error]="!sku.valid && sku.touched">  
        <label for="skuInput">SKU</label>  
        <input type="text"  
               id="skuInput"  
               placeholder="SKU"  
               [formControl]="sku">  
         <div *ngIf="!sku.valid"  
           class="ui error message">SKU is invalid</div>  
         <div *ngIf="sku.hasError('required')"  
           class="ui error message">SKU is required</div>  
      </div>

      <div *ngIf="!myForm.valid"  
        class="ui error message">Form is invalid</div>

      <button type="submit" class="ui button">Submit</button>  
    </form>  
  </div>  
  `
})  
export class DemoFormWithValidationsExplicit {  
  myForm: FormGroup;  
  sku: AbstractControl;

  constructor(fb: FormBuilder) {  
    this.myForm = fb.group({  
      'sku':  ['', Validators.required]  
    });

    this.sku = this.myForm.controls['sku'];  
  }

  onSubmit(value: string): void {  
    console.log('you submitted value: ', value);  
  }
}

skuインスタンスを削除してみる

上記サンプルではsku: AbstractControlとしてインスタンス変数を用意しています.
私たちはインスタンス変数に頼りがちですが,myForm.controlsプロパティを使うことで,インスタンス変数を使わずにFormControlを参照することもできます.

  <input type="text"  
             id="skuInput"  
             placeholder="SKU"  
             [formControl]="myForm.controls['sku']">  
       <div *ngIf="!myForm.controls['sku'].valid"  
         class="ui error message">SKU is invalid</div>  
       <div *ngIf="myForm.controls['sku'].hasError('required')"  
         class="ui error message">SKU is required</div>

こうすると,コンポーネントクラスにインスタンス変数を追加することなく,skuコントロールにアクセスできます.

随分長かったのですが,以上が第1部になります.
簡単にまとめると,Angular2でフォームをコントロールするには,一番小さなモジュールであるFormControl,それらをまとめるためのFormGroup,更にそれらをいい感じに利用できるようにするFormBuilderを理解すれば大丈夫ということになります.
第2部では,バリデーションのカスタマイズ,双方向データバインディングについて見ていきたいと思います.

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away