Angularで動的フォーム（クリックでformを追加 ）をつくる

こんにちは、初めてのオリジナルアプリを開発中のプログラミング初心者です。
今回テーマにしている__動的フォームをつくる__ですが、
理解に不安があったので噛み砕いて理解すべく本稿でまとめていきたいと思います。

本稿はangularMaterialと使ったシンプルな動的フォームの実装とをとその応用フォームの説明をしており対象読者は、プログラミング初心者かつAngular初心者です。

#### 開発環境
- Angular: 9.1.6
- typescript: 3.8.3
- angular/material: 9.2.3

## 基礎的なformの実装

まずシンプルな動的formの実装を見ていきます。

ReactiveFormsModule、MatFormFieldModule、MatInputModule、MatButtonModule
を追加します。

```ts:app.module.ts
import { ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';

@NgModule({
  imports: [ ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule ]
})
```
コンポーネントでは下記の4つを行います。

- formsからインポート
- formの定義、設定
- consuructorの定義
- submit

[validatorsはいろいろなオプションがあります](https://angular.io/api/forms/Validators)
本稿では深掘りしませんが、qiitaにもvalidationに関する記事はたくさんあるので検索してみてください。


```ts:app.component.ts
import { FormBuilder, Validators } from '@angular/forms';

export class AppComponent  {
  form = this.fb.group({
    familyName: ['鈴木', [    // inputフィールドに初期値が必要な場合はここに持たせる、なくてもいい
      Validators.required,   // requiredで必須入力になる
    ]],
    firstName: ['', [
      Validators.required,
    ]],
  });

  constructor(private fb: FormBuilder) {}

  submit() {
    console.log(this.form.value);
  }
}
```
mat-　が付いているタブはAngularMaterialです。

```html:app.component.html
 <form [formGroup]="form" (ngSubmit)=submit()> <!-- [formGroup]="form"でts側で定義してるformと結びつきます。 -->
    <mat-form-field>
      <mat-label>姓</mat-label> <!-- placeholderと同じようですがmat-labelは複雑な設定ができます。 -->
      <input matInput type=text autocomplete="off" formControlName=familyName> 
    </mat-form-field>
    <mat-form-field>
      <mat-label>名</mat-label>
      <input matInput type=text autocomplete="off" formControlName=firstName>
    </mat-form-field>

    <div>
      <button mat-flat-button color="primary" [disabled]="form.invalid" type="submit">送信</button> <!-- [disabled]="form.invalid"で必須入力の項目が抜けていたらクリック出来なくなります。 -->
    </div>
  </form>
```
```scss:app.component.scss
 form {
  padding: 16px;

  mat-form-field {
    display: block;
    width: 200px;
  }
}
```

こんな感じになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490773/f930f1cc-9bca-6574-161f-7e0d361f6586.png)


## formArrayを使って動的なフォームをつくる

前項のformを基に下記の５つを追加します。

- formArrayのインポート
- studentsName
- get studentsName()
- addStudentsName()
- removeStudentsName()

```ts:app.component.ts
import { FormBuilder, Validators } from '@angular/forms';

import { FormArray } from '@angular/forms';

export class AppComponent  {
  form = this.fb.group({
    familyName: ['鈴木', [
      Validators.required,
    ]],
    firstName: ['', [
      Validators.required,
    ]],
    studentsName: this.fb.array([  // groupではなくarrayにすることで単体からの複数の入力フィールドを持つことができます。
      this.fb.control('')     // ページ読み込み時にこの入力フィールドを表示させたくない場合この行を削除してください。
    ])
  });

  constructor(private fb: FormBuilder) {}

  get studentsName(): FormArray {        // このゲッターにより、addStudentsName()などをつくる際this.studentsNameでformのstudentsNameにアクセス出来ます。
    return this.form.get('StudentsName') as FormArray;
  }

  addStudentsName() {          // 入力フィールドを一つ追加します。
    this.studentsName.push(this.fb.control(''));
  }

  removeStudentsName(index: number) {     // 特定のindexを持つ入力フィールドを削除します。
    this.studentsName.removeAt(index);
  }

  submit() {
    console.log(this.form.value);
  }
}
```

HTML内の`<ng-container>`はAngular特有のタグ要素を持たないタグです。*ngForなどの指示だけしたいときに使います。



```html:app.component.html
  <form [formGroup]="form" (ngSubmit)=submit()>
    <mat-form-field>
      <mat-label>姓</mat-label>
      <input matInput type=text autocomplete="off" formControlName=familyName>
    </mat-form-field>
    <mat-form-field>
      <mat-label>名</mat-label>
      <input matInput type=text autocomplete="off" placeholder="名" formControlName=firstName>
    </mat-form-field>

<!-- ここから追加 -->
    <ng-container formArrayName="studentsName"> <!-- ts側のformArrayのstudentsNameと結びつける。 -->
      <button type="button" mat-raised-button color="primary" (click)="addStudentsName()">生徒追加</button> <!-- formタグ内のbuttonタグはデフォルトがtype="submit"なのでtype="button"の指定が必要 -->
      <button type="button" mat-raised-button color="basic" (click)="removeStudentsName()">キャンセル</button> <!-- 同じくtype="button"の指定が必要 -->

      <ng-container *ngFor="let alias of studentsName.controls; let i=index">  <!-- formArrayの中に作られている数だけ表示させます。 -->
        <mat-form-field>
          <mat-label>生徒の名前</mat-label>
          <input matInput type=text autocomplete="off" [formControlName]="i"> <!-- ループで表示されている入力フィールドを個別に認識するために[formControlName]="i"にインデックス番号を割り当てています。 -->
        </mat-form-field>
      </ng-container>

    </ng-container>
<!-- ここまで -->
    <div>
      <button mat-flat-button color="primary" [disabled]="form.invalid" type="submit">送信</button>
    </div>
  </form>
```
こうなります。
![22-49-14.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490773/e667a1ce-cf63-9c7d-c00a-dae391d7d500.gif)

## 動的フォームの応用

最後に僕が開発中のアプリで実装した形に近いformを見ていきます。

まず完成したformがこちらです。

![50-45.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490773/043ba29c-6ac0-61a5-8c16-110dd03d0410.gif)

では作っていきます。

MatMenuModuleを追加

```ts:app.module.ts
import { ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';

import { MatMenuModule } from '@angular/material/menu'; // (質問を選択)ボタンを押したときに出るやつ

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    ReactiveFormsModule,
    MatFormFieldModule,
    MatInputModule,
    MatButtonModule,

    MatMenuModule,
  ],
```

次に下記の6つを追加します。

- answers
- quwstionsList
- selectedQuestion
- get answers()
- addQuestion()
- removeAnawer()

```ts:app.component.ts
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { FormArray } from '@angular/forms';

export class AppComponent {

  form = this.fb.group({
    familyName: ['鈴木', [
      Validators.required,
    ]],
    firstName: ['', [
      Validators.required,
    ]],
    studentsName: this.fb.array([
      this.fb.control('')
    ]),

    answers: this.fb.array([])

  });

  questionslist = [ // matMenuの中に表示するリスト
    '子供の頃のニックネームは？',
    '初めて飼ったペットの名前は？',
    '憧れのニックネームは？',
    '好きな絵本の題名は？',
  ];

  selectedQuestion = []; // matMenuの中のquestionがクリックされたらここにそのquestionが入ります

  constructor(private fb: FormBuilder) {}

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

  addStudentsName() {
    this.studentsName.push(this.fb.control(''));
  }

  removeStudentsName(index: number) {
    this.studentsName.removeAt(index);
  }


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

  addQuestion(question: string) { // この関数はmatMenuで使います
    if (!this.selectedQuestion.includes(question)) { // selectedQuestionにクリックしたquestionが含まれていなかったら
      this.answers.push(                             // answers: this.fb.array([]) に
        this.fb.group({
          answer: ['', Validators.required]          // answer: ['', Validators.required] を追加
        })
      );
      this.selectedQuestion.push(question); // selectedQuestionにquestionを追加
    }  else {
      alert('この質問は既に選択されています');
    }
  }

  removeAnswer(index: number) {
    this.answers.removeAt(index); // removeAtは入力フィールドを削除するための専用のメソッドですのでformArray以外の配列には使えません。
    this.selectedQuestion.splice(index, 1); // selectedQuestionの中のindexの位置から一つ取り除く
  }

  sendAnswer(index: number) { // 単体で送信するための関数です。
    console.log(this.selectedQuestion[index],
      this.answers.value[index].answer);
  }

  submit() {
    console.log(this.form.value);
  }
```


```html:app.component.html
<form [formGroup]="form" (ngSubmit)="submit()">
  <mat-form-field>
    <mat-label>姓</mat-label>
    <input
      matInput
      type="text"
      autocomplete="off"
      formControlName="familyName"
    />
  </mat-form-field>
  <mat-form-field>
    <mat-label>名</mat-label>
    <input
      matInput
      type="text"
      autocomplete="off"
      placeholder="名"
      formControlName="firstName"
    />
  </mat-form-field>
  <ng-container formArrayName="studentsName">
    <button mat-raised-button color="primary" (click)="addStudentsName()">
      生徒追加
    </button>
    <button
      type="button"
      mat-raised-button
      color="basic"
      (click)="removeStudentsName()"
    >
      キャンセル
    </button>

    <ng-container
      *ngFor="let studentName of studentsName.controls; let i = index"
    >
      <mat-form-field>
        <mat-label>生徒の名前</mat-label>
        <input matInput type="text" autocomplete="off" [formControlName]="i" />
      </mat-form-field>
    </ng-container>
  </ng-container>
<!-- ここから追加 -->
  <p>下記のボタンから質問を選んで答えてください</p>
  <button
    mat-raised-button
    color="primary"
    type="button"
    [matMenuTriggerFor]="questionMenu"
  > <!-- 最下部でmatMenuを書いています。-->
    質問を選択
  </button>
  <ng-container formArrayName="answers">
    <ng-container *ngFor="let answer of answers.controls; index as i"> 
      <div [formGroupName]="i">
        <mat-form-field>
          <mat-label>
            {{ selectedQuestion[i] }} 
          </mat-label>
          <input
            matInput
            type="text"
            autocomplete="off"
            formControlName="answer"
          />
        </mat-form-field>
        <button
          type="button"
          mat-raised-button
          color="primary"
          (click)="sendAnswer(i)"
        >
          個別に送信
        </button>
        <button
          type="button"
          mat-raised-button
          color="basic"
          (click)="removeAnswer(i)"
        >
          キャンセル
        </button>
      </div>
    </ng-container>
  </ng-container>
<!-- ここまで -->
  <div>
    <button
      mat-flat-button
      color="primary"
      [disabled]="form.invalid"
      type="submit"
    >
      送信
    </button>
  </div>
</form>
<!-- ここから追加 -->
<mat-menu #questionMenu="matMenu">
  <button
    mat-menu-item
    (click)="addQuestion(question)"
    *ngFor="let question of questionslist; index as i"
  > <!-- ループ表示させたquestionそれぞれにaddQuestionを持たせています。 -->
    {{ i + 1 + '&emsp;' + question }}
  </button>
</mat-menu>
<!-- ここまで -->
```

以上で完成です。

補足　今回の実装はリアクティブフォームであり、テンプレート駆動型フォームではないのでformsModuleは使いませんでした。


詳しくは下記を参考にしてください。
[Angular公式ドキュメント：フォームの概要](https://angular.io/guide/forms-overview)
## まとめ

まだ経験が少なくて応用するためのロジックがなかなか出てこなくて毎回苦労していますが
こうして記事にして整理してみると全然難しいことはしていなかったんだなと実感しています。