Angular入門 ~ FormsModule編

## 概要
Angularで個人的に一番よく使っているFormsModuleを用いた入力管理の方法を入門レベルでまとめました。

:::note warn
プロジェクト全体のソースコードはこちらに置いてあります。
https://github.com/ist-h-i/Angular/tree/main/v-18
:::
## 環境設定
### node -v
v20.17.0

### ng version
Package | Version
— | ---
@angular-devkit/architect | 0.1802.6
@angular-devkit/build-angular | 18.2.6
@angular-devkit/core | 18.2.6 (cli-only)
@angular-devkit/schematics | 18.2.6
@schematics/angular | 18.2.6
rxjs | 7.8.1
typescript | 5.5.4
zone.js | 0.14.10
## 初期構築コマンド
### Angularインストール
npm install -g @angular/cli

### プロジェクト作成
ng new “アプリ名”

### コンポーネント作成
ng g c components/”コンポーネント名”

### node-modulesインストール
npm i

### アプリ起動
npm start
## 実装
上記手順でformsコンポーネントを作成しています。
### forms.component.html
```html
<form [formGroup]="myForm">
    <!-- formArrayName="items"で、FormArray("items")を指定 -->
    <div formArrayName="items">
        <!-- items.controlsをループして、各FormControlを表示 -->
        @for (item of items.controls; track $index) {
        <div>
            <!-- FormControlのvalueに紐づくinputを表示 -->
            <input [formControlName]="$index" type="text">
        </div>
        <!-- FormControlのエラーメッセージを表示 -->
        @if (item.invalid && (item.dirty || item.touched)) {
        <div>
            <!-- requiredエラーの場合 -->
            @if (item.errors?.['required']) {
            <div>入力してください</div>
            }
            <!-- minlengthエラーの場合 -->
            @if (item.errors?.['minlength']) {
            <div>最低文字数は{{ item.errors?.['minlength'].requiredLength }}文字です</div>
            }
        </div>
        }
        }
    </div>
    <!-- FormArrayのエラーメッセージを表示 -->
    @if (items.invalid && (items.dirty || items.touched)) {
    <div>
        <!-- atLeastOneItemエラーの場合 -->
        @if (items.errors?.['atLeastOneItem']) {<div>1つ以上の項目を入力してください</div>}
    </div>
    }
    <!-- 追加ボタン -->
    <button (click)="addItem()">追加</button>
    <!-- 削除ボタン -->
    <button (click)="removeItem()">削除</button>
</form>
```

### forms.component.ts
```typescript
import { Component } from '@angular/core';
import { FormGroup, FormArray, FormControl, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from '@angular/forms';
import { ValidatorFn } from '@angular/forms';

/**
 * formArrayを使用して複数のフォームコントロールを生成する
 * formArrayの長さが0の場合、atLeastOneItemValidatorによってエラーを返す
 */
@Component({
  selector: 'app-forms',
  standalone: true,
  imports: [
    ReactiveFormsModule
  ],
  templateUrl: './forms.component.html',
  styleUrl: './forms.component.scss'
})

export class FormsComponent {

  myForm: FormGroup;

  /**
   * formArrayを取得する
   */
  get items(): FormArray {
    return this.myForm.get('items') as FormArray;
  }

  /**
   * constructor
   */
  constructor() {
    /**
     * formArrayを生成する
     * formArrayの長さが0の場合、atLeastOneItemValidatorによってエラーを返す
     */
    this.myForm = new FormGroup({
      items: new FormArray([], this.atLeastOneItemValidator())
    });
  }

  /**
   * formArrayに新しいFormControlを追加
   *
   * @remarks
   *  - FormControlのvalueは空文字列
   *  - FormControlにはrequiredとminLength(3)のValidatorを追加
   */
  addItem(): void {
    this.items.push(new FormControl(
      '',
      [
        Validators.required,
        Validators.minLength(3)
      ]
    ));
  }

  /**
   * formArrayの最後尾のFormControlを削除
   */
  removeItem(): void {
    this.items.removeAt(this.items.length - 1);
  }

  /**
   * formArrayの長さが0の場合、atLeastOneItemValidatorによってエラーを返す
   * @param control formArray
   * @returns null or { atLeastOneItem: true }
   */
  private atLeastOneItemValidator(): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      const formArray = control as FormArray;
      return formArray.controls.length > 0 ? null : { atLeastOneItem: true };
    };
  }
}
```
## まとめ
今回はAngular入門第一弾としてよく使っているFormsでの入力管理をまとめてみました。
FormArrayやカスタムバリデータを使えると実装に幅が出ていろいろな状況で使えますね。
注意事項としてFormsは双方向バインディングのngModelと相性が悪いです。（前提として片方で事足りる場合がほとんどで、併用しないといけない状況が思い浮かばないですが下のおまけに併用方法を残しておきます。）
次回はAngularバージョン18で安定板になったzonelessでのsignalを用いた画面への変更反映をまとめる予定です。
## おまけ
Angular 14以降では、ngModelとFormGroupを併用するためのオプションが提供されています。
具体的には、ngModelOptionsディレクティブを使用して、ngModelとFormGroupを併用するための設定を指定できます。
次の例は、ngModelOptionsディレクティブを使用して、ngModelとFormGroupを併用する方法を示しています。

```html
<form [formGroup]="myForm">
  <input formControlName="name" [(ngModel)]="user.name" [ngModelOptions]="{ standalone: true }">
</form>
```