Angular(v8), ReactiveForm, boolean以外を使うときのチェックボックスフォームの構築の仕方


## はじめに
ReactiveFormのチェックボックスでboolean以外のvalueを扱うときのフォームの構築の仕方をまとめました。
ReactiveForm自体については説明しないので詳しいところは[公式ドキュメント](https://angular.jp/guide/reactive-forms)をご覧になってください

## こんな課題があったとする

- ReactiveFormでチェックボックスを使いたいけど値はbooleanを扱いたくない。

- フォームの見た目は以下の画像のようだけど、使用者によって入力されたフォームの値を扱うときは**イヌ → Dog, ネコ → Cat, ネズミ → Rat**のように各チェックボックスに紐づいた別の値を使用したい。
<img width="228" alt="スクリーンショット 2019-10-05 16.28.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266304/c1b1edc6-2fb5-69ee-6d31-c2b6c02a7c8c.png">

この場合どうするか。

## 解決策は2つ

**①** フォームモデルをテンプレートファイルにバインドせずに[changeイベント](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event)を利用してフォームモデルを更新させる。

**②** フォームモデルをテンプレートファイルにバインドさせて、フォームの値を取得するときにbooleanから欲しい値に変換させる。


**①**と**②**実際のコードを下に載せたいと思います。

## ①のコード　

```(form1.component.ts)
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, FormArray } from '@angular/forms';

interface Option {
  label: string;
  value: string;
}

@Component({
  selector: 'app-form１',
  templateUrl: './form１.component.html'
})
export class Form１Component {
  readonly animalsOption: Option[] = [
    { label: 'イヌ', value: 'Dog' },
    { label: 'ネコ', value: 'Cat' },
    { label: 'ネズミ', value: 'Rabbit' }
  ];
  ngForm: FormGroup = this.fb.group({
    animals: this.fb.array([])
  });
  showValue: string[] = [''];

  get animals(): FormArray {
    return this.ngForm.get('animals') as FormArray;
  }

  constructor(private fb: FormBuilder) {}

  onSubmit() {
    console.log(this.ngForm);
    this.showValue = this.animals.value;
  }

  onChange(value: string) {
    const array = this.animals.getRawValue();
    array.includes(value)
      ? this.animals.removeAt(array.indexOf(value))
      : this.animals.push(this.fb.control(value));
  }
}
```

```(form1.html)
<form [formGroup]="ngForm" (ngSubmit)="onSubmit()">
  <div>
    <label *ngFor="let option of animalsOption">
      <input type="checkbox" (change)="onChange(option.value)" />
      {{ option.label }}
    </label>
  </div>
  <button class="button" type="submit">
    Show Value!
  </button>
  <h4>form１ value -> {{ showValue }}</h4>
</form>
```

フォームモデルをテンプレートファイルにバインドさせていないので、
[FormArray](https://angular.io/api/forms/FormArray)の[removeAt()](https://angular.io/api/forms/FormArray#removeAt)メソッドと[push()](https://angular.io/api/forms/FormArray#push)メソッドを使用し、チェックボックスの状態に応じてFormControlを追加、削除を動的に行なっています。



## ②のコード　
```(form2.component)
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, FormArray } from '@angular/forms';

interface Option {
  label: string;
  value: string;
}

@Component({
  selector: 'app-form２',
  templateUrl: './form２.component.html'
})
export class Form２Component {
  readonly animalsOption: Option[] = [
    { label: 'イヌ', value: 'Dog' },
    { label: 'ネコ', value: 'Cat' },
    { label: 'ネズミ', value: 'Rabbit' }
  ];
  ngForm: FormGroup = this.fb.group({
    animals: this.fb.array(this.animalsOption.map(() => this.fb.control(false)))
  });
  showValue: string[] = [''];

  get animals(): FormArray {
    return this.ngForm.get('animals') as FormArray;
  }

  constructor(private fb: FormBuilder) {}

  onSubmit() {
    console.log(this.ngForm);
    this.showValue = this.convertFormValue(this.animals, this.animalsOption);
  }

  private convertFormValue(formArray: FormArray, option: Option[]): string[] {
    const values: boolean[] = formArray.value;
    return values
      .map((val, index) => {
        return val ? option[index].value : undefined;
      })
      .filter(v => v);
  }
}

```


```(form2.html)
<form [formGroup]="ngForm" (ngSubmit)="onSubmit()">
  <div>
    <span formArrayName="animals">
      <label *ngFor="let _ of ngForm.controls.animals.controls; let i = index">
        <input type="checkbox" [formControlName]="i" />
        {{ animalsOption[i].label }}
      </label>
    </span>
  </div>
  <button class="button" type="submit">
    Show Value!
  </button>
  <h4>form２ value -> {{ showValue }}</h4>
</form>
```

[FormArray](https://angular.io/api/forms/FormArray)の値(boolean[])と定義した変換させたい配列(animalsOption)を参照させて欲しい値に変換させています。


### それぞれの所感
- **①**はテンプレートファイルとフォームモデルが疎結合な状態であるので、チェック操作以外でフォームの値を変更する場合(例えばフォームのリセット操作([reset())](https://angular.jp/api/forms/FormArray#reset)につらい。 FormArrayで定義されているメソッドを使っているので可読性が高いと感じた。

- **②**はテンプレートファイルとフォームモデルが密結合な状態であるので、**①**のようなつらさが出ることはない。ただ、FormArrayの値を変換するところが若干、書いてて気持ちが悪い。 (改善案求めます)


## おわりに

コードベタ貼りだとわかりづらいと思うので
[stackblitz](https://stackblitz.com/edit/angular-s35zuc) に上記のフォームを実装したので詳しく動作確認してください。