📝

【Angular】しっかり理解する!フォームバリデーションのテクニック!

2023/08/09に公開

はじめに

皆さんこんにちは、株式会社エムアイ・ラボの新人エンジニアです!

今回は、Angularのリアクティブフォームのバリデーションについて業務で実装させていただく機会があり、学習したので皆さんに共有したいと思います。

Angularのフォームについて

Angularを用いてフォームを作成するとき、基本的には2つのアプローチがあります。
1つ目は「テンプレート駆動フォーム」で、主にHTML側で構築とバリデーションの制御が行われます。
もう1つは「リアクティブフォーム」で、こちらは主にコンポーネント側で実装され、バリデーションの設定が可能です。  
リアクティブフォームはテンプレート駆動フォームに比べて高い拡張性を備えています。

今回、主に触れていきたいのは、リアクティブフォームのバリデーションです。
リアクティブフォームでは、コンポーネント内でFormControlインスタンスに直接バリデータを設定することができます。
これらのバリデータは関数として定義され、入力が特定の条件を満たさない場合にエラーを返す役割をします。
リアクティブフォームのバリデーションにおいては、Validatorsクラスの関数を活用することが可能で、さらに状況に応じてカスタムバリデータを作成することも可能です。
また、実行時にバリデーションルールを変更することも可能です。
ユーザーの入力に基づいてフォームの挙動を動的に変更する必要がある場合(特定のチェックボックスがオンの場合にだけ特定のフィールドが必須になる)など複雑な条件でも実装することできます。

Validatorsクラスの関数

AngularのValidatorsクラスは、FormControlのバリデーションを行うためのメソッドを提供しています。これらは「バリデータ」であり、特定の条件が満たされていない場合にエラー情報を提供する関数になっています。

以下は、主要なメソッドです。

  1. required
    フィールドが必須であることを表し、ユーザーが何も入力しない場合にエラーを返します。

  2. minLength
    入力値の最小文字数を制限します。指定した最小文字数未満の場合、エラーを返します。

  3. maxLength
    入力値の最大文字数を制限します。指定した最大文字数を超える場合、エラーを返します。

  4. pattern
    入力値が特定の正規表現パターンに一致するかどうかを検証します。パターンに一致しない場合、エラーを返します。

  5. email
    入力値が有効なメールアドレス形式になっているかを検証します。メールアドレス形式でない場合、エラーを返します。

その他のバリデータ関数はこちらから(公式ドキュメント)

これらのバリデータは単独でも使用できますが、複数を組み合わせて使用することで、より具体的なバリデーションルールを作成することも可能です。
例えば、必須入力かつ文字数を制限したい場合、Validators.requiredValidators.minLength, Validators.maxLengthを組み合わせることでその要件を満たすことができます。

以下は、入力必須かつ8文字以上20文字以下の制限をかける例です。

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app-password-form',
  templateUrl: './password-form.component.html'
})
export class PasswordFormComponent implements OnInit {
  passwordForm: FormGroup;

  ngOnInit(): void {
    this.passwordForm = new FormGroup({
      'password': new FormControl(null, [
        Validators.required,
        Validators.minLength(8),
        Validators.maxLength(20)
      ]),
    });
  }
}
<form [formGroup]="passwordForm">
  <input type="password" formControlName="password" placeholder="Password">
</form>
<p *ngIf="passwordForm.get('password').errors?.minlength">
  8文字以上で入力してください!
</p>
<p *ngIf="passwordForm.get('password').errors?.maxlength">
  20文字以下で入力してください!
</p>

また、これらの基本的なバリデータに加えて、カスタムバリデータを作成し、アプリケーション固有の複雑なバリデーション要件を満たすことが可能になります!

カスタムバリデータについて

Angularでは、カスタムバリデータを作成して、フォームの入力値に対して独自の検証ルールを適用することができます。
カスタムバリデータは関数として定義します。
FormControlを引数に取り、バリデーションエラーがある場合にはエラーオブジェクトを返し、ない場合にはnullを返します。

カスタムバリデータで、パスワードと確認用パスワードが一致していることを確認する例は以下です。


import { FormGroup, ValidatorFn, ValidationErrors } from '@angular/forms';

// パスワードと確認パスワードが一致していることをチェックするバリデータ
export const passwordMatchValidator: ValidatorFn = (control: FormGroup): ValidationErrors | null => {
  const password = control.get('password');
  const confirmPassword = control.get('confirmPassword');

  return password && confirmPassword && password.value === confirmPassword.value ? null : { 'passwordMismatch': true };
};


このバリデータは、パスワードと確認用パスワードのフィールドが一致しているかどうかを確認し、一致していない場合にはエラーを返します。

このバリデータをFormBuilderで作成したFormGroupに適用することができます。

this.form = this.formBuilder.group({
  password: ['', [Validators.required, Validators.minLength(8)]],
  confirmPassword: ['', Validators.required],
}, { validators: passwordMatchValidator });

これにより、パスワードと確認パスワードが一致していることを確認できます。
このように、カスタムバリデータを使用することで、アプリケーションの特定の要件に対応したバリデーションを簡単に実装することができます。

ここに更に、特定のチェックボックス("changePassword")がチェックされている場合にのみ、パスワードと確認用パスワードが一致するかを確認するカスタムバリデータを実装してみます!

カスタムバリデータを使用して、"changePassword" チェックボックスがチェックされている場合にのみ、パスワードと確認用パスワードが一致することを確認します。

import { FormGroup, ValidatorFn, ValidationErrors } from '@angular/forms';

// チェックボックスがチェックされた場合のみ、パスワードと確認パスワードが一致していることをチェックする
export const PasswordMatchValidator: ValidatorFn = (control: FormGroup): ValidationErrors | null => {
  const changePassword = control.get('changePassword');
  const password = control.get('password');
  const confirmPassword = control.get('confirmPassword');

  return !changePassword.value || (password && confirmPassword && password.value === confirmPassword.value) ? null : { 'passwordMismatch': true };
};
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import { conditionalPasswordMatchValidator } from './password-match.validator';

@Component({
  selector: 'app-password-form',
  templateUrl: './password-form.component.html'
})
export class PasswordFormComponent implements OnInit {
  passwordForm: FormGroup;

  ngOnInit(): void {
    this.passwordForm = new FormGroup({
      'changePassword': new FormControl(false),
      'password': new FormControl(null),
      'confirmPassword': new FormControl(null)
    }, { validators: PasswordMatchValidator });
  }
}


<form [formGroup]="passwordForm">
  <input type="checkbox" formControlName="changePassword"> Change Password <br>
  <input type="password" formControlName="password" placeholder="Password"> <br>
  <input type="password" formControlName="confirmPassword" placeholder="Confirm Password">
</form>
<p *ngIf="passwordForm.errors?.passwordMismatch">
   パスワードが一致しません!
</p>

このような複雑な条件のバリデーションは、テンプレート駆動フォームでは実現が難しいものの一つです。

さらにさらに! フォームの状態(ユーザー操作の状態)でバリデーションのエラーメッセージを表示するタイミングを指定することも可能です!

<form [formGroup]="passwordForm">
  <input type="checkbox" formControlName="changePassword"> Change Password <br>
  <input type="password" formControlName="password" placeholder="Password" (blur)="passwordForm.get('password').markAsTouched()"> <br>
  <input type="password" formControlName="confirmPassword" placeholder="Confirm Password" (blur)="passwordForm.get('confirmPassword').markAsTouched()">
  <p *ngIf="passwordForm.get('password').touched && passwordForm.get('password').invalid">
    パスワードが無効です!
  </p>
  <p *ngIf="passwordForm.get('confirmPassword').touched && passwordForm.get('confirmPassword').invalid">
    確認用パスワードが無効です!
  </p>
  <p *ngIf="passwordForm.touched && passwordForm.errors?.passwordMismatch">
    パスワードが一致しません!
  </p>
</form>

この例では、ユーザーが一度でもパスワード入力フィールドからフォーカスが外したとき(つまり、touchedの状態になったとき)、その入力フィールドの値が無効ならエラーメッセージを表示します。

markAsTouched()ついては前回、私が書いた記事に詳しく書いてあるので是非読んでぜひご覧ください!

まとめ

プログラミングはインプットも大事ですが、アウトプットすることでより理解を深めることができると思っています。
angularの公式ドキュメントには環境構築の必要なしに、バリデーションが試すことができるサンプルコードがStackBlitz上に公開されています!

https://stackblitz.com/edit/wk7v3s?file=src%2Fapp%2Fapp.module.ts

リアクティブフォーム、テンプレートフォームどちらの実装もあり比較するのにとても良いサンプルだと思います。
参考にしてみてください。

また、日々の業務を通じて学んだことや気づきがあれば、記事として投稿していきたいと思います!

採用情報

エムアイ・ラボでは一緒に開発に携わってくれるエンジニアを積極的に採用中です!
カジュアル面談ももちろん大歓迎です!

私はまだ入社して4ヶ月ですが、すでに様々なことに挑戦させていただき、とても充実した日々を送っています。

ぜひお気軽にご連絡ください!

https://www.wantedly.com/companies/milab-inc

Discussion