9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-03-01

はじめに

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

経緯

以前自分がこちらの記事を投稿し、現場の新規サービスにもこのやり方を取り入れたのですがこれが後々問題になりました…😭
内容としては

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

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

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

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

前提事項

フォームのデザインはAngular Materialを使用します。これの設定等は既に済んでいる前提で進めていきます。
またリアクティブフォームでの実装をしていきます。

フォルダ構成

各フォームコンポーネントを配置する親コンポーネントを今回は`app.component.html`とします。 `base-form.ts`はリアクティブフォームをAngular Materialで実装した際、バリデーション実装の時に`ErrorStateMatcher`を使用するのでこのクラスに実装し各フォームコンポーネントでこの`base-form.ts`を継承することで共通化を図ります。

実装

`app.module.ts`
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 {
}
`base-form.ts`

ErrorStateMatcherの例から必要な箇所だけをとっただけの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();
}
`user-id`フォームコンポーネント

コンポーネント生成時にこのフォームコンポーネント自身を親コンポーネントに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.tsMatcherが適用されます。
あとはバリデーションメッセージを実装するだけです。

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>
各フォームを配置する親コンポーネント

親コンポーネント側では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, passworduser-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>

動作確認

ちゃんと入力された値も親コンポーネントで扱えています。😆

まとめ

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

おわりに

Angularすこだ…。

参考リンク

Partial Reactive Form With Angular Components

9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?