L08084のブログ

技術記事の執筆は、祈りに近い

【Angular】複数項目にまたがるカスタムバリデーションを作る(パスワード・確認用パスワードなど)

f:id:l08084:20181125171015p:plain
パスワード・確認用パスワード入力フォームの例

はじめに

アカウント登録画面を作っていて出現頻度が著しく高い項目といえば............そう!パスワードの入力フォームですね。というわけで今回は、パスワードと確認用パスワードが一致しているか確認するバリデーションを実装していきます。

そもそもAngularでカスタムバリデーションをどう作っていいかわからねーよという人は下記の記事を!

【JavaScript】文字列の間に空白(スペース)が入力されているか確認する(氏名などのバリデーション) - L08084のブログ

Angular Materialのインストール方法と使い方については、下記の記事を参照してください。

Angular Material の Tableを使う - L08084のブログ

Angular Materialでログインフォームを作る - L08084のブログ

バリデーション内容

パスワードについてのバリデーションを設定します。チェックする項目は下記とします。

  • パスワード欄と確認用パスワード欄で入力された内容が一致する

バージョン情報

JavaScriptのフレームワークとしてはAngularを、CSSのフレームワークとしてはAngular Materialを使用しています。

  • Angular@7.0.6
  • typescript@3.1.6
  • webpack@4.19.1
  • Angular Material@7.0.4

実装

パスワード入力フォーム作成

前回の記事で作成した入力フォームにコードを足していく感じで実装していきます。
コードが多くなってきて前回からの更新部分がわかりづらいですが、<!-- add this! -->とコメントされている行が追加された部分になります。

f:id:l08084:20181124201537p:plain
前回の記事で作成した入力フォーム

まずテンプレートに、パスワード入力フォームと確認用パスワードの入力フォーム、そしてパスワードが表示しないときに表示するエラーメッセージを表示します。

app.component.html

<div class="container">
  <mat-card class="login-card">
    <mat-card-header>
      <mat-card-title class="login-title">アカウント登録</mat-card-title>
    </mat-card-header>
    <mat-card-content>
      <form [formGroup]="nameRegisterForm" (ngSubmit)="onSubmit()" class="login-form">
        <!-- 氏名の入力フォーム -->
        <mat-form-field class="input-field">
          <input matInput placeholder="氏名(姓と名の間にスペースを入力してください)"
            id="name" name="name" [formControl]="nameRegisterForm.controls.name" required>
          <!-- 必須入力のエラーメッセージ -->
          <mat-error *ngIf="nameRegisterForm.controls.name.errors?.required">氏名は必須項目です</mat-error>
          <!-- スペースがない場合のエラーメッセージ -->
          <mat-error *ngIf="nameRegisterForm.controls.name.errors?.haveBlank">姓と名の間にはスペースを入力してください</mat-error>
        </mat-form-field>
        <!-- add this! -->
        <!-- パスワードの入力フォーム -->
        <mat-form-field class="input-field">
          <input [type]="hide ? 'password' : 'text'" matInput
            placeholder="パスワード" id="password" name="password" [formControl]="nameRegisterForm.controls.password" required>
          <mat-icon matSuffix (click)="hide = !hide">{{hide ? 'visibility' : 'visibility_off'}}</mat-icon>
          <mat-error *ngIf="nameRegisterForm.controls.password.errors?.required">パスワードは必須項目です</mat-error>
        </mat-form-field>
        <!-- 確認用パスワードの入力フォーム -->
        <mat-form-field class="input-field">
          <input [type]="hide ? 'confirmPassword' : 'text'" matInput
            placeholder="確認用パスワード" id="confirmPassword" name="confirmPassword" [formControl]="nameRegisterForm.controls.confirmPassword" required>
          <mat-icon matSuffix (click)="hide = !hide">{{hide ? 'visibility' : 'visibility_off'}}</mat-icon>
          <mat-error *ngIf="nameRegisterForm.controls.confirmPassword.errors?.required">確認用パスワードは必須項目です</mat-error>
          <!-- add this! -->
          <!-- パスワードが一致しないときに表示するエラー -->
          <mat-error *ngIf="nameRegisterForm.controls.confirmPassword.errors?.notMatchPassword">パスワードが一致しません</mat-error>
        </mat-form-field>
        <!-- 登録ボタン -->
        <button type="submit" class="register-button"
          mat-raised-button color="primary" [disabled]="nameRegisterForm.invalid">登録</button>
      </form>
    </mat-card-content>
  </mat-card>
</div>

コンポーネントクラスにもパスワード入力フォームに関するコードを追加します。(テンプレートの時と同じく追加部分には// add this!とコメントしています)

app.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms';
import { CustomValidator } from './custom-validator';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  public nameRegisterForm: FormGroup;
  public nameControl: FormControl;
  // add this!
  public passwordControl: FormControl;
  public confirmPasswordControl: FormControl;
  public hide = true;

  constructor(private builder: FormBuilder) {
    this.createForm();
  }

  public ngOnInit(): void {
    this.nameControl = this.nameRegisterForm.get('email') as FormControl;
    // add this!
    this.passwordControl = this.nameRegisterForm.get('password') as FormControl;
    this.confirmPasswordControl = this.nameRegisterForm.get('confirmPassword') as FormControl;
  }

  public onSubmit() {
    console.log();
  }

  /**
   * フォームグループの初期化を実行する
   *
   */
  private createForm() {
    // 氏名欄のバリデーションを設定している
    this.nameRegisterForm = this.builder.group({
      name: ['', [Validators.required, CustomValidator.haveBlank]],
      // add this!
      password: ['', [Validators.required]],
      confirmPassword: ['', [Validators.required]]
    }, {
      validator: CustomValidator.matchPassword
    });
  }
}

今回追加するのは複数項目に対するバリデーションなので、他のバリデーションと違ってvalidator: CustomValidator.matchPasswordという風にフォーム全体にバリデーションを定義しています。

最後にパスワード入力フォームに入力された内容と確認用パスワードに入力された内容が一致していることを確認するカスタムバリデーションmatchPasswordメソッドを追加します。

custom-validator.ts

import { ValidationErrors, FormControl, AbstractControl } from '@angular/forms';

export class CustomValidator {

  // 姓と名の間にスペースが入っているか確認する
  static haveBlank(control: FormControl): ValidationErrors | null {
    const value = (control.value || '') + '';
    // 両端のスペースを取り除く
    const name = value.trim();
    // 半角スペース
    const NAME_COLUMN_SPRIT_VALUE = ' ';
    // 全角スペース
    const NAME_COLUMN_SPRIT_VALUE_W = ' ';
    let isError = false;
    if (name.indexOf(NAME_COLUMN_SPRIT_VALUE) < 0
    && name.indexOf(NAME_COLUMN_SPRIT_VALUE_W) < 0) {
        // 半角スペースも全角スペースも含まれていない場合
        isError = true;
    }
    // 姓と名の間にスペースが入力されていない場合は、バリデーションエラーを返す
    return isError ? { haveBlank: true } : null;
  }

  // add this!
  // パスワードと確認用パスワードが一致するかチェック
  static matchPassword(ac: AbstractControl) {
    const password = ac.get('password').value;
    const passwordConfirm = ac.get('confirmPassword').value;
    if (password !== passwordConfirm) {
      ac.get('confirmPassword').setErrors({ notMatchPassword: true });
    }
  }
}

単項目に対するバリデーションであるhaveBlankには引数としてFormControlを渡していますが、複数項目に対するバリデーションであるmatchPasswordにはAbstractControlを渡しています。

動作確認

書いたコードを動かしてみます

f:id:l08084:20181128091521p:plain
初期表示

一致しないパスワードを入力するとエラーが表示されます。

f:id:l08084:20181128091844p:plain
パスワード不一致のエラーメッセージが表示されている

一致するパスワードを入力するとエラーが表示されなくなるので、バリデーションが正しく機能していることがわかります。

f:id:l08084:20181128092002p:plain