18
6

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入門 未経験から1ヶ月でサービス作れるようにする その9. カスタムバリデーション

Last updated at Posted at 2018-08-16

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

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

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

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

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

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

また新しく 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

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

ここまでのコミット

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

次は、バリデータのディレクティブ(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;
   }
 }

クラス定義の部分は、 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

ここまでのコミット

まとめ

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

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

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

入門記事一覧

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

18
6
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
18
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?