狛ログ

2022年8月2日火曜日

【Angular】FormのValidationの書き方を簡略化させる。

オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。

Angularに限ったことでは無いですが、Form の Validation って、記載が煩雑になりますよね。
弊社内のAngularプロジェクトでは、出来る限り記載を楽にしようと日々試行錯誤しています。

今回は、本家サイトのValidationの説明を参考に、
入力必須で最大10文字の「名前」項目
を作ってみます。

(1)とりあえずバリデーションを作ってみる

まずはテンプレート側を作ってみます。
[register.component.html]
<form [formGroup]="formRegister" (ngSubmit)="onSubmit()" (keydown.enter)="$event.preventDefault()">
    <div>
      <label>お名前</label>
      <input formControlName="name" type="text" class="form-control" placeholder="お名前を入力(必須)">

      <ng-container *ngIf="name.invalid && (name.dirty || name.touched)">
        <ng-container *ngIf="name.errors?.['required']">
          <p class="error-message">お名前は必ず入力してください。</p>
        </ng-container>
        <ng-container *ngIf="name.errors?.['maxLength']">
          <p class="error-message">お名前は10文字以内で入力してください。</p>
        </ng-container>
      </ng-container>

    </div>
    <button [disabled]="formRegister.invalid" type="submit">登録</button>
</form>

続いて、コントロール(コンポーネント)側を作ります。(抜粋)
[register.component.ts]
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  // (中略)
export class RegisterComponent implements OnInit {
  // (中略)
  formRegister: FormGroup = this.formBuilder.group({
    name: ['', [Validators.required, Validators.maxLength(10)]],
  });

  constructor(
    private formBuilder: FormBuilder,
  ) {}

  get name() { return this.formRegister.get('name'); }

  ngOnInit(): void {
  // (後略)

ここだけ見ると、何て簡単なんだ!って思うのですが・・・・・

(2)エラー時に背景色を変える

では、ここで、バリデーションエラーの時に、input項目の背景色を変えたい、となったとします。
テンプレート側の修正をしましょう。
[register.component.html]
    <label>お名前</label>
    <input formControlName="name" type="text" class="form-control" placeholder="お名前を入力(必須)"
    [ngClass]="{'alert-danger' : name.invalid && (name.dirty || name.touched)}">
「ngClass」を追加しました。
※ngClassの利用方法については、以前の記事を参照ください。

name.invalid && (name.dirty || name.touched)」の部分、
同じ条件なのに、また記載する事に・・・・

例えば、「名前」の他に、「住所」、「電話番号」などが増えていくと、
その都度、記載していく事に・・・・テンプレートがゴチャゴチャしていきますね。
特に、「 (xxxx.dirty || xxxx.touched)」の記載が大量になります。

「デザイナーとの分業?知ったこっちゃねぇ」なら良いですが、普通は分業していると思うのでhtml(デザイン)に修正が入ると、どんどんAngularに取り込むのが辛くなります。

「関心の分離」と言うことで、バリデーションはバリデーション専用のクラスを作り、それをコントロール(コンポーネント)側にDIする形にしてみようと思います。

(3)バリデーションの処理を分ける

まずは、バリデーション専用のクラスを新規で作成します。
[register.validator.ts]
import { Injectable } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Injectable()
export class RegisterValidator {
  private form: FormGroup;

  constructor() {}

  set formGroup(form: FormGroup) {
    this.form = form;
  }

  get nameInvalid() {
    return (
      this.form.controls['name'].invalid &&
      (this.form.controls['name'].dirty || this.form.controls['name'].touched)
    );
  }
  get nameHasErrorRequired() {
    return this.form.controls['name'].hasError('required');
  }
  get nameHasErrorMaxLength() {
    return this.form.controls['name'].hasError('maxlength');
  }
}
上記クラスは、モジュールで定義をしてDI出来るようにしておいてください
@NgModule({
    providers: [
      RegisterValidator,
  ],

続いて、コントロール(コンポーネント)側でDIします。
[register.component.ts]
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { RegisterValidator } from './register.validator';
  // (中略)
export class RegisterComponent implements OnInit {
  // (中略)
  formRegister: FormGroup = this.formBuilder.group({
    name: ['', [Validators.required, Validators.maxLength(10)]],
  });

  constructor(
    private formBuilder: FormBuilder,
    public v: RegisterValidator,
  ) {}

  ngOnInit(): void {
    // バリデーション設定
    this.v.formGroup = this.formRegister;
  // (後略)

get name() { return this.formRegister.get('name'); }
は不要になるので、削除しています。

コンストラクタで、
  constructor(
    private formBuilder: FormBuilder,
    public v: RegisterValidator,
  ) {}
のように定義して、DIを行います。 public にしているのは、このオブジェクトをテンプレート側で使いたい為です。
また、変数名はなるべく短い方が、テンプレート側がゴチャゴチャしません

「ngOnInit」で、Formの設定内容をバリデーションクラスに送るのも忘れずに。
  ngOnInit(): void {
    // バリデーション設定
    this.v.formGroup = this.formRegister;

(4)テンプレート側の記載を変える

最後はテンプレート側を変更します。
[register.component.html]
<form [formGroup]="formRegister" (ngSubmit)="onSubmit()" (keydown.enter)="$event.preventDefault()">
      <div>
        <label>お名前</label>
        <input formControlName="name" type="text" class="form-control" placeholder="お名前を入力(必須)"
          [ngClass]="{'alert-danger' : v.nameInvalid}">
        <ng-container *ngIf="v.nameInvalid">
          <ng-container *ngIf="v.nameHasErrorRequired">
            <p class="error-message">お名前は必ず入力してください。</p>
          </ng-container>
          <ng-container *ngIf="v.nameHasErrorMaxLength">
            <p class="error-message">お名前は10文字以内で入力してください。</p>
          </ng-container>
        </ng-container>
      </div>
      <button class="btn btn1 ml-auto" [disabled]="formRegister.invalid" type="submit">登録</button>
</form>

name.invalid && (name.dirty || name.touched)」の条件を「v.nameInvalid」で判断出来るので、かなり記載がスッキリします。

「v.nameHasErrorRequired」などについても、実際の処理は、バリデーション専用のクラス側にあるので、処理の分離も出来ています。

という事で、今回はAngularのValidationの書き方を簡略化してみました。


0 件のコメント:

コメントを投稿