5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

AngularでUXを意識したメールアドレス入力欄のバリデーションを実装してみた。

Last updated at Posted at 2019-02-03

はじめに

「UXを意識した~」と一丁前に言ってますが、CodeGridというフロントエンド関係を主に扱うサイトの、「よりよいメールアドレス入力欄を求めて」という記事の内容を一部実装しただけです💦
加えてメールアドレスの形式チェックに関してはあまり厳密に行っていません。

今回のポイント

1. メールアドレスチェックのタイミング

(個人的に)多く見かけるのが全てを入力し終え「登録」ボタンを押してから初めてチェックをし、入力の誤りをユーザーに通知するやり方です。これだと再入力へのステップ数が増えるほか、入力モードから解放され一息ついたユーザーを再度入力モードに戻してしまいストレスになります。どうせなら入力モードの状態で誤りを直しておきたいものです。
そこでチェックのタイミングはリアルタイム
で行います。
幸いにもAngularには便利なリアクティブフォームがあるのでリアルタイムなチェック処理は簡単に実装できます。👏

2. メールアドレスのチェック内容

チェック内容にも気を付けます。メールアドレスの形式チェックを行ってしまうと、正しいメールアドレスを入力しようとしているのに1文字目から形式チェックが走り怒られてしまうのです。
そこで今回は先の記事にある通り、メールアドレスの構文で禁止されている文字列が入力値に含まれていないかをチェックします。
そのため

  • 共通チェック
  • ローカル部分(@より前)のチェック
  • ドメイン部分(@より後)のチェック

この3つに分解してチェックを行うことにしました。

3. 登録ボタン

チェックに引っかからなければメールアドレスの形式で無くても登録ボタンが押せてしまいます。
これに関しては正しいメールアドレスの形式になるまでボタンを非活性にすることで対処します。

以上のポイントを踏まえて実装していきます。

実装に移る前に

何をもって正しいメールアドレスの形式なのか?というところで今回はWikipedia先生に力をお借りしました。
メールアドレス - Wikipedia
今回は、

  1. 禁則文字
  2. 「..」や「@@」
  3. 先頭の「.」や「@」
  4. 「.@」や「@.」
  5. ドメイン部分(「@」以降)に@が出現する

これらを誤った形式として実装していきます。

実装してみた

Angularのカスタムバリデーションに関しては以前自分が投稿した「Ionic3でCustomValidationとエラーメッセージコンポーネントを実装してみる」という記事に乗っ取って実装していきます。

top.html
    <form [formGroup]="myForm" (ngSubmit)="formSubmit()">

        <div class="input-area">
            <input [formControl]="email" placeholder="メールアドレス" (keyup)="isErrorExists()">
            <error-message [control]="email"></error-message>
        </div>

        <button type="submit" [disabled]="isDisabled" ion-button block color="dark">ログイン</button>

    </form>
top.ts
import { Component } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms";
import { EmailValidator } from "../../services/validator/email.validator";

@IonicPage()
@Component({
    selector: 'page-top',
    templateUrl: 'top.html',
})
export class TopPage {

    isDisabled: boolean = true;

    email = new FormControl('', [
        Validators.required,
        EmailValidator.commonCheck,
        EmailValidator.localCheck,
        EmailValidator.domainCheck
    ]);

    myForm: FormGroup = this.builder.group({
        email: this.email,
    });

    constructor(
        private builder: FormBuilder,
    ) { }

    isErrorExists() {
        // FormGroupのチェック状態を確認
        if (this.myForm.invalid) {
            this.isDisabled = true;
            return;
        }
        // 設定したcheckを通過していたらemailの形式チェックを行う。
        this.isDisabled = !!(!this.myForm.controls[ 'email' ].value.match(/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/));
    }
}
email.validator.ts
import { AbstractControl, ValidationErrors } from "@angular/forms";

export class EmailValidator {

    static readonly AT_SIGN = '@';
    static readonly NO_AT_SIGN = 1;
    static readonly VALID_EMAIL = 2;

    static commonCheck(control: AbstractControl): ValidationErrors | null {        
        // 禁則文字を検知
        if (control.value.match(/[<>\"'&|%\\\\]/)) {
            return { 'illegalCharacter': true };
        }
        // ..または@@を検知
        if (control.value.match(/(\.|@)\1+/)) {
            return { 'email': true };
        }
        return null;
    }

    static localCheck(control: AbstractControl): ValidationErrors | null {
        // 先頭の.または@を検知
        if (control.value.match(/(^\.|^@)/)) {
            return { 'email': true };
        }
        // @直前の.を検知
        if (control.value.match(/\.+@/)) {
            return { 'email': true };
        }
        return null;
    }

    static domainCheck(control: AbstractControl): ValidationErrors | null {

        const emailAtSignByDividedArray = control.value.split(EmailValidator.AT_SIGN);
        const thatEmail = emailAtSignByDividedArray.length;

        // @が無いときはドメイン部分のチェックは不要
        if (thatEmail === EmailValidator.NO_AT_SIGN) {
            return null;
        }
        // @が2つあるとき正しいメールアドレスの形式ではない
        if (thatEmail !== EmailValidator.VALID_EMAIL) {
            return { 'email': true };
        }

        const domain = emailAtSignByDividedArray[ 1 ];

        // @直後の.を検知
        if (domain.match(/(^\.)/)) {
            return { 'email': true };
        }
        return null;
    }
}

解説

ボタンに関してはisDisabledで活性非活性を管理しました。

top.html
<button type="submit" [disabled]="isDisabled" ion-button block color="dark">ログイン</button>

これはFormGroup内のバリデーションが全て通りかつメールアドレスの形式が正しいものになった時、活性になるようにしています。
わざわざFormGroupを用いているのは登録画面はほぼほぼ他の項目もあるためです。

top.ts
    isErrorExists() {
        // FormGroupのチェック状態を確認
        if (this.myForm.invalid) {
            this.isDisabled = true;
            return;
        }
        // 設定したcheckを通過していたらemailの形式チェックを行う。
        this.isDisabled = !!(!this.myForm.controls[ 'email' ].value.match(/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/));
    }

isErrorExists()はメールアドレスが入力されるたびに走ります。
なぜ(ngModelChange)ではなく(keyup)なのかと言うと、isErrorExists()email.validatorクラスのバリデーションよりも先に走ってしまうためです。結果、ボタンの活性非活性のタイミングが1テンポ遅れてしまいます。

top.html
<input [formControl]="email" placeholder="メールアドレス" (keyup)="isErrorExists()">

email.validator.tsに関してはソースコードのままです。
commonCheck()では全体を通して禁則文字と連続する「..」及び「@@」を検知します。
localCheckでは先頭の「.」及び「@」、「@」直前の「.」を検知します。
domainCheckでは「@」が入力されてから初めてチェックが始まり、「@」直後の「.」とドメイン部分の「@」を検知します。

実際の動作

qiita_mail_valid.gif

出来ました👏👏👏

最後に

こんなに分割しなくても済むような正規表現の書き方があると思いますが今回はこのレベルで😖
UI/UXって大事だなぁ…(シミジミ)

参考サイト

よりよいメールアドレス入力欄を求めて

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?