Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

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

前回の振り返り

前回は、Angularのバリデーションの基礎を学びました。

この記事のソースコード

https://github.com/seteen/AngularGuides/tree/入門その09

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

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

今回は 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 について

export function forbiddenWordValidator(word: string): ValidatorFn {

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

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

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

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

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

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+クリックで簡単に定義に飛ぶことができます。

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

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 について

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

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

HTMLの実装

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

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

動作確認

001.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

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

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

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

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

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

クラス定義の部分は、 Validatorimplements し、 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://github.com/seteen/AngularGuides/commit/dfadb8120e6e0ce592fbe639d08bacb4c2f20f20

まとめ

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

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

Angular入門 未経験から1ヶ月でサービス作れるようにする その10. Firebaseを使ったAPI通信1

入門記事一覧

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

seteen
Angularを広めたいエンジニア Ruby, Angularが好きです。
https://twitter.com/yazumoto
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away