我が名はなんとか菜である!

主に技術系の記事を書きますが、ポエムも混入します。

【Angular4】FormGroupに入れ子になったFormArrayをComponentの親子で共有する

Angular4でも2でも動くと思うけど4って書いたほうがかっこいいかなと思って。

Angular4でComponentを書いてるとしばしば次のような問題にブチ当たります。

Componentが無限にデカくなる

最初から適切に画面の分割ができていればいいんですが、実際のプロダクトだと設計書の段階ですでに闇鍋だったりして、頑張ってServiceにロジックを詰め込んでも、一つのcomponent.tsがUI操作だけで2000行とかになったりして無事に死にます。
これに対する応急措置として、親子関係を作ってComponentを分割するというのがありますが、FormGroupを使って入力値の管理をしている場合は一筋縄では行きません。(僕は行きませんでした)
特に、親元のFormGroupの中にFormArrayが入っていてるときに、その部分を分割しようとすると、子Componentから直接親のFormGroupが見えないため、Cannot read property 'getFormArray' of nullようなエラーが出て上手く動きません。
公式ドキュメントもイマイチ参考にならなかったので色々試行錯誤したところ、ようやくうまくいく方法が見つかったので、覚え書きしておきます。

parent.component.ts

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

@Component({
    selector: 'app-parent',
    template: `
<div>
    <form [formGroup]="rootFormGroup" (ngSubmit)="onSubmit()">
        Title:<input formControlName="title"/>
        <app-child [parentForm]="rootFormGroup"></app-child>
        <button type="submit">submit</button>
    </form>
    <pre>{{dump}}</pre>
</div>
  `
})
export class ParentComponent {
    rootFormGroup: FormGroup;
    dump = '';
    constructor(private fb: FormBuilder) {
        this.rootFormGroup = fb.group({
            title: [''],
            addrFormArray: fb.array([
                fb.group({
                    name: [''],
                    address: [''],
                })
            ])
        });
    }
    public onSubmit() {
        this.dump = JSON.stringify(this.rootFormGroup.value, null, '\t');
    }
}

child.component.ts

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

@Component({
  selector: 'app-child',
  template: `
<div>
    <form [formGroup]="parentForm">
        <div formArrayName="addrFormArray">
            <button (click)="add()">+Add</button>
            <ng-container *ngFor="let control of addrFormArray.controls;let i=index;">
                <div formGroupName="{{i}}">
                    Name:<input formControlName="name"/>
                    Address:<input formControlName="address"/>
                </div>
            </ng-container>
        </div>
    </form>
</div>
  `
})
export class ChildComponent {
  @Input() parentForm: FormGroup;

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

  constructor(private fb: FormBuilder) {
  }

  public add() {
    this.addrFormArray.push(
      this.fb.group({
        name: [''],
        address: ['']
      })
    );
  }
}

結果

f:id:happo31:20170616003249p:plain
結論から言えば「@Input()で親のFormGroupを子に渡す」です。
最初はFormArrayを直接渡していたりしたんですが、そうすると値の更新が上手く行きませんでした。
推測ですが、FormArrayは単体で使うのではなくて、あくまでFormGroupに内包されているのが前提という仕様なのではないか、と思っています。

動かせるやつ

github.com

もっと効率のいい方法はありそうですが、取り敢えず動いたのでこれで。