開発覚書はてな版

個人的な開発関連の備忘録

【Angular/class-validator】C#/WPF の INotifyDataErrorInfoをTypeScriptで実現する

概要

  • C#/WPF では INotifyDataErrorInfoとDataAnnotations を使用した入力バリデーションが存在します。今回はそれをAngular版にしてみました。
  • 今回はclass-validator + Angular + TypeScriptで実現します。

INotifyDataErrorInfo実装サンプル

github.com

実装方針

  • class-validatorのデコレータをプロパティに定義する。
  • ベースとなるViewModel内でclass-validatorのvalidateSyncで入力バリデーションを行う。
  • プロパティ変更時に上記のバリデーションが実行されるように実装する。

実行環境

  • Node.js - 10.x
  • Yarn - 1.17.x

使用ライブラリ

  • TypeScript - 3.5.x
  • Angular - 8.2.x
  • class-validator - 0.10.x

サンプルソース

view-models/notify-error-info.view-model.ts

import { validateSync } from 'class-validator';

export abstract class NotifyErrorInfoViewModel {
  protected readonly errors = new Map<string, string[]>();

  get hasErrors() {
    return this.errors.size > 0;
  }

  getErrors(propertyName: string): string[] {
    const errors = this.errors.get(propertyName);
    return errors ? errors : [];
  }

  protected onPropertyChanged(propertyName: string): void {
    this.setErrors(propertyName);
  }

  protected setErrors(propertyName: string): void {
    const results = validateSync(this);
    const errorMsgs = results.filter((r) => r.property === propertyName).map((r) => Object.values(r.constraints));

    if (errorMsgs.length > 0) {
      this.errors.set(propertyName, errorMsgs.reduce((prev, current) => prev.concat(current)));
    } else {
      this.errors.delete(propertyName);
    }
  }
}

view-models/edit-data.view-model.ts

import { MaxLength, Max, Min, IsNotEmpty } from 'class-validator';
import { NotifyErrorInfoViewModel } from './notify-error-info.view-model';

export class EditData extends NotifyErrorInfoViewModel {
  /** name */
  @MaxLength(20, { message: 'name は 20文字以内で入力してください。' })
  @IsNotEmpty({ message: 'name を入力してください。' })
  set name(value: string) {
    this._name = value;
    this.onPropertyChanged('name');
  }
  get name() {
    return this._name;
  }

  /** age */
  @Max(99, { message: 'age は 10~99で入力してください。' })
  @Min(10, { message: 'age は 10~99で入力してください。' })
  set age(value: number) {
    this._age = value;
    this.onPropertyChanged('age');
  }
  get age() {
    return this._age;
  }

  /** name */
  private _name = '';

  /** age */
  private _age = 20;
}

app.component.ts

import { Component } from '@angular/core';

import { EditData } from './view-models/edit-data.view-model';

@Component({
  selector: 'app-root',
  template: `
    <div>
      Name: <input name="name" type="text" [(ngModel)]="editData.name" />
      <span style="color: red;" *ngIf="editData.getErrors('name').length > 0">
        {{ editData.getErrors('name')[0] }}
      </span>
    </div>
    <div>
      Age: <input name="age" type="number" [(ngModel)]="editData.age" />
      <span style="color: red;" *ngIf="editData.getErrors('age').length > 0">
        {{ editData.getErrors('age')[0] }}
      </span>
    </div>
    <div>hasError: {{ editData.hasErrors }}</div>
  `,
})
export class AppComponent {
  /** 編集データ */
  editData = new EditData();
}

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, FormsModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

実行結果

f:id:kakkoya:20191028211258g:plain

サンプルソース一式

github.com

おわりに

  • 多言語でよくあるメタデータベースでのバリデーションがTypeScriptでも実現できました。
  • Angular/TypeScriptでonPropertyChangedパターンで実装すると冗長になるので、もうちょいシンプルな記載にしたいですね。