かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その22 )( 入力画面2を作成する )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その21 )( 入力画面1を作成する5 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 入力画面2の作成
    • Javascript の処理を実装します。

参照したサイト・書籍

  1. 電話番号に関するQ&A
    http://www.soumu.go.jp/main_sosiki/joho_tsusin/top/tel_number/q_and_a.html

  2. 正規表現でのメールアドレスチェックは見直すべき – ReDoS
    https://blog.ohgaki.net/redos-must-review-mail-address-validation

  3. 正しいメールアドレスのチェック方法
    https://blog.ohgaki.net/how-to-check-email-address

目次

  1. input02.html 内の input タグに maxlength 属性を追加する
  2. 変換&入力チェック処理の仕様を決める
  3. validator.js の validateRegexp 関数を変更する
  4. Form.js に isAllEmpty, isAnyNotEmpty メソッドを追加する
  5. 入力チェックを実装する
  6. input02.html を修正する
  7. 動作確認
  8. 次回は。。。

手順

input02.html 内の input タグに maxlength 属性を追加する

INQUIRY_DATA テーブルのカラムに定義した文字数を src/main/resources/templates/web/inquiry/input02.html 内の <input type="text" .../> に maxlength 属性として追加します。

変換&入力チェック処理の仕様を決める

各項目で以下の変換&入力チェック処理を行います。

項目 変換&入力チェック処理
郵便番号 半角数字変換
必須チェック
7桁固定チェック
住所 必須チェック
電話番号 半角数字変換
「市外局番」の先頭 0 チェック
「市外局番」+「市内局番」= 6桁固定チェック
「加入者番号」= 4桁固定チェック
メールアドレス 半角文字変換
メールアドレスチェック
  • 文字数は maxlength 属性で制御されている前提とし、Javascript ではチェックしません(サーバ側では文字数チェックは行います)。
  • 「郵便番号」のように項目が2つに分かれているものは、両方にカーソルが入った後か、「前の画面へ戻る」「次へ」ボタンをクリックした時でないと変換&入力チェック処理は行いません。例えば「郵便番号」の左側の項目にカーソルがある状態の時に文字列を入力してからマウスで「住所」にカーソルを移動した場合、「郵便番号」の変換&入力チェック処理は実行しません。
  • 「電話番号」と「メールアドレス」は全てにカーソルが入った後にどちらも入力されていなければエラーメッセージを表示します。
  • 「次へ」ボタンがクリックされた時は全ての入力チェックを行いますが、「前の画面へ戻る」ボタンがクリックされた時は入力されているものだけ入力チェックを行います(必須チェックを行いません)。

量が多いので、以下の処理は次回以降に実装します。

  • メールアドレスチェック
  • 「前の画面へ戻る」ボタンがクリックされた時は入力されているものだけ入力チェックする

validator.js の validateRegexp 関数を変更する

src/main/assets/js/lib/util/validator.js の validateRegexp 関数は idList で渡された id の要素を1つずつ正規表現にマッチするかチェックしていますが、id の要素の値を全て結合した文字列に対して正規表現にマッチするかチェックするように変更します。

src/main/assets/js/lib/util/validator.js を以下のように変更します。

/**
 * 正規表現チェック用共通関数
 * @param {Array} idList - チェックを行う要素の id の配列
 * @param {string} pattern - チェックで使用する正規表現のパターン文字列
 * @returns {boolean} true:チェックOK, false:チェックNG
 */
function validateRegexp(idList, pattern) {
    var regexp = new RegExp(pattern);
    return idList.reduce(function (p, id) {
        return p + $(id).val();
    }, "").match(regexp);
}
  • validateRegexp 関数を上記の内容に変更します。

Form.js に isAllEmpty, isAnyNotEmpty メソッドを追加する

idList で渡された id の要素が全て未入力/未選択かをチェックする isAllEmpty メソッド、idList で渡された id の要素の中に1つでも入力/選択されているものがあるかをチェックする isAnyNotEmpty メソッドを追加します。

..........

/**
 * idList で渡された id の要素が全て未入力/未選択かチェックする
 * @param {Array} idList - チェックする id がセットされた配列
 * @returns {boolean} true:全て未入力/未選択である, false:1つ以上入力/選択されているものがある
 */
Form.prototype.isAllEmpty = function (idList) {
    var allEmpty = true;
    idList.forEach(function (id) {
        if ($(id).attr("type") === "radio") {
            if ($(id + ":checked").val() !== undefined) {
                allEmpty = false;
            }
        } else if ($(id).val() !== "") {
            allEmpty = false;
        }
    });
    return allEmpty;
};

/**
 * idList で渡された id の要素の中に未入力/未選択のものがあるかチェックする
 * @param {Array} idList - チェックする id がセットされた配列
 * @returns {boolean} true:未入力/未選択のものがある, false:全て入力/選択されている
 */
Form.prototype.isAnyEmpty = function (idList) {
    ..........
};

/**
 * idList で渡された id の要素の中に1つでも入力/選択されているものがあるかチェックする
 * @param {Array} idList - チェックする id がセットされた配列
 * @returns {boolean} true:1つでも入力/選択されている, false:全て未入力/未選択である
 */
Form.prototype.isAnyNotEmpty = function (idList) {
    var anyNotEmpty = false;
    idList.forEach(function (id) {
        if ($(id).attr("type") === "radio") {
            if ($(id + ":checked").val() !== undefined) {
                anyNotEmpty = true;
            }
        } else if ($(id).val() !== "") {
            anyNotEmpty = true;
        }
    });
    return anyNotEmpty;
};

今さらながら気づきましたが、vaildator.js は Array.reduce で実装していてこちらの処理も Array.reduce を使えばいいのに、Form.js は Array.forEach で実装していました。。。 次回あたりに修正します。

入力チェックを実装する

src/main/assets/js/inquiry/input02.js を以下のように変更します。

"use strict";

var Form = require("lib/class/Form.js");
var converter = require("lib/util/converter.js");
var validator = require("lib/util/validator.js");

var form = new Form([
    "#zipcode1",
    "#zipcode2",
    "#address",
    "#tel1",
    "#tel2",
    "#tel3",
    "#email"
]);

var zipcodeValidator = function (event) {
    var idFormGroup = "#form-group-zipcode";
    var idList = ["#zipcode1", "#zipcode2"];
    form.convertAndValidate(form, event, idFormGroup, idList,
        function () {
            converter.convertHanAlphaNumeric(idList);
        },
        function () {
            validator.checkRequired(form, idFormGroup, idList, "郵便番号を入力してください");
            validator.checkRegexp(form, idFormGroup, idList, "^[0-9]{7}$", "郵便番号は7入力してください");
        }
    );
};

var addressValidator = function (event) {
    var idFormGroup = "#form-group-address";
    var idList = ["#address"];
    form.convertAndValidate(form, event, idFormGroup, idList,
        undefined,
        function () {
            validator.checkRequired(form, idFormGroup, idList, "住所を入力してください");
        }
    );
};

var telAndEmailValidator = function (event) {
    var telIdFormGroup = "#form-group-tel";
    var emailIdFormGroup = "#form-group-email";
    var telIdList = ["#tel1", "#tel2", "#tel3"];
    var emailIdList = ["#email"];
    var idList = telIdList.concat(emailIdList);

    var validateFunction = function () {
        if (form.isAllEmpty(idList)) {
            var errmsg = "電話番号とメールアドレスのいずれか一方を入力してください";
            form.setError(telIdFormGroup, errmsg);
            form.setError(emailIdFormGroup, errmsg);
            throw new Error(errmsg);
        } else {
            // 最初に入力チェックOKの状態にしておく
            form.setSuccess(telIdFormGroup);
            form.setSuccess(emailIdFormGroup);

            // 「電話番号」に1つでも値が入力されていたら入力チェックする
            if (form.isAnyNotEmpty(telIdList)) {
                validator.checkRegexp(form, telIdFormGroup, ["#tel1"],
                    "^0", "市外局番の先頭には 0 の数字を入力してください");
                validator.checkRegexp(form, telIdFormGroup, ["#tel1", "#tel2"],
                    "^[0-9]{6}$", "市外局番+市内局番の組み合わせが数字6桁になるように入力してください");
                validator.checkRegexp(form, telIdFormGroup, ["#tel3"],
                    "^[0-9]{4}$", "加入者番号には4桁の数字を入力してください");
            }
        }
    };

    form.convertAndValidate(form, event, telIdFormGroup, idList,
        function () {
            converter.convertHanAlphaNumeric(telIdList);
        },
        validateFunction
    );
    form.convertAndValidate(form, event, emailIdFormGroup, idList,
        function () {
            converter.convertHanAlphaNumeric(emailIdList);
        },
        validateFunction
    );
};

var executeAllValidator = function (event) {
    form.forceAllFocused(form);
    [
        zipcodeValidator,
        addressValidator,
        telAndEmailValidator
    ].forEach(function (validator) {
        validator(event);
    });
};

var btnBackOrNextClickHandler = function (event, url) {
    // 全ての入力チェックを実行する
    executeAllValidator(event);
    // 入力チェックエラーがある場合には処理を中断する
    if (event.isPropagationStopped()) {
        // 一番最初のエラーの項目にカーソルを移動する
        $(".has-error:first :input:first").focus().select();
        return false;
    }

    // 「前の画面へ戻る」「次へ」ボタンをクリック不可にする
    $(".js-btn-back").prop("disabled", true);
    $(".js-btn-next").prop("disabled", true);

    // サーバにリクエストを送信する
    $("#input02Form").attr("action", url);
    $("#input02Form").submit();

    // return false は
    // event.preventDefault() + event.stopPropagation() らしい
    return false;
};

$(document).ready(function () {
    // 入力チェック用の validator 関数をセットする
    $("#zipcode1").on("blur", zipcodeValidator);
    $("#zipcode2").on("blur", zipcodeValidator);
    $("#address").on("blur", addressValidator);
    $("#tel1").on("blur", telAndEmailValidator);
    $("#tel2").on("blur", telAndEmailValidator);
    $("#tel3").on("blur", telAndEmailValidator);
    $("#email").on("blur", telAndEmailValidator);

    // 「前の画面へ戻る」「次へ」ボタンクリック時の処理をセットする
    $(".js-btn-back").on("click", function (event) {
        return btnBackOrNextClickHandler(event, "/inquiry/input/02/?move=back");
    });
    $(".js-btn-next").on("click", function (event) {
        return btnBackOrNextClickHandler(event, "/inquiry/input/02/?move=next");
    });

    // 「郵便番号」の左側の項目にフォーカスをセットする
    $("#zipcode1").focus().select();
});

input02.html を修正する

「次へ」ボタンはいつでも押せるようにします。src/main/resources/templates/web/inquiry/input02.html を以下のように変更します。

            <div class="text-center">
              <br/>
              <p class="text-primary text-bold">※電話番号とメールアドレスのいずれか一方は必ず入力してください。</p><br/>
              <button class="btn bg-blue js-btn-back"><i class="fa fa-arrow-left"></i> 前の画面へ戻る</button>
              <button class="btn bg-green js-btn-next"><i class="fa fa-arrow-right"></i> 次へ</button>
            </div>
  • <button class="btn bg-green js-btn-next" disabled><button class="btn bg-green js-btn-next"> へ変更します。

動作確認

npm run springboot を実行し、Tomcat を起動して http://localhost:9080/inquiry/input/02/ にアクセスすると入力画面2が表示されます。

f:id:ksby:20170920224201p:plain

何も入力せずに Tab キーでカーソルを「前の画面へ戻る」ボタンまで移動すると赤色になってエラーメッセージが表示されます。

f:id:ksby:20170920224341p:plain

F5 キーを押してリロードした後「次へ」ボタンをいきなりクリックすると全ての項目がエラーになり、カーソルは一番最初のエラー項目である「郵便番号」の左側の項目へ移動します。また入力画面3へは遷移しません。今は「前の画面へ戻る」ボタンをクリックしても同じように全ての項目が赤色になり、画面遷移はしません。

f:id:ksby:20170920224505p:plain

F5 キーを押してリロードした後、全ての項目にエラーにならないようデータを入力すると、全ての項目が緑色になります。

f:id:ksby:20170920225837p:plain

「次へ」ボタンをクリックすると入力画面3へ遷移します。

f:id:ksby:20170920230125p:plain

F5 キーを押してリロードした後、電話番号とメールアドレスに “電話番号とメールアドレスのいずれか一方を入力してください” のエラーメッセージが表示されている状態にしてから、

f:id:ksby:20170920230726p:plain

電話番号を入力すると、入力されている値に従ったエラーメッセージが表示されます。電話番号が入力されていればメールアドレスは入力不要なので緑色になります。

f:id:ksby:20170920230848p:plain f:id:ksby:20170920230957p:plain f:id:ksby:20170920231134p:plain

電話番号には何も入力せずにメールアドレスを入力すると、電話番号もメールアドレスもどちらも緑色になります。

f:id:ksby:20170920231338p:plain

メールアドレスを入力して、かつ電話番号にエラーのある値を入力すると、電話番号だけエラーメッセージが表示されます。

f:id:ksby:20170921063058p:plain

問題なく動作しているようです。

次回は。。。

引き続き Javascript の処理を実装します。

今回実装しなかった以下の2つと、

  • メールアドレスチェック
  • 「前の画面へ戻る」ボタンがクリックされた時は入力されているものだけ入力チェックする

実装していて気づいた以下の1点の予定です。

  • Form.js のメソッド内の処理で Array.reduce に変更可能なものは変更する

履歴

2017/09/21
初版発行。