4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Reactive Forms] FormArrayの実装を子コンポーネントに切り出す

Last updated at Posted at 2020-03-15

前回やったこと

前回は下記の記事で、動的に追加&削除できるフォームを単一のコンポーネントの中で実装しました。

前回の記事
[Reactive Forms] FormArrayで動的に追加削除できる入力フォームを実装する

フォームを実装する際、複数の入力欄が必要な場合がほとんどで単一のコンポーネントに詰め込むと可読性が下がってしまいます。今回はFormArrayの実装部分を子コンポーネントに切り出していきたいと思います。

悩んだところ

こちらが前回のテンプレートです。

app.component.html
<h3>フォーム</h3>

<form [formGroup]="form">
  <div>名前: <input type="text" formControlName="userName"></div>

  <div>
    <p>スキル情報</p>
    <div formArrayName="userSkills">
      <div *ngFor="let skill of userSkills.controls; let i = index">
        <div [formGroupName]="i">
          <div>スキル名: <input type="text" formControlName="skillName"></div>
          <div>レベル: <input type="number" formControlName="skillLevel"></div>
          <button (click)="removeUserSkill(i)">削除</button>
        </div>
      </div>
    </div>
    <button (click)="addUserSkills()">スキルを追加</button>
  </div>
</form>

私はどの階層の部分から切り出してあげると良いのか?で少し悩みました。
一旦の結論としてはFormGroupを子コンポーネントに渡して、FormArrayの部分を丸々切り出すと上手く実装できるのでは?という結論に至りました。

サンプルソース

ここではSkillFormというコンポーネントを新たに作成し、こちらに処理を切り出していきます。

子コンポーネントの実装

結論から書くと以下のようなコンポーネントの実装となります。

skill-form-component.ts
import {Component, Input, OnInit} from '@angular/core';
import {FormArray, FormBuilder, FormGroup} from '@angular/forms';

@Component({
  selector: 'app-skill-form',
  templateUrl: './skill-form.component.html',
  styleUrls: ['./skill-form.component.scss']
})
export class SkillFormComponent implements OnInit {
  @Input() parentForm: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
  }

  get userSkillForm(): FormGroup {
    return this.fb.group({
      skillName: [''],
      skillLevel: [''],
    });
  }

  get userSkills(): FormArray {
    return this.parentForm.get('userSkills') as FormArray;
  }

  addUserSkills() {
    this.userSkills.push(this.userSkillForm);
  }

  removeUserSkill(index: number) {
    this.userSkills.removeAt(index);
  }
}

ほぼそのまま移植していますが、ポイントは@Input() parentForm: FormGroup;で親コンポーネントのFormGroupとバインディングしている点です。

子テンプレートの実装

skill-form-component.html
<ng-container [formGroup]="parentForm">
  <div>
    <p>スキル情報</p>
    <div formArrayName="userSkills">
      <div *ngFor="let skill of userSkills.controls; let i = index">
        <div [formGroupName]="i">
          <div>スキル名: <input type="text" formControlName="skillName"></div>
          <div>レベル: <input type="number" formControlName="skillLevel"></div>
          <button (click)="removeUserSkill(i)">削除</button>
        </div>
      </div>
    </div>
    <button (click)="addUserSkills()">スキルを追加</button>
  </div>
</ng-container>

FormArrayの記述の前に、親コンポーネントと同期したFormGroupを定義してあげることで、期待通りの挙動になります。

親テンプレートの実装

app.component.html
  <form [formGroup]="form">
    <div>名前: <input type="text" formControlName="userName"></div>

    <app-skill-form [parentForm]="form"></app-skill-form>
    
  </form>

親テンプレートでは呼び出す時に、子コンポーネントのFormGroupに自身のFormGroupを渡してあげます。

結果として以下のように、子コンポーネントでのFormの変更を親コンポーネントが取り込めていることがわかります。

スクリーンショット 2020-03-15 14.13.59.png

FormGroupではなく、FormArrayをバインディングできないのか?

できるのかもしれませんが、公式にも記述が見当たらなかったため把握できていません。
そもそもFormArrayは[FormGroup]が定義された中で、FormArrayNameを宣言して利用するためFormGroupがいることが前提なのかなと推測しました。

まとめ

FormArrayに限らずですが、子コンポーネントにリアクティブフォームの中身を切り出すときには、親のFormGroupをバインディングしてあげると上手く実装できそうです。

親コンポーネントでは最終的にサブミットされるデータの扱いに集中できるため、責務を分離した実装ができます。

間違った点がありましたら、ご指摘お願いします。

参考文献

4
2
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
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?