前回やったこと
前回は下記の記事で、動的に追加&削除できるフォームを単一のコンポーネントの中で実装しました。
前回の記事
[Reactive Forms] FormArrayで動的に追加削除できる入力フォームを実装する
フォームを実装する際、複数の入力欄が必要な場合がほとんどで単一のコンポーネントに詰め込むと可読性が下がってしまいます。今回はFormArrayの実装部分を子コンポーネントに切り出していきたいと思います。
悩んだところ
こちらが前回のテンプレートです。
<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
というコンポーネントを新たに作成し、こちらに処理を切り出していきます。
子コンポーネントの実装
結論から書くと以下のようなコンポーネントの実装となります。
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とバインディングしている点です。
子テンプレートの実装
<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を定義してあげることで、期待通りの挙動になります。
親テンプレートの実装
<form [formGroup]="form">
<div>名前: <input type="text" formControlName="userName"></div>
<app-skill-form [parentForm]="form"></app-skill-form>
</form>
親テンプレートでは呼び出す時に、子コンポーネントのFormGroupに自身のFormGroupを渡してあげます。
結果として以下のように、子コンポーネントでのFormの変更を親コンポーネントが取り込めていることがわかります。
FormGroupではなく、FormArrayをバインディングできないのか?
できるのかもしれませんが、公式にも記述が見当たらなかったため把握できていません。
そもそもFormArrayは[FormGroup]
が定義された中で、FormArrayName
を宣言して利用するためFormGroupがいることが前提なのかなと推測しました。
まとめ
FormArrayに限らずですが、子コンポーネントにリアクティブフォームの中身を切り出すときには、親のFormGroupをバインディングしてあげると上手く実装できそうです。
親コンポーネントでは最終的にサブミットされるデータの扱いに集中できるため、責務を分離した実装ができます。
間違った点がありましたら、ご指摘お願いします。