📘

Angular の FormArray の使い方

2022/04/03に公開

Angular の FormArray の使い方

Angular の ReactiveForm には、FormArray という、配列形式でフォームを取り扱うための仕組みが用意されています。

FormArrayを利用することで、ユーザに動的にフォーム項目を追加させる必要がある場面などに対応できますが、
その使い方は若干ややこしさをはらんでいます。

https://angular.jp/guide/reactive-forms#動的フォームの作成-

FormArray の宣言

FromArray は new FormArrayの形式で宣言する事ができます。
引数には配列で、初期値のフォーム要素を指定できます

例によって、FormArray へのアクセサとしての getter 定義も忘れず行っておきましょう。

@Component({
  // ...  
})
export class AppComponent {
  name = 'Angular ' + VERSION.major;

  form = new FormGroup({
    name: new FormControl('', Validators.required),
    tel: new FormControl('', Validators.required),
    addressList: new FormArray([
      new FormGroup({
        zip: new FormControl('', Validators.required),
        address: new FormControl('', Validators.required),
      })
    ]),
  });

  get addressList(): FormArray {
    return this.form.get('addressList') as FormArray;
  }
}

コンストラクタ引数は AbstractForm[] となっており、FormControl だけでなく、上記のように FormGroup を要素としてもたせることができます。

コンストラクタの初期値として上記のようにフォーム要素を指定する他、単純に new FormArray([]) として空の配列で宣言することも可能です。

画面への描画

FormArray には、FormGroup 同様に controls が生えているため、
これにアクセスして FormArray の配列要素の数だけ ngFor で UI を複製することが可能です。

が、 controls の型が AbstractControl[] となっており、そのまま参照すると型エラーを起こす場面も多いため、以下のように getter を定義して型定義を行っておくと良いでしょう。

@Component({
  // ...  
})
export class AppComponent {
  form = new FormGroup({
    name: new FormControl('', Validators.required),
    tel: new FormControl('', Validators.required),
    addressList: new FormArray([]),
  });
  // ....
  /**
   * this.addressList.controls は AbstractControl[] なので、
   * そのまま template で利用すると input で FormGroup に渡すときに 型エラーになります。
   */
  get addressListControls(): FormGroup[] {
    return this.addressList.controls as unknown as FormGroup[];
  }
}

ngFor で Form UI のループを記載する場合には、コンポーネント分割を行っておくと良いでしょう。
formGroup の受け渡しにうっかり [FormGroup][FormControl] などの input 名を用いると、ReactiveFormsModule が提供するディレクティブ名とバッティングするため、my などの prefix をつけてわかりやすくしておくと良いでしょう。

    <app-address
      *ngFor="let address of addressListControls; index as i"
      [myFormGroup]="address"
      (remove)="removeAddress(i)"
    ></app-address>

子コンポーネント(app-address)に FormGroup を渡しておくと、子コンポーネント内で入力された値はそのまま親の form 変数で取得できます。
子コンポーネントは自分で自分自身を削除することはできないため、remove イベントのみ output として定義しておくと良いでしょう。

親要素での remove イベントのハンドラは以下のような形で実装できます。

    removeAddress(i) {
        this.addressList.removeAt(i);
    }

フォーム要素の追加

FormArray への要素追加は、push 関数で行います。

以下のような関数を定義し (click)="addAddress()" を宣言すれば、ボタンをクリックしてフォームの要素が増えていく様を確認できるはずです。

  addAddress() {
    this.addressList.push(
      new FormGroup({
          ...
      })
    );
  }

フォームへの値セット

フォームへの値セットは通常通り、setValue で行うことができます。

setValue は 親の FormGroup から呼ぶこともできますし、FormArrayから直接コールすることも可能です。

this.form.setValue({
    name: 'KATO TOMOHARU',
    tel: '050-0000-0000',
    addressList: [
        { zip: '100-0000', address: '東京都千代田区 加藤コーポレーション' },
    ],
});

FormArray 要素への setValue で注意が必要なのは、「内部で用意されている配列の要素数より、setValue で渡された 配列の要素数が少ない場合にエラーが発生する」ということです。

patchValue を利用してこのエラーを回避することもできるようですが、例えば 3つの配列準備がある FormArray に length 1 の配列を突っ込んでも、FormArray の要素数は 3 のままで、適用した値に応じて FormArray の要素数が縮小することはありません。

多くのケースでは、以下のような関数を作成して、引数で指定した個数で FormArray の length を調整する関数を作成することになるでしょう

  private resetAddressForm(count: number) {
    // ループの中で this.addressListControls.length が変化するので、
    // 変数に一旦保持しないと挙動がおかしくなる
    let length = this.addressListControls.length;
    for (let index = 0; index < length; index++) {
      this.addressList.removeAt(0);
    }
    for (let index = 0; index < count; index++) {
      this.addressList.push(
        new FormGroup({
          zip: new FormControl('', Validators.required),
          address: new FormControl('', Validators.required),
        })
      );
    }
  }

参考

サンプルコード(だいぶなぐり書きです。)

https://stackblitz.com/edit/angular-ivy-daekw8?file=src%2Fapp%2Fapp.component.ts

Discussion