Angular入門 未経験から1ヶ月でサービス作れるようにする その9. カスタムバリデーション

Angularで自作のバリデーション（カスタムバリデーション）の作り方を解説していきます。

- カスタムバリデーションの作り方
- カスタムバリデーションのディレクティブの作り方

### 前回の振り返り

[前回](https://qiita.com/seteen/items/d1525383aaf37322b107)は、Angularのバリデーションの基礎を学びました。

## この記事のソースコード
[https://github.com/seteen/AngularGuides/tree/入門その09](https://github.com/seteen/AngularGuides/tree/%E5%85%A5%E9%96%80%E3%81%9D%E3%81%AE09)

## カスタムバリデーションを作ってみる

自作のバリデーションを作ってみます。

今回は `name` に指定した文字列が入っていると、エラーとなる自作のバリデーションを作ってみます。

バリデーションは、 `app/shared/validators` ディレクトリに置いていきます。

実際に動くコード全部を見たい方は、後述のコミットを参照してください。

``` src/app/shared/validators/forbidden-word.ts
import { AbstractControl, ValidatorFn } from '@angular/forms';

export function forbiddenWordValidator(word: string): ValidatorFn {
  return (control: AbstractControl): {[key: string]: any} | null => {
    const forbidden = control.value.includes(word);
    return forbidden ? {'forbiddenWord': {value: control.value}} : null;
  };
}
```

``` src/app/product/product-edit/product-edit.component.ts
... 略
export class ProductEditComponent implements OnInit {
  productForm = this.fb.group({
    id: [''],
    name: ['', forbiddenWordValidator('ぬるぽ')], // <= 変更
    price: ['', Validators.min(100)],
    description: [''],
  });

... 略
```

``` src/app/product/product-edit/product-edit.component.html
... 略
          <input id="name" type="text" formControlName="name" required maxlength="50">
          <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert">
            <div *ngIf="name.errors.required">入力してください</div>
            <div *ngIf="name.errors.maxlength">50文字以内で入力してください</div>
            <div *ngIf="name.errors.forbiddenWord">ガッ</div> // <= 追加
          </div>
... 略
```

### 解説

#### forbidden-word.ts について

``` .ts
export function forbiddenWordValidator(word: string): ValidatorFn {
```

`word` を受け取り、 `ValidatorFn` を返すメソッドを定義しています。この `ValidatorFn` を返すメソッドを作ることで、バリデータを作ることができます。

`ValidatorFn` の実際のコードを見ると、下記のようになっています。

``` .ts
export interface ValidatorFn {
    (c: AbstractControl): ValidationErrors | null;
}
```

この `interface` の中で、 `(xxx: 型): 型` のように表されるのは、メソッドのインターフェースです。
つまり、 `ValidatorFn` が返り値のメソッドは、 `AbstractCtonrol` を引数として、 `ValidationErros` か `null` を返すメソッドを返すという意味になります。

また新しく `ValidationErros` という型が出てきているので確認してみます。

``` .ts
export declare type ValidationErrors = {
    [key: string]: any;
};
```

これは、 `string` をキーとして、何かを返すという型になります。

> TIPS: interface か type か
> Typescript の interface と type は似ています。
> 違いとしては、 typeは、 implements できない。 typeは、宣言のマージ ( declaration merging )ができないがあります。
> 個人的には、とりあえず interface 使ってれば良いのでは、くらいで考えています。
> https://stackoverflow.com/questions/37233735/typescript-interfaces-vs-types

> TIPS: Angularの実装を見る
> Typescriptでは、上記のように、Angularで定義されている型をチェックしたいときが結構あります。
> こういうときは、 webstorm を使っていれば、 Command+クリックで簡単に定義に飛ぶことができます。


実際のコードを見ていきましょう。

``` .ts
return (control: AbstractControl): {[key: string]: any} | null => {
  const forbidden = control.value.includes(word);
  return forbidden ? {'forbiddenWord': {value: control.value}} : null;
};
```

`control.value` で `<input>` に入力された内容が取れるので、そこに `word` が含まれているかを確かめ、含まれていれば、エラーのオブジェクトを返すようにしています。

### product-edit.component.ts について

``` .ts
name: ['', forbiddenWordValidator('ぬるぽ')], // <= 変更
```

`name` の定義に、 `forbiddenWordValidator` を追加しています。

### HTMLの実装

``` .html
<div *ngIf="name.errors.forbiddenWord">ガッ</div> // <= 追加
```

先程バリデータで返すようにしたエラーオブジェクトの key を `forbiddenWord` にしたので、その値が入っているときは、 `ガッ` とエラーメッセージを表示するようにしています。

### 動作確認

![001.gif](https://qiita-image-store.s3.amazonaws.com/0/67424/6a4515db-8077-581b-4fdc-cee078e7a966.gif)

エラーメッセージが出るようになっていますね。

### ここまでのコミット

https://github.com/seteen/AngularGuides/commit/28f7f153c58185aa5a377844e01a81eaf84750ab

## カスタムバリデータのディレクティブを作る

次は、バリデータのディレクティブ（HTML上の属性）を作ってみましょう。
ディレクティブを作ると、わざわざTypescript側で設定しなくても、HTMLだけでバリデーションができます。

`forbidden-word.ts` にコードを追加します。

``` src/app/shared/validators/forbidden-word.ts
import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn } from '@angular/forms';
import { Directive, Input } from '@angular/core';

export function forbiddenWordValidator(word: string): ValidatorFn {
  return (control: AbstractControl): {[key: string]: any} | null => {
    const forbidden = control.value.includes(word);
    return forbidden ? {'forbiddenWord': {value: control.value}} : null;
  };
}

// ↓　追加
@Directive({
  selector: '[appForbiddenWord]',
  providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenWordValidatorDirective, multi: true}]
})
export class ForbiddenWordValidatorDirective implements Validator {
  @Input('appForbiddenWord') forbiddenWord: string;

  validate(control: AbstractControl): {[key: string]: any} | null {
    return this.forbiddenWord ? forbiddenWordValidator(this.forbiddenWord)(control)
      : null;
  }
}
```

HTMLに作ったディレクティブ（属性）を追加します。

``` src/app/product/product-edit/product-edit.component.html
<input id="name" type="text" formControlName="name" required maxlength="50" appForbiddenWord="ぬるぽ"> // <- 変更
<div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert">
  <div *ngIf="name.errors.required">入力してください</div>
  <div *ngIf="name.errors.maxlength">50文字以内で入力してください</div>
  <div *ngIf="name.errors.forbiddenWord">ガッ</div>
</div>
```

作ったディレクティブを `app.module.ts` に追加します。

``` src/app.module.ts
... 略
@NgModule({
  declarations: [
    ...,
    ForbiddenWordValidatorDirective,
  ],
  imports: [
  ...
})
export class AppModule { }
```

Typescriptのバリデータの設定を削除します。

``` src/app/product/product-edit/product-edit.component.ts
... 略
export class ProductEditComponent implements OnInit {
  productForm = this.fb.group({
    id: [''],
    name: [''], // <= 変更
    price: ['', Validators.min(100)],
    description: [''],
  });
```

### 解説

#### forbidden-word.ts

ディレクティブを追加しています。

``` .ts
@Directive({
  selector: '[appForbiddenWord]',
  providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenWordValidatorDirective, multi: true}]
})
```

この部分は、ここから定義するのはディレクティブですよ、という合図のものです。
`selector` は、HTMLの属性で使う文字列を指定していて、
`providers` は、バリデータ用の設定をしています。
内容はバリデータを作るときのおまじない用のようなものと思って大丈夫です（ `useExisting` は自分で作ったクラス名を入れる必要があります）

``` .ts
export class ForbiddenWordValidatorDirective implements Validator {
   @Input('appForbiddenWord') forbiddenWord: string;

   validate(control: AbstractControl): {[key: string]: any} | null {
     return this.forbiddenWord ? forbiddenWordValidator(this.forbiddenWord)(control)
       : null;
   }
 }
```

クラス定義の部分は、 `Validator` を `implements` し、　`validate` メソッドを定義しています。

`validate` メソッドは引数として、このディレクティブと一緒に使われた `FormControl` を与えてくれます。
そのため、ここから取った `FormControl` を、先程作った `forbiddenWordValidator` に渡すという処理を行っています。

#### product-edit.component.html について

追加したディレクティブを `<input>`　要素に追加しています。

#### app.module.ts について

ディレクティブを定義したので、 `declaration` のところに追記しています。（ディレクティブは、コンポーネント同様`declaration` に追加します)

#### product-edit.component.ts について

HTML側で追加するようにしたので、先程追加した、 `name` への `forbiddenWord` のバリデータの追加部分を消しています。

### 動作確認

先ほどと全く同じ動作をするはずです。

![001.gif](https://qiita-image-store.s3.amazonaws.com/0/67424/6a4515db-8077-581b-4fdc-cee078e7a966.gif)

### ここまでのコミット

https://github.com/seteen/AngularGuides/commit/dfadb8120e6e0ce592fbe639d08bacb4c2f20f20

## まとめ

今回は、Angularでのカスタムバリデーションの作り方について学んでいきました。
簡単に作れるので、みなさんも必要になったら作ってみてください。

次回は、API通信のやり方を解説していきます。
バックエンドは、Firebaseを使っていきます。

[Angular入門 未経験から1ヶ月でサービス作れるようにする その10. Firebaseを使ったAPI通信1](https://qiita.com/seteen/items/aa62787d3d47033d3b4c)

## 入門記事一覧
「Angular入門 未経験から1ヶ月でサービス作れるようにする」は、記事数が多いため、[まとめ記事](https://qiita.com/seteen/items/43908e33e08a39612a07) を作っています。
https://qiita.com/seteen/items/43908e33e08a39612a07