Angularで各フォームを子コンポーネントとして切り出す。

## はじめに
今回は再利用性や保守性をより高めるため、各フォーム項目をコンポーネントとして切り出すことをしていきます。

## 経緯
以前自分が[こちらの記事](https://qiita.com/masaks/items/830b0dc050e5e7d7d84f)を投稿し、現場の新規サービスにもこのやり方を取り入れたのですがこれが後々問題になりました…😭
内容としては

- 条件分岐をうまくやらないと同じ文言が**複数**表示されてしまうため、**条件が複雑になりがち**である。
- 共通化してしまっているのでバリデーションが増えるとほかのフォームのテストもしなければいけなくなる。

この2点が問題になりました。

そこで考えた結果、**各フォームをコンポーネントに切り出せば幸せになるのでは**ということで今の形に落ち着きました。

では実際に切り出していきます。

## 前提事項
フォームのデザインは[Angular Material](https://material.angular.io/)を使用します。これの設定等は既に済んでいる前提で進めていきます。
また[リアクティブフォーム](https://angular.jp/guide/reactive-forms)での実装をしていきます。

## フォルダ構成
<img src="https://qiita-image-store.s3.amazonaws.com/0/316318/d8581787-cb58-3854-c208-1e715a3b677d.jpeg" width=50%>
各フォームコンポーネントを配置する親コンポーネントを今回は`app.component.html`とします。
`base-form.ts`はリアクティブフォームをAngular Materialで実装した際、バリデーション実装の時に`ErrorStateMatcher`を使用するのでこのクラスに実装し各フォームコンポーネントでこの`base-form.ts`を継承することで共通化を図ります。

## 実装
<details><summary>`app.module.ts`</summary><div>

``` app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule, MatFormFieldModule, MatInputModule, MatListModule } from '@angular/material';

import { AppComponent } from './app.component';
import { UserIdComponent } from './form-components/user-id/user-id.component';
import { PasswordComponent } from './form-components/password/password.component';
import { UserNameComponent } from './form-components/user-name/user-name.component';

@NgModule({
    declarations: [
        AppComponent,
        UserIdComponent,
        PasswordComponent,
        UserNameComponent
    ],
    imports: [
        BrowserModule,
        ReactiveFormsModule,
        BrowserAnimationsModule,
        MatFormFieldModule,
        MatInputModule,
        MatButtonModule,
        MatListModule,
    ],
    providers: [],
    bootstrap: [ AppComponent ]
})
export class AppModule {
}
```
</div></details>
<details><summary>`base-form.ts`</summary><div>

[ErrorStateMatcherの例](https://material.angular.io/components/input/overview#changing-when-error-messages-are-shown)から必要な箇所だけをとっただけの`base-form`クラスになります。

``` base-form.ts
import { ErrorStateMatcher } from "@angular/material";
import { AbstractControl } from "@angular/forms";

export class MyErrorStateMatcher implements ErrorStateMatcher {
    isErrorState(control: AbstractControl | null): boolean {
        return !!(control && control.invalid && (control.dirty || control.touched));
    }
}

export class BaseForm {
    matcher = new MyErrorStateMatcher();
}
```
</div></details>
<details><summary>`user-id`フォームコンポーネント</summary><div>

コンポーネント生成時にこのフォームコンポーネント自身を親コンポーネントに`emit`します。


``` user-id.ts
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { BaseForm } from "../base-form";
import { AbstractControl, FormControl, Validators } from "@angular/forms";

@Component({
    selector: 'user-id',
    templateUrl: './user-id.component.html',
    styleUrls: [ './user-id.component.css' ]
})
// ErrorStateMatcherを使用するためBaseFormを継承
export class UserIdComponent extends BaseForm implements OnInit {
    // 親コンポーネントにこのフォームコンポーネントをemitするための@Output()
    @Output() formReady = new EventEmitter<AbstractControl>();
    // フォームの定義
    form: AbstractControl = new FormControl('', [ Validators.required ]);

    constructor() {
        super();
    }

    // コンポーネント生成時に親コンポーネントへフォームコンポーネントをemit
    ngOnInit() {
        this.formReady.emit(this.form);
    }
}
```
ビュー側ではAngular Materialに沿って実装するだけです。
`[formControle]`にコンポーネントクラスで宣言した`FormControl`を指定します。
`[errorStateMatcher]`には`base-form.ts`の`Matcher`が適用されます。
あとはバリデーションメッセージを実装するだけです。

``` user-id.html
<mat-form-field appearance="outline">
    <mat-label>ユーザーID</mat-label>
    <input matInput placeholder="" [formControl]="form" [errorStateMatcher]="matcher">
    <mat-error *ngIf="form.hasError('required')">
        必須項目です。
    </mat-error>
</mat-form-field>
```
</div></details>
<details><summary>各フォームを配置する親コンポーネント</summary><div>

親コンポーネント側では`FormGroup`の宣言とその`FormGroup`に各フォームコンポーネントをセットするための`formInitialized()`を実装します。これをビュー側から呼んであげるだけで各フォームコンポーネントがセットされていきます。
値は

``` .ts
this.FormGroup.get('key').value
```
で取得することができます。

``` app.component.ts
import { Component } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup } from "@angular/forms";

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: [ './app.component.css' ]
})
export class AppComponent{
    // 各フォームコンポーネントをセットするためのFormGroup
    myForm: FormGroup;

    // 初期化時にFormGroupをインスタンス化
    constructor(private fb: FormBuilder) {
        this.myForm = this.fb.group({});
    }

    // 子コンポーネントから受け取ったフォームを親のFormGroupに追加
    formInitialized(name: string, form: AbstractControl) {
        this.myForm.setControl(name, form);
    }

    showValue() {
        // 値はthis.formGroup.get('key').valueで取得することができる。
        console.log(`ユーザーID ... ${this.myForm.get('userId').value}`);
        console.log(`ユーザーネーム ... ${this.myForm.get('userName').value}`);
        console.log(`パスワード ... ${this.myForm.get('password').value}`);
    }
}
```

ビュー側では子コンポーネントから`@Output()`によって`emit`された値を受け取り、親コンポーネントの`formInitialized()`に`key`とともに渡します。これだけで親コンポーネントの`FormGroup`にセットされます。
※`user-name`, `password`も`user-id`とほぼ同じの実装

``` app.component.html
<div id="home-container">
    <form [formGroup]="myForm" (ngSubmit)="showValue()">
        <mat-list>
            <mat-list-item>
                会員登録
            </mat-list-item>
            <mat-list-item>
                <user-id (formReady)="formInitialized('userId', $event)"></user-id>
            </mat-list-item>
            <mat-list-item>
                <user-name (formReady)="formInitialized('userName', $event)"></user-name>
            </mat-list-item>
            <mat-list-item>
                <password (formReady)="formInitialized('password', $event)"></password>
            </mat-list-item>
            <mat-list-item>
                <button type="submit" mat-raised-button color="primary">登録</button>
            </mat-list-item>
        </mat-list>
    </form>
</div>
```
</div></details>

## 動作確認
<img src="https://qiita-image-store.s3.amazonaws.com/0/316318/7da7e9bb-c7f1-570a-0e14-f6e61ce98642.gif" width=90%>
ちゃんと入力された値も親コンポーネントで扱えています。😆

## まとめ
このようにコンポーネントとして切り出すことで、フォームごとに挙動を変えても他のフォームにほぼ影響が出なくなります。
また一つのフォームコンポーネントを修正するだけで、他に使われている箇所まで反映されるので実装がとても楽になります。

## おわりに
Angularすこだ…。

## 参考リンク
[Partial Reactive Form With Angular Components](https://itnext.io/partial-reactive-form-with-angular-components-443ca06d8419)