Ionic3でCustomValidationとエラーメッセージコンポーネントを実装してみる

# はじめに
IonicでのCustomValidationとそのおまけでエラーメッセージコンポーネントも実装したので共有します。
実装に関しての参考リンクは一番下へ😘

追記：**IonicというよりAngularですねこれ💦**

## 注意事項
ionicがどういったもの？Angularって？といったことは省略させていただきます。
またNode.js, Ionic, cordova等のインストールが済んでいる前提で進めていきます。

## CustomValidation編

※今回の例ではemailの形式チェックと、数字のみ使用されているかのチェックを実装します。

```services/custom.validation.ts
import { AbstractControl, ValidationErrors } from "@angular/forms";

export class CustomValidation {

    /**
     * バリデーションチェック対象のエラーメッセージを返却する。
     * @param validatorName
     * @param validatorValue
     * @return string
     */
    static getValidationErrorMessage(validatorName: string, validatorValue?: any) {

        let config = {
            'required': `必須項目です。`,
            'invalidEmail': 'メールアドレスの形式が正しくありません。',
            'invalidNumeric': 'コードは数字のみ使用できます。',
            'maxlength': `${validatorValue.requiredLength}字以内で入力してください。`
        };

        return config[ validatorName ]
    }

    /**
     * emailの形式に沿っているか判定する。
     * @param control
     * @return { key: true } | null
     */
    static emailValidator(control: AbstractControl): ValidationErrors | null {

        return !control.value.match(/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/)
            ? { 'invalidEmail': true } : null;
    }

    /**
     * 数値だけかを判定する。
     * @param control
     * @return { key: true } | null
     */
    static numericValidator(control: AbstractControl): ValidationErrors | null {

        return !control.value.match(/^[0-9]+$/)
            ? { 'invalidNumeric': true } : null;
    }

}
```

`getValidationErrorMessage()`に関しては後述のエラーメッセージコンポーネントで使用するメソッドです。
エラーメッセージのkeyと文言に値を使用するためのオブジェクトを引数として受け取ります。
今回はmaxlengthに指定された数値を取得しています。

CustomeValidationでは`{ 'key': boolean }`の形で返却する必要があります。
**チェックに引っかかった時`True`になる**点に注意してください。

当初は`static`メソッドにせず、使用するコンポーネントクラスで初期化して、`this.~.required`のような形で使用していましたが、後述のエラーメッセージコンポーネントでも使用することから`static`メソッドに切り替えました。

### CustomValidation 使い方

```hoge.ts
/* 他のimportは省略 */
import { CustomValidation } from "../../services/custom.validation"; // ←追加

@IonicPage()
@Component({
    selector: 'page-hoge',
    templateUrl: 'hoge.html',
})
export class HogePage {

    userId = new FormControl('', [
        Validators.required,
        Validators.maxLength(10)
    ]);
    password = new FormControl('', [
        Validators.required,
        Validators.maxLength(16)
    ]);
    email = new FormControl('', [
        Validators.required,
        CustomValidation.emailValidator // ←CustomValidationを使用
    ]);
    code = new FormControl('', [
        Validators.required,
        CustomValidation.numericValidator　// ←CustomValidationを使用
    ]);

    myForm: FormGroup = this.builder.group({
        userId: this.userId,
        password: this.password,
        email: this.email,
        code: this.code
    });

    constructor(
        public navCtrl: NavController,
        public navParams: NavParams,
        private builder: FormBuilder,
    ) {
    }

    formSubmit(): void {
        console.log(`ユーザーID is ...${this.userId.value}`);    //....... valueが表示される
        console.log(`パスワード is ...${this.password.value}`);  //....... valueが表示される
        console.log(`メールアドレス is ...${this.email.value}`);  //....... valueが表示される
        console.log(`コード is ...${this.code.value}`);  //....... valueが表示される
    }
}
```

とても簡単でした。😝

## エラーメッセージコンポーネント編
公式サイトや書籍などのチュートリアルだとビュー側で、

``` hoge.html
    <form [formGroup]="myForm" (ngSubmit)="formSubmit()">

        <div class="input-area">
            <input [formControl]="userId" placeholder="ユーザーID">
            <span class="error-message" *ngIf="(userId.dirty || userId.touched) && userId.errors?.required">ユーザーIDは必須です。</span>
            <span class="error-message" *ngIf="userId.errors?.maxlength">ユーザーIDは10文字以内で入力してください。</span>
        </div>

        <!-- 一部省略 -->

        <button type="submit" [disabled]="myForm.invalid" ion-button block color="dark">ログイン</button>

    </form>

```
このように記述して表示を制御していると思います。
ただ、入力フォームの数やエラーの種類が増えてくると非常にめんどくさい😖
そこでこのエラーメッセージの表示をコンポーネントとして切り出して簡単に再利用できるようにしましょう！

```pages/common/error.message/error.message.ts
import { Component, Input } from "@angular/core";
import { FormControl } from "@angular/forms";
import { CustomValidation } from "../../../services/custom.validation";

@Component({
    selector: 'error-message',
    template: `
        <div [style.display]="errorMessage !== null ? 'block' : 'none'">{{errorMessage}}</div>`,
    styles: [`
        div {
            color: red;
            font-size: 12px;
        }
    `]
})
export class ErrorMessage {

    @Input() control: FormControl;

    /**
     * エラーメッセージを取得する。
     * @return string | null
     */
    get errorMessage(): string | null {

        for (let validatorName in this.control.errors) {
            if (this.control.errors.hasOwnProperty(validatorName) && (this.control.touched || this.control.dirty)) {
                return CustomValidation.getValidationErrorMessage(validatorName, this.control.errors[ validatorName ]);
            }
        }

        return null;
    }
}
```

```エラーメッセージコンポーネント実装後のフォーム周り.html
    <form [formGroup]="myForm" (ngSubmit)="formSubmit()">

        <div class="input-area">
            <input [formControl]="userId" placeholder="ユーザーID">
            <error-message [control]="userId"></error-message>
        </div>

        <!-- 一部省略 -->
        
        <button type="submit" [disabled]="myForm.invalid" ion-button block color="dark">ログイン</button>

    </form>

```
一度フォーカスを外したり入力値がバリデーションチェックに引っかかったりすると、エラーメッセージを表示する処理になっています。

このようにコンポーネント化することで例えどんなにエラーの種類が増えても、<error-message>タグを記述するだけで対応できるようになります。

## さいごに
エラーメッセージコンポーネントの説明が雑になってしまいましたが、これでバリデーション周りはスッキリするかと思います。
しかし形式チェックのようなものだと、1文字入力した途端に「形式が正しくありません。」と怒られてしまうのであまりよろしくありません。
そこはうまい具合にしたいですね…。

## 参考リンク
[AngularAngular Form Builder and Validation Management](https://coryrylan.com/blog/angular-form-builder-and-validation-management)
[Angular4(2+)でカスタムValidationを作る](https://matsnow.hatenablog.com/entry/angular/validation)
[Form Validation(公式)](https://angular.io/guide/form-validation)