noxi雑記

.NET、Angularまわりの小ネタブログ

Angularの入力バリデーションをディレクティブとして実装する

Angularで入力バリデーションを実装する際、ReactiveFormではFormControlに対して様々なバリデーションを設定できますが、テンプレート駆動型だと設定することはできません。またReactiveFormもバリデーションを設定できるとは言え、事細かに全てをコードで設定するのは面倒だったりします。そこで入力バリデーションをディレクティブとして実装することで、テンプレート駆動型でもReactiveFormでも共通で利用できるようになります。

angular.io

試した環境

この記事は以下の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 を実装します。このインターフェースには validateregisterOnValidatorChange の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 を使用することと multitrue を設定することです。