Angularの入力バリデーションをディレクティブとして実装する
Angularで入力バリデーションを実装する際、ReactiveFormではFormControlに対して様々なバリデーションを設定できますが、テンプレート駆動型だと設定することはできません。またReactiveFormもバリデーションを設定できるとは言え、事細かに全てをコードで設定するのは面倒だったりします。そこで入力バリデーションをディレクティブとして実装することで、テンプレート駆動型でもReactiveFormでも共通で利用できるようになります。
試した環境
この記事は以下のAngularバージョンで作成されています。
_ _ ____ _ ___ / \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _| / △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | | / ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | | /_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___| |___/ Angular CLI: 7.2.2 Node: 9.11.1 OS: darwin x64 Angular: 7.2.1 ... animations, common, compiler, compiler-cli, core, forms ... language-service, platform-browser, platform-browser-dynamic ... router Package Version ----------------------------------------------------------- @angular-devkit/architect 0.12.2 @angular-devkit/build-angular 0.12.2 @angular-devkit/build-optimizer 0.12.2 @angular-devkit/build-webpack 0.12.2 @angular-devkit/core 7.2.2 @angular-devkit/schematics 7.2.2 @angular/cli 7.2.2 @ngtools/webpack 7.2.2 @schematics/angular 7.2.2 @schematics/update 0.12.2 rxjs 6.3.3 typescript 3.2.4 webpack 4.28.4
入力バリデーションディレクティブの実装
Angularのドキュメントにもある通り、入力バリデーションディレクティブを実装することはとても簡単です。やらなければいけないことは次の2点です。
Validator
を実装したディレクティブを作るNG_VALIDATORS
として実装したディレクティブをprovideする
今回は数値の最小バリデーション( <input type="number">
の min
)を実装してみます。
プロジェクトを作る
まずは適当にAngular CLIを使用してプロジェクトを作成します。
ng new form-validation
ディレクティブを作る
プロジェクトを作成したら次に入力バリデーション用のディレクティブを作成します。これもAngular CLIを使用するととても簡単です。
ng generate directive min-validator
Angular Schematicによってサクッとこんなディレクティブが生成されます。
import { Directive } from '@angular/core'; @Directive({ selector: '[appMinValidator]' }) export class MinValidatorDirective { constructor() { } }
Validator
を実装する
ディレクティブが生成されたら、次に生成されたディレクティブに対して @angular/forms/Validator
を実装します。このインターフェースには validate
と registerOnValidatorChange
の2つのメソッドがあります。 validate
は実際に入力された値に対するバリデーションを実行するメソッドです。 registerOnValidatorChange
は最初値の値が変更された時など、入力バリデーションの条件が変更された時などに呼び出すコールバックを登録するメソッドです。
先ほどのディレクティブに Validator
を空実装するとこんな感じになるでしょう。
import { Directive } from '@angular/core'; import { AbstractControl, ValidationErrors, Validator } from '@angular/forms'; @Directive({ selector: '[appMinValidator]' }) export class MinValidatorDirective implements Validator { constructor() { } validate(control: AbstractControl): ValidationErrors | null { return undefined; } registerOnValidatorChange(fn: () => void): void { } }
最小値バリデーションなので、比較する値を取得するプロパティを追加します。
private _min!: number; get min(): number | string { return this._min; } @Input() set min(min: number | string) { this._min = typeof min === 'number' ? min : parseFloat(min); }
そして取得した最小値からバリデーションの関数を生成し、バリデーションを実行します。。バリデーションロジック自体はAngularが標準で持っている @angular/forms/Validators.min
を使用します。
private _min!: number; private _validator!: ValidatorFn; get min(): number | string { return this._min; } @Input() set min(min: number | string) { this._min = typeof min === 'number' ? min : parseFloat(min); this._validator = Validators.min(this._min); } validate(control: AbstractControl): ValidationErrors | null { return this.min == null ? null : this._validator(control); }
バリデーションの処理を実装したら、次は条件変更時のコールバック呼び出しを実装します。コールバック呼び出しまでを含めた全ての実装はこんな感じになるでしょう。
import { Directive, Input } from '@angular/core'; import { AbstractControl, ValidationErrors, Validator, ValidatorFn, Validators } from '@angular/forms'; @Directive({ selector: '[appMinValidator]' }) export class MinValidatorDirective implements Validator { private _min!: number; private _validator!: ValidatorFn; private _onChange!: () => void; get min(): number | string { return this._min; } @Input() set min(min: number | string) { this._min = typeof min === 'number' ? min : parseFloat(min); this._validator = Validators.min(this._min); if (this._onChange) { this._onChange(); } } validate(control: AbstractControl): ValidationErrors | null { return this.min == null ? null : this._validator(control); } registerOnValidatorChange(fn: () => void): void { this._onChange = fn; } }
ディレクティブのセレクター変更
今回実装したディレクティブは <input type="number">
の min
に反応させたいため、セレクターを変更します。
@Directive({ selector: 'input[type=number]' })
NG_VALIDATORS
のprovide
最後に、ディレクティブを入力バリデーションで使用できるものだとAngularに認識させるために @angular/forms/NG_VALIDATORS
に対するprovideを追加します。
import { Directive, forwardRef, Input } from '@angular/core'; import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn, Validators } from '@angular/forms'; export const MIN_VALIDATOR: StaticProvider = { provide: NG_VALIDATORS, useExisting: forwardRef(() => MinValidatorDirective), multi: true }; @Directive({ selector: 'input[type=number]', providers: [MIN_VALIDATOR] }) export class MinValidatorDirective implements Validator { ... }
provide追加時のポイントは useExisting
を使用することと multi
に true
を設定することです。