Angular の FormArray で項目数が動的に増える入力フォームを実現する

Angular 4 以降の ReactiveForms で検証。

例えばユーザ情報を登録するフォームがあったとして、電話番号と種別 (固定電話なのか携帯なのかなど) を複数入力できるようにしたいとする。

画面のイメージとしては、電話番号テキストボックスと、種別テキストボックスが1行に並んでいて、「追加」ボタンを押すとその行が増やせる、みたいな作りだ。

この場合、動的にフォーム入力できる項目を増やしたりすることになるが、どうやって実装したら良いのか。調べてみると FormArray という仕組みで Angular が提供してくれていた。この使い方を紹介する。

目次

フォームの生成と動的追加

まずはコンポーネント TypeScript を以下のように実装する。

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

/**
 * ユーザ情報登録画面
 */
@Component({
  selector: 'app-user-register',
  templateUrl: './user-register.component.html',
  styleUrls: ['./user-register.component.scss']
})
export class UserRegisterComponent implements OnInit {
  /** ユーザ情報登録フォーム */
  public userForm: FormGroup;
  
  /**
   * コンストラクタ
   * 
   * @param formBuilder FormBuilder
   */
  constructor(private formBuilder: FormBuilder) { }
  
  /**
   * フォームを用意する
   */
  ngOnInit() {
    this.userForm = this.userFormBuilder.group({
      // 名前
      name: [''],
      // 電話番号 : 動的に追加できる
      phoneNumbers: this.userFormBuilder.array([])
    });
  }
  
  /**
   * formArrayName を取得するために必要な Getter
   * 
   * @return 電話番号の FormArray
   */
  get phoneNumbers(): FormArray {
    return this.userForm.get('phoneNumbers') as FormArray;
  }
  
  /**
   * 電話番号入力フォームを1行追加する
   */
  addPhoneNumber() {
    // Getter を用意したいので「this.phoneNumbers」でアクセス可能
    this.phoneNumbers.push(this.userFormBuilder.group({
      // 電話番号
      phoneNumber: '',
      // 種別 : 「固定電話」「携帯」などの情報
      type: ''
    });
}

次に HTML はこんな感じ。

<form [formGroup]="userForm" novalidate>
  <!-- 通常の ReactiveForms どおり、userForm.name をバインディングしている -->
  <p>名前:<input type="text" formControlName="name"></p>
  <!-- Getter を作成したので、TypeScript 側の this.phoneNumbers (FormArray) にアクセスできる -->
  <div formArrayName="phoneNumbers">
    <ul>
      <li *ngFor="let phoneNumber of phoneNumbers.controls; let i = index" [formGroupName]="i">
        <!-- 擬似的に表現すると、this.phoneNumbers[0].phoneNumber とのバインディングができている状態 -->
        電話番号:<input type="text" formControlName="phoneNumber">
        種別    :<input type="text" formControlName="type">
      </li>
    </ul>
  </div>
  <!-- 電話番号入力フォームを1行追加する -->
  <p><button type="button" (click)="addPhoneNumber()">追加</button></p>
</form>

コレで、動的に追加できるフォームを作成できた。

FormArray をまるごと変更する : FormGroup#setControl()

例えば編集画面において、取得したデータを基に編集フォームを生成するような場合。既に生成してある FormArray の中身を全量入れ替えるには、FormGroup#setControl() を使う。

// フォームに初期値として埋め込むデータ。実際は非同期通信で取得するようなテイ
const dataList = [
  { phoneNumber: '090-0000-0000', type: '携帯' },
  { phoneNumber:  '03-0000-0000', type: '自宅' }
];

// データを基に FormGroup の配列を作る
const phoneNumberFormGroups = dataList.map((data) => {
  return this.userFormBuilder.group(data);
});

// FormGroup の配列を取り込んで FormArray を作る
const phoneNumbersFormArray = this.userFormBuilder.array(phoneNumberFormGroups);

// 元々のフォームに適用する
this.userForm.setControl('phoneNumbers', phoneNumbersFormArray');

this.userForm に何か代入するワケではないので注意。

FormArray の途中にデータを入れる : FormArray#insert()

さきほどは FormArray#push() で末尾にデータを追加したが、配列の途中に挿入したい場合は、FormArray#insert() が使える。

// 「(this.userForm.get('phoneNumbers') as FormArray)」部分は「this.phoneNumbers」と書いても良い (Getter メソッドがあるので)
// insert() の第1引数が、第2引数の FormGroup を挿入したい配列の添字。この場合、添字 2、つまり3つ目の要素として挿入できる
(this.userForm.get('phoneNumbers') as FormArray).insert(2, this.userFormBuilder.group({
  phoneNumber: '090-0000-0000',
  type: '携帯'
});

FormArray のデータを参照する : FormArray.valueFormArray.controls

FormArray のデータを参照するには、FormArray.valueFormArray.controls が使える。FormArray.value は単純にオブジェクトの配列になっている。FormArray.controls の方は、FormGroup の配列になっているので、掘り下げていけば各行のデータの操作もできる。

// フォーム配列の中身を参照する
(this.userForm.get('phoneNumbers') as FormArray).value;

// フォーム配列1件目のデータを参照する
(this.userForm.get('phoneNumbers') as FormArray).controls[0].value;

// フォーム配列1件目のデータのうち phoneNumber 項目のデータを参照する
(this.userForm.get('phoneNumbers') as FormArray).controls[0].get('phoneNumber').value;

FormArray 内のデータを編集する

先程の例で get('phoneNumber') と出てきたので分かるかもしれないが、FormArray の中身は userForm 同様、ただの FormGroup なので、setValue() などで編集できる。

// フォーム配列1件目のデータのうち phoneNumber 項目のデータを編集する
(this.userForm.get('phoneNumbers') as FormArray).controls[0].get('phoneNumber').setValue('03-9999-9999');

以上

このように、フォーム1行を FomArray として定義し、1行の要素は FormGroup で作成していくことで、動的に行追加できるフォームを作成できた。