かんがるーさんの日記

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

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

概要

記事一覧はこちらです。

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

  • 今回の手順で確認できるのは以下の内容です。
    • 入力画面2の作成
    • 前回に引き続き Javascript の処理を実装します。以下の処理を実装します。
      • メールアドレスチェック
      • 「前の画面へ戻る」ボタンがクリックされた時は入力されているものだけ入力チェックする
    • Form.js のメソッド内の処理で Array.reduce に変更可能なものは変更しようと思いましたが、forEach 内で if 文で分岐して長めの処理にしていると Array.reduce に変更するメリットがなさそう(ソースが見にくくなりそう)に思えたので、止めます。

参照したサイト・書籍

  1. JavaScriptあれこれ/正規表現パターン
    http://winter-tail.sakura.ne.jp/pukiwiki/index.php?JavaScript%A4%A2%A4%EC%A4%B3%A4%EC%2F%C0%B5%B5%AC%C9%BD%B8%BD%A5%D1%A5%BF%A1%BC%A5%F3#v919ec2e

目次

  1. メールアドレスチェックを実装する
  2. 「前の画面へ戻る」ボタンがクリックされた時は入力されているものだけ入力チェックする
  3. 次回は。。。

手順

メールアドレスチェックを実装する

正しいメールアドレスのチェック方法 の記事を参考に、以下2つのチェックのみ行うようにします。

  • @で分割して要素数が2でなければエラー
  • 分割後の各要素に空白、制御文字、非ASCII文字が入っていたらエラー

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

    checkRegexp: function (form, idFormGroup, idList, pattern, errmsg) {
        var isValid = validateRegexp(idList, pattern);
        setSuccessOrError(form, idFormGroup, errmsg, isValid);
    },

    /**
     * メールアドレスチェック用関数
     * @param {Form} form - Form オブジェクト
     * @param {string} idFormGroup - Validation の SUCCESS/ERROR の結果を反映する要素の id
     * @param {string} id - チェックを行う要素の id
     * @param {string} errmsg - チェックエラー時に表示するエラーメッセージ
     */
    checkEmail: function (form, idFormGroup, id, errmsg) {
        // @で分割して要素数が2つかどうかチェックする
        var elements = $(id).val().split("@");
        var isValid = (elements.length === 2);

        // 1つ目及び2つ目の要素に空白、制御文字、非ASCII文字が含まれていないかチェックする
        if (isValid === true) {
            isValid = elements.reduce(function (p, element) {
                return p && element.match(/^[\x21-\x7E]+$/);
            }, true);
        }

        setSuccessOrError(form, idFormGroup, errmsg, isValid);
    }
  • checkEmail 関数を追加します。

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

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);
            var errmsg = "";

            // 「電話番号」に1つでも値が入力されていたら入力チェックする
            if (form.isAnyNotEmpty(telIdList)) {
                try {
                    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桁の数字を入力してください");
                } catch (e) {
                    errmsg = e.message;
                }
            }

            // 「メールアドレス」が入力されていたら入力チェックする
            if (form.isAnyNotEmpty(emailIdList)) {
                try {
                    emailIdList.forEach(function (id) {
                        validator.checkEmail(form, emailIdFormGroup, id, "メールアドレスを入力してください");
                    })
                } catch (e) {
                    errmsg = e.message;
                }
            }

            // 入力チェックエラーが発生している場合には Error オブジェクトを throw する
            if (errmsg !== "") {
                throw new Error(errmsg);
            }
        }
    };

    form.convertAndValidate(form, event, telIdFormGroup, idList,
        function () {
            converter.convertHanAlphaNumeric(telIdList);
        },
        validateFunction
    );
    form.convertAndValidate(form, event, emailIdFormGroup, idList,
        function () {
            converter.convertHanAlphaNumeric(emailIdList);
        },
        validateFunction
    );
};
  • telAndEmailValidator 関数内の validateFunction 関数の以下の点を変更します。
    • var errmsg = ""; を追加します。
    • if (form.isAnyNotEmpty(telIdList)) { ... } 内の処理を try { ... } catch (e) { errmsg = e.message; } で囲みます。
    • if (form.isAnyNotEmpty(emailIdList)) { ... } の処理を追加します。
    • if (errmsg !== "") { throw new Error(errmsg); } を追加します。

動作確認してみます。

test@sample.co.jp はチェックOKとなり、

f:id:ksby:20170927013745p:plain

@ が2つあったり、スペースがあるとエラーになります。

f:id:ksby:20170927014004p:plain f:id:ksby:20170927014103p:plain f:id:ksby:20170927014204p:plain

電話番号を入力してメールアドレスが未入力の時は、チェックOKになります。

f:id:ksby:20170927014429p:plain

「前の画面へ戻る」ボタンがクリックされた時は入力されているものだけ入力チェックする

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

  • checkRequired 関数以外は、値が空の場合に入力チェックを行わないように変更します。
  • ignoreCheckRequired プロパティを追加して、true の場合には checkRequired 関数何もしないようにします。
"use strict";

module.exports = {

    ignoreCheckRequired: false,

    /**
     * 初期状態に戻す
     */
    reset: function () {
        this.ignoreCheckRequired = false;
    },

    /**
     * 必須チェック用関数
     * @param {Form} form - Form オブジェクト
     * @param {string} idFormGroup - Validation の SUCCESS/ERROR の結果を反映する要素の id
     * @param {Array} idList - チェックを行う要素の id の配列
     * @param {string} errmsg - チェックエラー時に表示するエラーメッセージ
     */
    checkRequired: function (form, idFormGroup, idList, errmsg) {
        if (this.ignoreCheckRequired === true) return;
        var isValid = !form.isAnyEmpty(idList);
        setSuccessOrError(form, idFormGroup, errmsg, isValid);
    },

    /**
     * ひらがなチェック用関数
     * @param {Form} form - Form オブジェクト
     * @param {string} idFormGroup - Validation の SUCCESS/ERROR の結果を反映する要素の id
     * @param {Array} idList - チェックを行う要素の id の配列
     * @param {string} errmsg - チェックエラー時に表示するエラーメッセージ
     */
    checkHiragana: function (form, idFormGroup, idList, errmsg) {
        if (form.isAllEmpty(idList)) return;
        var isValid = validateRegexp(idList, "^[\u3041-\u3096]+$");
        setSuccessOrError(form, idFormGroup, errmsg, isValid);
    },

    /**
     * 正規表現チェック用関数
     * @param {Form} form - Form オブジェクト
     * @param {string} idFormGroup - Validation の SUCCESS/ERROR の結果を反映する要素の id
     * @param {Array} idList - チェックを行う要素の id の配列
     * @param {string} pattern - チェックで使用する正規表現のパターン文字列
     * @param {string} errmsg - チェックエラー時に表示するエラーメッセージ
     */
    checkRegexp: function (form, idFormGroup, idList, pattern, errmsg) {
        if (form.isAllEmpty(idList)) return;
        var isValid = validateRegexp(idList, pattern);
        setSuccessOrError(form, idFormGroup, errmsg, isValid);
    },

    /**
     * メールアドレスチェック用関数
     * @param {Form} form - Form オブジェクト
     * @param {string} idFormGroup - Validation の SUCCESS/ERROR の結果を反映する要素の id
     * @param {string} id - チェックを行う要素の id
     * @param {string} errmsg - チェックエラー時に表示するエラーメッセージ
     */
    checkEmail: function (form, idFormGroup, id, errmsg) {
        // 値が入力されていなければチェックしない
        if ($(id).val() === "") return;

        // @で分割して要素数が2つかどうかチェックする
        var elements = $(id).val().split("@");
        var isValid = (elements.length === 2);

        // 1つ目及び2つ目の要素に空白、制御文字、非ASCII文字が含まれていないかチェックする
        if (isValid === true) {
            isValid = elements.reduce(function (p, element) {
                return p && element.match(/^[\x21-\x7E]+$/);
            }, true);
        }

        setSuccessOrError(form, idFormGroup, errmsg, isValid);
    }

};

/**
 * 正規表現チェック用共通関数
 * @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);
}

/**
 *
 * @param {Form} form - Form オブジェクト
 * @param {string} idFormGroup - Validation の SUCCESS/ERROR の結果を反映する要素の id
 * @param {string} errmsg - チェックエラー時に表示するエラーメッセージ
 * @param {boolean} isValid - チェック結果
 */
function setSuccessOrError(form, idFormGroup, errmsg, isValid) {
    if (isValid) {
        form.setSuccess(idFormGroup);
    } else {
        form.setError(idFormGroup, errmsg);
        throw new Error(errmsg);
    }
}
  • ignoreCheckRequired: false を追加します。
  • reset 関数を追加します。
  • checkRequired 関数内に if (this.ignoreCheckRequired === true) return; を追加します。
  • checkHiragana 関数と checkRegexp 関数内に if (form.isAllEmpty(idList)) return; を追加します。
  • checkEmail 関数内に if ($(id).val() === "") return; を追加します。

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

var telAndEmailValidator = function (event) {
    ..........

    var validateFunction = function () {
        if (validator.ignoreCheckRequired && form.isAllEmpty(idList)) {
            return;
        }

        ..........
    };

    ..........
};

..........

var btnBackOrNextClickHandler = function (event, url, ignoreCheckRequired) {
    // 全ての入力チェックを実行する
    try {
        validator.ignoreCheckRequired = ignoreCheckRequired;
        executeAllValidator(event);
    } finally {
        validator.reset();
    }
    ..........
};

$(document).ready(function () {
    ..........

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

    ..........
});
  • telAndEmailValidator 関数の中で定義している validateFunction 関数内に if (validator.ignoreCheckRequired && form.isAllEmpty(idList)) { return; } を追加します。
  • btnBackOrNextClickHandler 関数の以下の点を変更します。
    • 引数に ignoreCheckRequired を追加します。
    • executeAllValidator(event); の前に validator.ignoreCheckRequired = ignoreCheckRequired; を呼び出し、処理した後に validator.reset(); が呼び出されるようにします。
  • $(document).ready(function () { ... } 内で btnBackOrNextClickHandler 関数を呼び出しているところで、$(".js-btn-back") の場合には第3引数に true を、$(".js-btn-next") の場合には false を渡すようにします。

動作確認してみます。

入力画面1に入力して「次へ」ボタンをクリックします。

f:id:ksby:20170927201508p:plain

入力画面2が表示されます。

f:id:ksby:20170927201732p:plain

何も入力せずに「次へ」ボタンをクリックすると、必須チェックが実行されてエラーメッセージが表示されます。

f:id:ksby:20170927201900p:plain

「前の画面へ戻る」ボタンをクリックすると、必須チェックは実行されず入力画面1へ戻ります。

f:id:ksby:20170927202300p:plain

「次へ」ボタンを押して入力画面2に遷移した後、今度は「電話番号」の一番左側の項目に 3 とだけ入力してから「前の画面へ戻る」ボタンをクリックすると、入力チェックエラーが発生して入力画面1には戻りません。

f:id:ksby:20170928035031p:plain

3 を消して「前の画面へ戻る」ボタンをクリックすると、入力画面1へ戻ります。。。と思いましたが、戻らずに「電話番号とメールアドレスのいずれか一方を入力してください」のエラーメッセージが表示されました?

f:id:ksby:20170928034935p:plain

調べてみると「前の画面へ戻る」ボタンをクリックした時の処理は実行されておらず、フォーカスが外れたことで入力チェックが実行されていたためでした。「前の画面へ戻る」ボタンを押した時に全ての項目にフォーカスがセットされたことにしているので、入力チェックが実行されてしまうのが原因でした。

「前の画面へ戻る」ボタンが押された時は入力チェックの時だけフォーカスがセットされたことにして、チェック終了後に元に戻すことにします。

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

..........

/**
 * form.idList にセットされている id を全て form.focused にセットして
 * focused イベントが発生したことにする
 * @param {Form} form - Form オブジェクト
 */
Form.prototype.forceAllFocused = function (form) {
    form.setFocusedFromList(form, form.idList);
};

/**
 * form.focused の値をバックアップする
 * @param {Form} form - Form オブジェクト
 */
Form.prototype.backupFocusedState = function (form) {
    form.backupFocused = form.focused.concat();
};

/**
 * form.focused の値を form.backupFocusedState を呼び出した時の状態に戻す
 * @param {Form} form - Form オブジェクト
 */
Form.prototype.restoreFocusedState = function (form) {
    form.focused = form.backupFocused.concat();
};

..........
  • backupFocusedState 関数と restoreFocusedState 関数を追加します。

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

var btnBackOrNextClickHandler = function (event, url, ignoreCheckRequired) {
    // 全ての入力チェックを実行する
    try {
        if (ignoreCheckRequired) {
            validator.ignoreCheckRequired = ignoreCheckRequired;
            form.backupFocusedState(form);
        }
        executeAllValidator(event);
    } finally {
        if (ignoreCheckRequired) {
            validator.reset();
            form.restoreFocusedState(form);
        }
    }
    ..........
  • form.backupFocusedState(form);form.restoreFocusedState(form); を追加します。
  • また executeAllValidator(event); の前後の処理を if (ignoreCheckRequired) { ... } で囲むようにします。

今度は入力画面2で「電話番号」の一番左側の項目に 3 を入力して「前の画面へ戻る」ボタンをクリックするとエラーメッセージが表示されますが、

f:id:ksby:20170928040314p:plain

電話番号をクリアして「前の画面へ戻る」ボタンをクリックすると入力画面1へ戻るようになりました。

f:id:ksby:20170928040540p:plain

次回は。。。

サーバ側の処理を実装します。

履歴

2017/09/28
初版発行。

IntelliJ IDEA を 2017.2.3 → 2017.2.4 へバージョンアップ

IntelliJ IDEA を 2017.2.3 → 2017.2.4 へバージョンアップする

IntelliJ IDEA の 2017.2.4 がリリースされたのでバージョンアップします。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates…」を選択します。

  2. IDE and Plugin Updates」ダイアログが表示されます。左下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。

    f:id:ksby:20170922003945p:plain

  3. Plugin の update も表示されました。「Error-prone Compiler Integration」はバージョンアップすると動かなくなりますので、これだけチェックを外して「Update and Restart」ボタンをクリックします。

    f:id:ksby:20170922004128p:plain

  4. Patch がダウンロードされて IntelliJ IDEA が再起動します。

  5. IntelliJ IDEA が起動すると画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

    f:id:ksby:20170922005039p:plain

  6. IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2017.2.4 へバージョンアップされていることを確認します。

  7. Gradle Tool Window のツリーを見ると「Tasks」の下に「other」しかない状態になっているので、左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

    f:id:ksby:20170922005418p:plain

  8. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認しようとしたら、Rebuild Project で以下の画像のエラーが出ました。

    f:id:ksby:20170922010002p:plain

    エラーメッセージの途中に com.haskforce.jps.model... と出力されているので、どうも Haskell のためにインストールしている HaskForce Plugin が原因のようです。

    HaskForce Plugin をアンインストールしてから clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると、今度は “BUILD SUCCESSFUL” のメッセージが出力されました。

    f:id:ksby:20170922011052p:plain

  9. Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run ‘All Tests’ with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20170922013832p:plain

ちなみに HaskForce Plugin は Scala Plugin をインストールしたら再び使えるようになりました。

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
初版発行。

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

概要

記事一覧はこちらです。

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

  • 今回の手順で確認できるのは以下の内容です。
    • 入力画面1の作成
    • サーバ側のテストを作成します。

参照したサイト・書籍

  1. Learning Ant path style
    https://stackoverflow.com/questions/2952196/learning-ant-path-style

目次

  1. InquiryInput01Form クラスのテストを作成する
  2. ksbysample-webapp-lending から TestHelper, HtmlResultMatchers クラスをコピーする
  3. HtmlResultMatchers クラスに val メソッドを追加する
  4. テストデータを用意する
  5. InquiryInputController クラスのテストを作成する
  6. 次回は。。。

手順

InquiryInput01Form クラスのテストを作成する

src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput01Form.java で Ctrl+Shift+T を押して「Create Test」ダイアログを表示してから、以下の画像の値にした後「OK」ボタンをクリックします。

f:id:ksby:20170909065713p:plain

src/test/groovy/ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput01FormTest.groovy が新規作成されるので、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.web.inquiry.form

import spock.lang.Specification
import spock.lang.Unroll

import javax.validation.ConstraintViolation
import javax.validation.Validation

class InquiryInput01FormTest extends Specification {

    def validator
    def inquiryInput01Form

    def setup() {
        validator = Validation.buildDefaultValidatorFactory().getValidator()
        inquiryInput01Form = new InquiryInput01Form(
                lastname: "田中"
                , firstname: "太郎"
                , lastkana: "たなか"
                , firstkana: "たろう"
                , sex: "1"
                , age: "30"
                , job: "1")
    }

    def "placeholder で表示している例の Bean Validation のテスト"() {
        when:
        Set<ConstraintViolation<InquiryInput01Form>> constraintViolations =
                validator.validate(inquiryInput01Form)

        then:
        constraintViolations.size() == 0
    }

    @Unroll
    def "lastname の Bean Validation の Bean Validation のテスト(#lastname --> #size)"() {
        setup:
        inquiryInput01Form.lastname = lastname
        Set<ConstraintViolation<InquiryInput01Form>> constraintViolations =
                validator.validate(inquiryInput01Form)

        expect:
        constraintViolations.size() == size

        where:
        lastname || size
        ""       || 2
        "a"      || 0
        "a" * 20 || 0
        "a" * 21 || 1
    }

    @Unroll
    def "firstname の Bean Validation のテスト(#firstname --> #size)"() {
        setup:
        inquiryInput01Form.firstname = firstname
        Set<ConstraintViolation<InquiryInput01Form>> constraintViolations =
                validator.validate(inquiryInput01Form)

        expect:
        constraintViolations.size() == size

        where:
        firstname || size
        ""        || 2
        "a"       || 0
        "a" * 20  || 0
        "a" * 21  || 1
    }

    @Unroll
    def "lastkana の Bean Validation のテスト(#lastkana --> #size)"() {
        setup:
        inquiryInput01Form.lastkana = lastkana
        Set<ConstraintViolation<InquiryInput01Form>> constraintViolations =
                validator.validate(inquiryInput01Form)

        expect:
        constraintViolations.size() == size

        where:
        lastkana || size
        ""       || 3
        "a"      || 1
        "a" * 20 || 1
        "a" * 21 || 2
        "あ"      || 0
        "あ" * 20 || 0
        "あ" * 21 || 1
        "ア"      || 1
        "A"      || 1
        "1"      || 1
        "あa"     || 1
    }

    @Unroll
    def "firstkana の Bean Validation のテスト(#firstkana --> #size)"() {
        setup:
        inquiryInput01Form.firstkana = firstkana
        Set<ConstraintViolation<InquiryInput01Form>> constraintViolations =
                validator.validate(inquiryInput01Form)

        expect:
        constraintViolations.size() == size

        where:
        firstkana || size
        ""        || 3
        "a"       || 1
        "a" * 20  || 1
        "a" * 21  || 2
        "あ"       || 0
        "あ" * 20  || 0
        "あ" * 21  || 1
        "ア"       || 1
        "A"       || 1
        "1"       || 1
        "あa"      || 1
    }

    @Unroll
    def "sex の Bean Validation のテスト(#sex --> #size)"() {
        setup:
        inquiryInput01Form.sex = sex
        Set<ConstraintViolation<InquiryInput01Form>> constraintViolations =
                validator.validate(inquiryInput01Form)

        expect:
        constraintViolations.size() == size

        where:
        sex || size
        ""  || 1
        "0" || 1
        "1" || 0
        "2" || 0
        "3" || 1
    }

    @Unroll
    def "age の Bean Validation のテスト(#age --> #size)"() {
        setup:
        inquiryInput01Form.age = age
        Set<ConstraintViolation<InquiryInput01Form>> constraintViolations =
                validator.validate(inquiryInput01Form)

        expect:
        constraintViolations.size() == size

        where:
        age    || size
        ""     || 2
        "0"    || 0
        "1"    || 0
        "999"  || 0
        "1000" || 1
        "0.1"  || 1
    }

    @Unroll
    def "job の Bean Validation のテスト(#job --> #size)"() {
        setup:
        inquiryInput01Form.job = job
        Set<ConstraintViolation<InquiryInput01Form>> constraintViolations =
                validator.validate(inquiryInput01Form)

        expect:
        constraintViolations.size() == size

        where:
        job || size
        ""  || 0
        "0" || 1
        "1" || 0
        "2" || 0
        "3" || 0
        "4" || 1
    }

}

テストを実行して全て成功することを確認します。

f:id:ksby:20170909070152p:plain

ksbysample-webapp-lending から TestHelper, HtmlResultMatchers クラスをコピーする

テストに使用したいので、ksbysample-webapp-lending から TestHelper, HtmlResultMatchers クラスをコピーします。

src/test/java の下に ksbysample.common.test.helper パッケージを作成します。

src/test/java/ksbysample/common/test/helper の下に TestHelper.java をコピーします。

src/test/java/ksbysample/common/test の下に matcher パッケージを作成します。

src/test/java/ksbysample/common/test/matcher の下に HtmlResultMatchers.java をコピーします。

HtmlResultMatchers クラスは jsoup が必要なので導入します。build.gradle の以下の点を変更します。

dependencies {
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    ..........
    testCompile("com.google.code.findbugs:jsr305:3.0.2")
    testCompile("org.jsoup:jsoup:1.10.3")

    ..........
}
  • testCompile("org.jsoup:jsoup:1.10.3") を追加します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

HtmlResultMatchers クラスに val メソッドを追加する

今回のテストでは value 属性の値をチェックしたいのですが、今の HtmlResultMatchers クラスには value 属性を検証するためのメソッドが用意されていないので追加します。

src/test/java/ksbysample/common/test/matcher/HtmlResultMatchers.java の以下の点を変更します。

public class HtmlResultMatchers {

    ..........

    public ResultMatcher text(final String expectedText) {
        return mvcResult -> assertThat(selectFirst(mvcResult).text(), is(expectedText));
    }

    /**
     * HTML 内の cssQuery で指定された Element の value 属性の値を取得し、
     * 引数で渡された文字列と同じかチェックする
     *
     * @param expectedText 文字列の期待値
     * @return {@link ResultMatcher}
     */
    public ResultMatcher val(final String expectedText) {
        return mvcResult -> assertThat(selectFirst(mvcResult).val(), is(expectedText));
    }

    ..........

}
  • val メソッドを追加します。

テストデータを用意する

src/test/resources の下に ksbysample/webapp/bootnpmgeb/web/inquiry ディレクトリを新規作成します。

src/test/resources/ksbysample/webapp/bootnpmgeb/web/inquiry の下に InquiryInput01Form_001.yaml を新規作成し、以下の内容を記述します。

!!ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput01Form
# 最大文字数のデータ、選択肢の場合には一番最後のデータをセットする
lastname: 12345678901234567890
firstname: 12345678901234567890
lastkana: あいうえおかきくけこさしすせそたちつてと
firstkana: なにぬねのはひふへほまみむめもあいうえお
sex: 2
age: 999
job: 3

InquiryInputController クラスのテストを作成する

src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryInputController.java で Ctrl+Shift+T を押して「Create Test」ダイアログを表示してから、以下の画像の値にした後「OK」ボタンをクリックします。

f:id:ksby:20170913001740p:plain

MockMvc を使用するので Groovy + Spock ではなく Groovy + JUnit4 で作成するのですが、「Create Test」ダイアログで JUnit4 を選択すると拡張子が .java になってしまうので、Spock を選択してファイルを生成してから中身を Groovy + JUnit4 に変更します。

src/test/groovy/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryInputControllerTest.groovy が新規作成されるので、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.web.inquiry

import ksbysample.common.test.helper.TestHelper
import ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput01Form
import org.junit.Before
import org.junit.Test
import org.junit.experimental.runners.Enclosed
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.mock.web.MockHttpSession
import org.springframework.test.context.junit4.SpringRunner
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.MvcResult
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.context.WebApplicationContext
import org.yaml.snakeyaml.Yaml

import static ksbysample.common.test.matcher.HtmlResultMatchers.html
import static org.assertj.core.api.Assertions.catchThrowable
import static org.assertj.core.api.AssertionsForClassTypes.assertThat
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

@RunWith(Enclosed)
class InquiryInputControllerTest {

    @RunWith(SpringRunner)
    @SpringBootTest
    static class 入力画面1のテスト {
        private InquiryInput01Form inquiryInput01Form_001 =
                (InquiryInput01Form) new Yaml().load(getClass().getResourceAsStream("InquiryInput01Form_001.yaml"))

        @Autowired
        private WebApplicationContext context

        MockMvc mockMvc

        @Before
        void setup() {
            mockMvc = MockMvcBuilders.webAppContextSetup(context)
                    .build()
        }

        @Test
        void "初期表示時は画面の項目には何もセットされない"() {
            expect:
            mockMvc.perform(get("/inquiry/input/01"))
                    .andExpect(status().isOk())
                    .andExpect(html("#lastname").val(""))
                    .andExpect(html("#firstname").val(""))
                    .andExpect(html("#lastkana").val(""))
                    .andExpect(html("#firstkana").val(""))
                    .andExpect(html("input[name='sex'][checked='checked']").notExists())
                    .andExpect(html("#age").val(""))
                    .andExpect(html("select[name=job] option[selected]").notExists())
        }

        @Test
        void "項目全てに入力して入力画面2へ遷移してから戻ると以前入力したデータがセットされて表示される"() {
            expect: "項目全てに入力して「次へ」ボタンをクリックする"
            MvcResult result = mockMvc.perform(
                    TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/02"))
                    .andReturn()
            MockHttpSession session = result.getRequest().getSession()

            and: "再び入力画面1を表示する"
            mockMvc.perform(get("/inquiry/input/01").session(session))
                    .andExpect(status().isOk())
                    .andExpect(html("#lastname").val(inquiryInput01Form_001.lastname))
                    .andExpect(html("#firstname").val(inquiryInput01Form_001.firstname))
                    .andExpect(html("#lastkana").val(inquiryInput01Form_001.lastkana))
                    .andExpect(html("#firstkana").val(inquiryInput01Form_001.firstkana))
                    .andExpect(html("input[name='sex'][checked='checked']").val(inquiryInput01Form_001.sex))
                    .andExpect(html("#age").val(inquiryInput01Form_001.age))
                    .andExpect(html("select[name=job] option[selected]").val(inquiryInput01Form_001.job))
        }

        @Test
        void "入力チェックエラーのあるデータで入力画面2へ遷移しようとするとIllegalArgumentExceptionが発生する"() {
            setup: "入力チェックエラーになるデータを用意する"
            inquiryInput01Form_001.lastname = "x" * 21

            expect: "入力画面1の「次へ」ボタンをクリックする"
            Throwable thrown = catchThrowable({
                mockMvc.perform(
                        TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001))
                        .andExpect(status().isOk())
            })
            assertThat(thrown.cause).isInstanceOf(IllegalArgumentException)
        }

    }

}

テストを実行して全て成功することを確認します。

f:id:ksby:20170913002519p:plain

次回は。。。

入力画面2を作成します。

履歴

2017/09/13
初版発行。

IntelliJ IDEA を 2017.2.2 → 2017.2.3 へバージョンアップ

IntelliJ IDEA を 2017.2.2 → 2017.2.3 へバージョンアップする

IntelliJ IDEA の 2017.2.3 がリリースされたのでバージョンアップします。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates…」を選択します。

  2. IDE and Plugin Updates」ダイアログが表示されます。左下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。

    f:id:ksby:20170908071144p:plain

  3. Plugin の update も表示されました。「Error-prone Compiler Integration」はバージョンアップすると動かなくなりますので、これだけチェックを外して「Update and Restart」ボタンをクリックします。

    f:id:ksby:20170908071303p:plain

  4. Patch がダウンロードされて IntelliJ IDEA が再起動します。

  5. IntelliJ IDEA が起動すると画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

    f:id:ksby:20170908071819p:plain

  6. IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2017.2.3 へバージョンアップされていることを確認します。

  7. Gradle Tool Window のツリーを見ると「Tasks」の下に「other」しかない状態になっているので、左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

    f:id:ksby:20170908072009p:plain

  8. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20170908072949p:plain

  9. Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run ‘All Tests’ with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20170908073911p:plain

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

概要

記事一覧はこちらです。

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

  • 今回の手順で確認できるのは以下の内容です。
    • 入力画面1の作成
    • サーバ側の処理を実装します。また実装後に見つかった HTML, Javascript 側の問題点も修正します。

参照したサイト・書籍

  1. ModelMapper
    http://modelmapper.org/

  2. Spring Boot ModelMapper Starter
    https://github.com/rozidan/modelmapper-spring-boot-starter

目次

  1. Form クラスを作成する
  2. セッション保存用のクラスを作成する
  3. input01.html, input01.js を変更する
  4. ModdelMapper をインストールする
  5. 画面表示時と「次へ」ボタンクリック時の処理を実装する
  6. 動作確認
  7. HTML5 の autofocus 属性では項目にデータが入っている時に選択状態にしてくれない問題を修正する(Firefox の autokana の問題も「お名前(漢字)」の「名」が文字化けする問題もこれで解決!)
  8. 入力チェック前の状態ではなく入力チェックした後の状態で表示されるようにする
  9. autokana で自動入力すると maxlength 以上の文字列が入力される問題を修正する
  10. 次回は。。。
  11. メモ書き
    1. IntelliJ IDEA の正規表現チェック機能を使ってみる

手順

Form クラスを作成する

src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry の下に form パッケージを新規作成します。

src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form の下に InquiryInput01Form.java を新規作成し、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.web.inquiry.form;

import ksbysample.webapp.bootnpmgeb.values.JobValues;
import ksbysample.webapp.bootnpmgeb.values.SexValues;
import ksbysample.webapp.bootnpmgeb.values.validation.ValuesEnum;
import lombok.Data;
import org.hibernate.validator.constraints.NotEmpty;

import javax.validation.constraints.Digits;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.io.Serializable;

/**
 * 入力画面1用 Form クラス
 */
@Data
public class InquiryInput01Form implements Serializable {

    private static final long serialVersionUID = -7017712118767300185L;

    @NotEmpty
    @Size(min = 1, max = 20)
    private String lastname;

    @NotEmpty
    @Size(min = 1, max = 20)
    private String firstname;

    @NotEmpty
    @Size(min = 1, max = 20)
    @Pattern(regexp = "^[\\u3041-\\u3096]+$")
    private String lastkana;

    @NotEmpty
    @Size(min = 1, max = 20)
    @Pattern(regexp = "^[\\u3041-\\u3096]+$")
    private String firstkana;

    @ValuesEnum(enumClass = SexValues.class)
    private String sex;

    @NotEmpty
    @Digits(integer = 3, fraction = 0)
    private String age;

    @ValuesEnum(enumClass = JobValues.class, allowEmpty = true)
    private String job;

}

セッション保存用のクラスを作成する

src/main/java/ksbysample/webapp/bootnpmgeb の下に session パッケージを新規作成します。

src/main/java/ksbysample/webapp/bootnpmgeb/session の下に SessionData.java を新規作成し、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.session;

import ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput01Form;
import lombok.Data;

import java.io.Serializable;

/**
 * セッションデータ用クラス
 */
@Data
public class SessionData implements Serializable {

    private static final long serialVersionUID = -2673191456750655164L;

    private InquiryInput01Form inquiryInput01Form;

}

input01.html, input01.js を変更する

src/main/resources/templates/web/inquiry/input01.html の以下の点を変更します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{web/common/fragments :: common_header(~{::title}, ~{::link}, ~{::style})}">
  <title>入力フォーム - 入力画面1</title>
</head>

<body class="skin-blue layout-top-nav">
<div class="wrapper">

  <!-- Content Wrapper. Contains page content -->
  <div class="content-wrapper">
    <!-- Content Header (Page header) -->
    <section class="content-header">
      <h1>
        入力画面1
      </h1>
    </section>

    <!-- Main content -->
    <section class="content">
      <div class="row">
        <div class="col-xs-12">
          <!--/*@thymesVar id="inquiryInput01Form" type="ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput01Form"*/-->
          <form id="inquiryInput01Form" class="form-horizontal" method="post" action=""
                th:action="@{/inquiry/input/01/}"
                th:object="${inquiryInput01Form}">
            <!-- お名前(漢字) -->
            <div class="form-group" id="form-group-name">
              <div class="control-label col-sm-2">
                <label class="float-label">お名前(漢字)</label>
                <div class="label label-required">必須</div>
              </div>
              <div class="col-sm-10">
                <div class="row">
                  <div class="col-sm-10">
                    <input type="text" name="lastname" id="lastname" class="form-control form-control-inline"
                           style="width: 150px;" maxlength="20" value="" placeholder="例)田中" autofocus
                           th:field="*{lastname}"/>
                    <input type="text" name="firstname" id="firstname" class="form-control form-control-inline"
                           style="width: 150px;" maxlength="20" value="" placeholder="例)太郎"
                           th:field="*{firstname}"/>
                  </div>
                </div>
                <div class="row hidden js-errmsg">
                  <div class="col-sm-10">
                    <p class="form-control-static text-danger">
                      <small>ここにエラーメッセージを表示します</small>
                    </p>
                  </div>
                </div>
              </div>
            </div>

            <!-- お名前(かな) -->
            <div class="form-group" id="form-group-kana">
              <div class="control-label col-sm-2">
                <label class="float-label">お名前(かな)</label>
                <div class="label label-required">必須</div>
              </div>
              <div class="col-sm-10">
                <div class="row">
                  <div class="col-sm-10">
                    <input type="text" name="lastkana" id="lastkana" class="form-control form-control-inline"
                           style="width: 150px;" maxlength="20" value="" placeholder="例)たなか"
                           th:field="*{lastkana}"/>
                    <input type="text" name="firstkana" id="firstkana" class="form-control form-control-inline"
                           style="width: 150px;" maxlength="20" value="" placeholder="例)たろう"
                           th:field="*{firstkana}"/>
                  </div>
                </div>
                <div class="row hidden js-errmsg">
                  <div class="col-sm-10">
                    <p class="form-control-static text-danger">
                      <small>ここにエラーメッセージを表示します</small>
                    </p>
                  </div>
                </div>
              </div>
            </div>

            <!-- 性別 -->
            <div class="form-group" id="form-group-sex">
              <div class="control-label col-sm-2">
                <label class="float-label">性別</label>
                <div class="label label-required">必須</div>
              </div>
              <div class="col-sm-10">
                <div class="row">
                  <div class="col-sm-10">
                    <div class="radio-inline" th:each="sexValue : ${@vh.values('SexValues')}">
                      <label><input type="radio" name="sex" th:value="${sexValue.value}"
                                    th:text="${sexValue.text}"
                                    th:field="*{sex}"></label>
                    </div>
                  </div>
                </div>
                <div class="row hidden js-errmsg">
                  <div class="col-sm-10">
                    <p class="form-control-static text-danger">
                      <small>ここにエラーメッセージを表示します</small>
                    </p>
                  </div>
                </div>
              </div>
            </div>

            <!-- 年齢 -->
            <div class="form-group" id="form-group-age">
              <div class="control-label col-sm-2">
                <label class="float-label">年齢</label>
                <div class="label label-required">必須</div>
              </div>
              <div class="col-sm-10">
                <div class="row">
                  <div class="col-sm-10">
                    <input type="text" name="age" id="age" class="form-control form-control-inline"
                           style="width: 150px;" maxlength="3" value="" placeholder="例)30"
                           th:field="*{age}"/>
                    <p class="form-control-static-inline"></p>
                  </div>
                </div>
                <div class="row hidden js-errmsg">
                  <div class="col-sm-10">
                    <p class="form-control-static text-danger">
                      <small>ここにエラーメッセージを表示します</small>
                    </p>
                  </div>
                </div>
              </div>
            </div>

            <!-- 職業 -->
            <div class="form-group" id="form-group-job">
              <div class="control-label col-sm-2">
                <label class="float-label">職業</label>
              </div>
              <div class="col-sm-10">
                <div class="row">
                  <div class="col-sm-10">
                    <select name="job" id="job" class="form-control" style="width: 250px;">
                      <th:block th:each="jobValue,iterStat : ${@vh.values('JobValues')}">
                        <option th:if="${iterStat.first}" value="">選択してください</option>
                        <option th:value="${jobValue.value}" th:text="${jobValue.text}" th:field="*{job}">会社員</option>
                      </th:block>
                    </select>
                  </div>
                </div>
                <div class="row hidden js-errmsg">
                  <div class="col-sm-10">
                    <p class="form-control-static text-danger">
                      <small>ここにエラーメッセージを表示します</small>
                    </p>
                  </div>
                </div>
              </div>
            </div>

            <div class="text-center">
              <button class="btn bg-green js-btn-next"><i class="fa fa-arrow-right"></i> 次へ</button>
            </div>
          </form>
        </div>
      </div>
    </section>
    <!-- /.content -->
  </div>
  <!-- /.content-wrapper -->
</div>
<!-- ./wrapper -->

<!-- REQUIRED JS SCRIPTS -->
<script src="/js/inquiry/input01.js"></script>

</body>
</html>
  • form タグの上に <!--/*@thymesVar id="inquiryInput01Form" type="ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput01Form"*/--> を追加します。
  • <form id="input01Form" ...<form id="inquiryInput01Form" ... に変更します。
  • form タグの末尾に th:object="${inquiryInput01Form}" を追加します。
  • 入力/選択項目のタグに th:field="*{...}"(… には入力項目に対応した変数を記述) を追加します。

form タグの id 属性を変更したので src/main/assets/js/inquiry/input01.js の以下の点を変更します。

var btnNextClickHandler = function (event) {
    ..........

    // サーバにリクエストを送信する
    $("#inquiryInput01Form").attr("action", "/inquiry/input/01/?move=next");
    $("#inquiryInput01Form").submit();

    ..........
};
  • $("#input01Form")$("#inquiryInput01Form") に変更します。

ModdelMapper をインストールする

今回 POJO 間のデータコピーには org.springframework.beans.BeanUtils クラスではなく ModelMapper を使用してみます。インストールは ModelMapper を直接インストールするのではなく、Spring Boot の Auto Configuraion を見つけましたので Spring Boot ModelMapper Starter を使用してインストールします。

build.gradle の以下の点を変更します。

dependencies {
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    compile("com.integralblue:log4jdbc-spring-boot-starter:1.0.1")
    compile("org.flywaydb:flyway-core:4.2.0")
    compile("com.h2database:h2:1.4.192")
    compile("com.github.rozidan:modelmapper-spring-boot-starter:1.0.0")
    testCompile("org.dbunit:dbunit:2.5.3")
    testCompile("com.icegreen:greenmail:1.5.5")
    testCompile("org.assertj:assertj-core:3.8.0")
    testCompile("org.spockframework:spock-core:${spockVersion}")
    testCompile("org.spockframework:spock-spring:${spockVersion}")
    testCompile("com.google.code.findbugs:jsr305:3.0.2")

    ..........
}
  • compile("com.github.rozidan:modelmapper-spring-boot-starter:1.0.0") を追加します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

画面表示時と「次へ」ボタンクリック時の処理を実装する

src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryInputController.java の以下の点を変更します。

@Controller
@RequestMapping("/inquiry/input")
@SessionAttributes("sessionData")
public class InquiryInputController {

    ..........

    private final ModelMapper modelMapper;

    /**
     * コンストラクタ
     *
     * @param modelMapper {@link ModelMapper} オブジェクト
     */
    public InquiryInputController(ModelMapper modelMapper) {
        this.modelMapper = modelMapper;
    }

    /**
     * 入力画面1 初期表示処理
     *
     * @return 入力画面1の Thymeleaf テンプレートファイルのパス
     */
    @GetMapping("/01")
    public String input01(InquiryInput01Form inquiryInput01Form
            , SessionData sessionData) {
        // セッションに保存されているデータがある場合にはコピーする
        if (sessionData.getInquiryInput01Form() != null) {
            modelMapper.map(sessionData.getInquiryInput01Form(), inquiryInput01Form);
        }

        return TEMPLATE_INPUT01;
    }

    /**
     * 入力画面1 「次へ」ボタンクリック時の処理
     *
     * @return 入力画面2の URL
     */
    @PostMapping(value = "/01", params = {"move=next"})
    public String input01MoveNext(@Validated InquiryInput01Form inquiryInput01Form
            , BindingResult bindingResult
            , SessionData sessionData
            , UriComponentsBuilder builder) {
        if (bindingResult.hasErrors()) {
            throw new IllegalArgumentException("セットされるはずのないデータがセットされています");
        }

        // 入力されたデータをセッションに保存する
        sessionData.setInquiryInput01Form(inquiryInput01Form);

        return UrlBasedViewResolver.REDIRECT_URL_PREFIX
                + builder.path(UrlConst.URL_INQUIRY_INPUT_02).toUriString();
    }
  • @SessionAttributes("sessionData") を追加します。
  • private final ModelMapper modelMapper; とコンストラクタを追加します。
  • input01 メソッドに上記の引数を追加し、セッションに保存されているデータがある場合にはコピーする処理を追加します。
  • input01MoveNext メソッドに上記の引数を追加し、入力チェック処理と入力されたデータをセッションに保存する処理を追加します。

動作確認

動作確認します。npm run springboot コマンドを実行し Tomcat を起動した後、ブラウザで http://localhost:9080/inquiry/input/01/ にアクセスします。

データを入力してから、

f:id:ksby:20170902211625p:plain

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

f:id:ksby:20170902211829p:plain

「前の画面へ戻る」ボタンをクリックして入力画面1へ戻ると、

f:id:ksby:20170903211022p:plain

入力画面1へ戻りデータが表示されるのですが、以下の問題がありました。

  • 「お名前(漢字)」の「姓」にカーソルが当たるのですが選択状態で表示されません。
  • 入力チェックした後の状態ではなく入力チェック前の状態で表示されています。
  • なぜか「お名前(漢字)」の「名」だけ文字化けしています。何度か試したところ、この現象は IE だけ出て FirefoxChrome では出ず、IE でも最初から出る時と最初は出ずに入力画面2と何度か画面遷移させると出る時がありました。謎です。。。

また上記以外にもいろいろ試していて、以下の問題にも気づきました。

  • Firefox だと一番最初の「お名前(漢字)」の「姓」から1度カーソルを移動しないと autokana が機能しません。
  • 「お名前(漢字)」にかなを20文字入力すると「お名前(かな)」にもかなが20文字入力されるのですが、その後に「お名前(漢字)」にカーソルを移動して末尾の1文字を削除してから1文字追加すると「お名前(かな)」の maxlength は 20 で設定しているのに21文字入力されてしまいます。

あと、「お名前(漢字)」に何も入力せずに「お名前(かな)」を先に入力して、その後に「お名前(漢字)」にカーソルを移動すると「お名前(かな)」に入力していたデータが消えるのですが、これは autokana を入れている場合には正常な動作だと思うのでこのままにします。

HTML5 の autofocus 属性では項目にデータが入っている時に選択状態にしてくれない問題を修正する(Firefox の autokana の問題も「お名前(漢字)」の「名」が文字化けする問題もこれで解決!)

HTML5 の autofocus はフォーカスを移動してくれるだけで、選択状態にはしてくれないようです。FirefoxChrome でもダメでした。

画面初期表示時に Javascript でフォーカスをセット&選択状態にするようにします。

src/main/assets/js/inquiry/input01.js の以下の点を変更します。

$(document).ready(function () {
    ..........

    // 「お名前(漢字)」の「姓」にフォーカスをセットする
    $("#lastname").focus().select();
});
  • $("#lastname").focus().select(); を追加します。

src/main/resources/templates/web/inquiry/input01.html の以下の点を変更します。

                    <input type="text" name="lastname" id="lastname" class="form-control form-control-inline"
                           style="width: 150px;" maxlength="20" value="" placeholder="例)田中"
                           th:field="*{lastname}"/>
  • <input type="text" name="lastname" ... から autofocus を削除します。

動作確認すると今度は入力画面2から入力画面1に戻った時に選択状態になり、ついでに Firefox でカーソルを移動しないと autokana が機能しない問題も解決していました。

f:id:ksby:20170903092237p:plain

さらになぜか「お名前(漢字)」の「名」だけ文字化けする問題も解決していました!(入力画面2と何度か画面遷移させても全く現象が出ませんでした) 最初 HTML5 の autofocus を見た時はこんな便利なものがあるのか、と思ったものでしたが、使えませんでした。残念です。。。

入力チェック前の状態ではなく入力チェックした後の状態で表示されるようにする

セッションにデータが保存されていた場合には、画面表示時に入力チェックが実行されるようにします。

src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput01Form.java の以下の点を変更します。

@Data
public class InquiryInput01Form implements Serializable {

    ..........

    private boolean copiedFromSession = false;

}
  • private boolean copiedFromSession = false; を追加します。セッションからデータをコピーした時に true にします。

src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryInputController.java の以下の点を変更します。

    @GetMapping("/01")
    public String input01(InquiryInput01Form inquiryInput01Form
            , SessionData sessionData) {
        // セッションに保存されているデータがある場合にはコピーする
        if (sessionData.getInquiryInput01Form() != null) {
            modelMapper.map(sessionData.getInquiryInput01Form(), inquiryInput01Form);
            inquiryInput01Form.setCopiedFromSession(true);
        }

        return TEMPLATE_INPUT01;
    }
  • input01 メソッド内に inquiryInput01Form.setCopiedFromSession(true); を追加します。

src/main/resources/templates/web/inquiry/input01.html の以下の点を変更します。

          <form id="inquiryInput01Form" class="form-horizontal" method="post" action=""
                th:action="@{/inquiry/input/01/}"
                th:object="${inquiryInput01Form}">
            <input type="hidden" name="copiedFromSession" id="copiedFromSession" th:value="*{copiedFromSession}" />
  • form タグのすぐ下に <input type="hidden" name="copiedFromSession" id="copiedFromSession" th:value="*{copiedFromSession}" /> を追加します。

src/main/assets/js/inquiry/input01.js の以下の点を変更します。

var executeAllValidator = function (event) {
    form.forceAllFocused(form);
    [
        nameValidator,
        kanaValidator,
        sexValidator,
        ageValidator,
        jobValidator
    ].forEach(function (validator) {
        validator(event);
    });
};

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

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

    // サーバにリクエストを送信する
    $("#inquiryInput01Form").attr("action", "/inquiry/input/01/?move=next");
    $("#inquiryInput01Form").submit();

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

$(document).ready(function (event) {
    // 「お名前(漢字)」が入力された時に、かな文字列を「お名前(かな)」に自動入力されるようにする
    $.fn.autoKana('#lastname', '#lastkana');
    $.fn.autoKana('#firstname', '#firstkana');

    // 入力チェック用の validator 関数をセットする
    $("#lastname")
        .on("blur", nameValidator)
        .on("blur", kanaAutoInputValidator);
    $("#firstname")
        .on("blur", nameValidator)
        .on("blur", kanaAutoInputValidator);
    $("#lastkana").on("blur", kanaValidator);
    $("#firstkana").on("blur", kanaValidator);
    $("input:radio[name='sex']").on("blur", sexValidator);
    $("#age").on("blur", ageValidator);
    $("#job").on("blur", jobValidator);

    // 「次へ」ボタンクリック時の処理をセットする
    $(".js-btn-next").on("click", btnNextClickHandler)

    // 初期画面表示時にセッションに保存されていたデータを表示する場合には
    // 入力チェックを実行して画面の表示を入力チェック後の状態にする
    if ($("#copiedFromSession").val() === "true") {
        executeAllValidator(event);
    }

    // 「お名前(漢字)」の「姓」にフォーカスをセットする
    $("#lastname").focus().select();
});
  • var executeAllValidator = function (event) { ... } を追加します。
  • btnNextClickHandler 関数内で全ての入力チェックを実行していた処理を executeAllValidator に置き換えます。
  • $(document).ready(function () {$(document).ready(function (event) { に変更します。
  • $(document).ready(function (event) { ... } 内に if ($("#copiedFromSession").val() === "true") { ... } を追加し、#copiedFromSession の値が "true" の場合には全ての入力チェックを実行します。

動作確認します。npm run springboot コマンドを停止→実行し Tomcat を再起動した後、http://localhost:9080/inquiry/input/01/ にアクセスしてデータを入力します。

f:id:ksby:20170906064749p:plain

「次へ」ボタンをクリックして入力画面2へ遷移し、

f:id:ksby:20170906064908p:plain

「前の画面へ戻る」ボタンをクリックすると全ての項目が緑で表示されました。

f:id:ksby:20170906065041p:plain

autokana で自動入力すると maxlength 以上の文字列が入力される問題を修正する

keyup イベント発生時に maxlength の文字数を超えていたら超えた分を削除するようにします。

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

function delStringExceedingMaxlength(id) {
    $(id).val($(id).val().substring(0, $(id).attr("maxlength")));
}

$(document).ready(function (event) {
    // 「お名前(漢字)」が入力された時に、かな文字列を「お名前(かな)」に自動入力されるようにする
    $.fn.autoKana('#lastname', '#lastkana');
    $.fn.autoKana('#firstname', '#firstkana');

    // autokana で自動入力されると maxlength の文字数を超える文字が自動入力される場合があるので
    // maxlegnth の文字数を超えた分を削除する
    $("#lastname").on("keyup", function (event) {
        delStringExceedingMaxlength("#lastkana");
    });
    $("#firstname").on("keyup", function (event) {
        delStringExceedingMaxlength("#firstkana");
    });

    // 入力チェック用の validator 関数をセットする
    ..........
  • function delStringExceedingMaxlength(id) { ... } を追加します。
  • $(document).ready(function (event) { ... } 内に以下の2つの処理を追加します。
    • $("#lastname").on("keyup", function (event) { ... }
    • $("#firstname").on("keyup", function (event) { ... }

動作確認します。「お名前(漢字)」に あいうえおかきくけこさしすせそたちつてと を入力すると「お名前(かな)」にも あいうえおかきくけこさしすせそたちつてと が入力されて、

f:id:ksby:20170906233824p:plain

「お名前(漢字)」で末尾の を削除して を入力しても「お名前(かな)」は あいうえおかきくけこさしすせそたちつてと のままでした。

f:id:ksby:20170906234209p:plain

次回は。。。

サーバ側のテストを書きます。入力画面1の実装は次で最後の予定です。

メモ書き

IntelliJ IDEA の正規表現チェック機能を使ってみる

今回 src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput01Form.java に以下の画像の正規表現(ひらがなだけかチェックする)を記述しましたが、

f:id:ksby:20170906235208p:plain

正規表現のところにカーソル移動してから Alt+Enter を押してコンテキストメニューを表示した後、「Check RegExp」を選択すると、

f:id:ksby:20170906235341p:plain

入力された文字が正規表現と一致するのか否かを検証できるダイアログが表示されます。

f:id:ksby:20170906235520p:plain

あいうえお と入力すると右下に Matches! と表示されて入力フィールドも緑色になりますが、

f:id:ksby:20170906235715p:plain

アイウエオ に変えると No match と表示されて赤色に変わります。

f:id:ksby:20170906235919p:plain

確か機能が追加されたのは 2017.2 ではなくもう少し前のはずですが、変更内容をきちんと見ていませんでした。正規表現の動作確認をしたい時はよくあるので、これは嬉しい機能です。

履歴

2017/09/07
初版発行。

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

概要

記事一覧はこちらです。

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

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

参照したサイト・書籍

  1. JQUERY】自動的にカナ入力してくれるAUTOKANA.JSプラグインはいまや必須!
    http://plugmin.co/472/

  2. autokana
    https://github.com/harisenbon/autokana

  3. How to use jQuery-Plugins w/Webpack?
    https://laracasts.com/discuss/channels/elixir/how-to-use-jquery-plugins-wwebpack

  4. Webpackを使い倒す
    http://thujikun.github.io/blog/2014/12/07/webpack/

  5. ProvidePlugin
    https://webpack.js.org/plugins/provide-plugin/

目次

  1. autokana で「お名前(漢字)」が入力されたら「お名前(かな)」を自動入力させる
  2. ProvidePlugin を利用して jQuery を各ファイルで require 不要にする
  3. 「お名前(漢字)」「お名前(かな)」をどちらも入力チェックエラーにしてから、「お名前(漢字)」を入力して「お名前(かな)」に autokana で自動入力されても「お名前(かな)」の入力チェックエラー状態が解除されないのを修正する
  4. メモ書き
    1. input01.js で関数を var xxx = function() { ... } で記述しているのは IntelliJ IDEA が見やすく表示してくれるからである

手順

autokana で「お名前(漢字)」が入力されたら「お名前(かな)」を自動入力させる

日本語入力に対するふりがな/フリガナを自動的に別フィールドに入力する autokana という jQueryプラグインがあることを知りましたので、インストールして「お名前(漢字)」に入力された文字のかな文字列を「お名前(かな)」に自動入力されるようにしてみます。

autokana は npm ではインストールできませんので、autokanaGitHub のページから jquery.autoKana.js をダウンロードします。

src/main/assets/js/lib の下に vendor/autokana ディレクトリを新規作成した後、その下に jquery.autoKana.js を配置します。

src/main/assets/js/inquiry/input01.js の以下の点を変更します。

window.$ = window.jQuery = require("admin-lte/plugins/jQuery/jquery-2.2.3.min.js");
var Form = require("lib/class/Form.js");
var converter = require("lib/util/converter.js");
var validator = require("lib/util/validator.js");
require("vendor/autokana/jquery.autoKana.js");

..........

$(document).ready(function () {
    // 「お名前(漢字)」が入力された時に、かな文字列を「お名前(かな)」に自動入力されるようにする
    $.fn.autoKana('#lastname', '#lastkana');
    $.fn.autoKana('#firstname', '#firstkana');

    // 入力チェック用の validator 関数をセットする
    $("#lastname").on("blur", nameValidator);
    $("#firstname").on("blur", nameValidator);
    $("#lastkana").on("blur", kanaValidator);
    $("#firstkana").on("blur", kanaValidator);
    $("input:radio[name='sex']").on("blur", sexValidator);
    $("#age").on("blur", ageValidator);
    $("#job").on("blur", jobValidator);

    // 「次へ」ボタンクリック時の処理をセットする
    $(".js-btn-next").on("click", btnNextClickHandler)
});
  • jQuery を require する変数を var $ =window.$ = window.jQuery = に変更します。
  • require("vendor/autokana/jquery.autoKana.js"); を追加します。
  • $(document).ready(function () { ... } の中に以下の2行を追加します。
    • $.fn.autoKana('#lastname', '#lastkana');
    • $.fn.autoKana('#firstname', '#firstkana');

動作確認します。npm run springboot コマンドを実行し、Tomcat を起動した後、http://localhost:9080/inquiry/input/01/ にアクセスします。

「お名前(漢字)」に入力すると「お名前(かな)」にかな文字列が入力されました。これは便利ですね。

f:id:ksby:20170902071806p:plain

ProvidePlugin を利用して jQuery を各ファイルで require 不要にする

jQuery を利用する時には各 js ファイルで var $ = require("admin-lte/plugins/jQuery/jquery-2.2.3.min.js"); を記述していましたが、ProvidePlugin を利用すると省略できるそうなので設定してみます。

webpack.config.js を以下のように変更します。

var webpack = require('webpack');

module.exports = {
    entry: {
        "js/app": ["./src/main/assets/js/app.js"],
        "js/inquiry/input01": ["./src/main/assets/js/inquiry/input01.js"],
        "js/inquiry/input02": ["./src/main/assets/js/inquiry/input02.js"],
        "js/inquiry/input03": ["./src/main/assets/js/inquiry/input03.js"],
        "js/inquiry/confirm": ["./src/main/assets/js/inquiry/confirm.js"]
    },
    output: {
        path: __dirname + "/src/main/resources/static",
        publicPath: "/",
        filename: "[name].js"
    },
    resolve: {
        modules: [
            "node_modules",
            "src/main/assets/js"
        ]
    },
    plugins: [
        new webpack.ProvidePlugin({
            $: "admin-lte/plugins/jQuery/jquery-2.2.3.min.js",
            jQuery: "admin-lte/plugins/jQuery/jquery-2.2.3.min.js"
        })
    ]
};
  • var webpack = require('webpack'); を追加します。
  • plugins: [ new webpack.ProvidePlugin({ ... }) ] を追加します。

src/main/assets/js の下の全ての js ファイルから require("admin-lte/plugins/jQuery/jquery-2.2.3.min.js"); の行を削除します。

npm run springboot コマンドを停止して起動し直すとエラーが出ずに起動し、入力チェックの処理や autokana も問題なく動作しました。

「お名前(漢字)」「お名前(かな)」をどちらも入力チェックエラーにしてから、「お名前(漢字)」を入力して「お名前(かな)」に autokana で自動入力されても「お名前(かな)」の入力チェックエラー状態が解除されないのを修正する

autokana で「お名前(かな)」に自動入力されるようにしたのですが、今の実装だと「お名前(漢字)」「お名前(かな)」をどちらも入力チェックエラーにしてから、

f:id:ksby:20170902080436p:plain

「お名前(漢字)」にカーソルを移動して漢字を入力し「お名前(かな)」にかな文字列が反映されても、「お名前(漢字)」の「名」から「お名前(かな)」の「姓」にカーソルが移動したタイミングでは「お名前(かな)」の入力チェックエラー状態が解除されません。

f:id:ksby:20170902081421p:plain

解除されるようにしてみます。

src/main/assets/js/lib/class/Form.js の以下の点を変更します。

..........

/**
 * 渡された id を form.focused にセットして
 * focused イベントが発生したことにする
 * @param {Form} form - Form オブジェクト
 * @param {string} id - focused イベントを発生したことにする要素の id
 */
Form.prototype.setFocused = function (form, id) {
    form.focused[id] = true;
};

/**
 * 渡された idList にセットされている id を form.focused にセットして
 * focused イベントが発生したことにする
 * @param {Form} form - Form オブジェクト
 * @param {Array} idList - focused イベントが発生したことにする要素の id の配列
 */
Form.prototype.setFocusedFromList = function (form, idList) {
    idList.forEach(function (id) {
        form.setFocused(form, id);
    })
};

/**
 * form.idList にセットされている id を全て form.focused にセットして
 * focused イベントが発生したことにする
 * @param {Form} form - Form オブジェクト
 */
Form.prototype.forceAllFocused = function (form) {
    form.setFocusedFromList(form, form.idList);
};

/**
 * Form オブジェクトの idList に列挙された id の要素の focus イベントハンドラに
 * Form.focused 配列に id をセットする関数をセットする
 * @param {Form} form - Form オブジェクト
 */
function addFocusEventListener(form) {
    form.idList.forEach(function (id) {
        $(id).on("focus", function (event) {
            form.setFocused(form, id);
        })
    })
}
  • Form.prototype.setFocused メソッドを追加します。
  • Form.prototype.setFocusedFromList メソッドを追加します。
  • Form.prototype.forceAllFocused メソッド内の処理を form.idList.forEach(function (id) { ... }form.setFocusedFromList(form, form.idList); に変更します。
  • addFocusEventListener 関数内の処理を form.focused[id] = true;form.setFocused(form, id); に変更します。

src/main/assets/js/inquiry/input01.js の以下の点を変更します。

var kanaAutoInputValidator = function (event) {
    var idList = ["#lastkana", "#firstkana"];
    // 「お名前(漢字)」を入力して「お名前(かな)」が自動入力された時に、
    // 「お名前(かな)」の入力チェックが実行されるようにする
    if (!form.isAnyEmpty(idList)) {
        form.setFocusedFromList(form, idList);
        kanaValidator(event);
    }
};

..........

$(document).ready(function () {
    // 「お名前(漢字)」が入力された時に、かな文字列を「お名前(かな)」に自動入力されるようにする
    $.fn.autoKana('#lastname', '#lastkana');
    $.fn.autoKana('#firstname', '#firstkana');

    // 入力チェック用の validator 関数をセットする
    $("#lastname")
        .on("blur", nameValidator)
        .on("blur", kanaAutoInputValidator);
    $("#firstname")
        .on("blur", nameValidator)
        .on("blur", kanaAutoInputValidator);
    $("#lastkana").on("blur", kanaValidator);
    $("#firstkana").on("blur", kanaValidator);
    $("input:radio[name='sex']").on("blur", sexValidator);
    $("#age").on("blur", ageValidator);
    $("#job").on("blur", jobValidator);

    // 「次へ」ボタンクリック時の処理をセットする
    $(".js-btn-next").on("click", btnNextClickHandler)
});
  • kanaAutoInputValidator メソッドを追加します。
  • $(document).ready(function () { ... } 内の以下の点を変更します。
    • $("#lastname")$("#firstname").on("blur", kanaAutoInputValidator) を追加します。

動作確認します。

http://localhost:9080/inquiry/input/01/ にアクセスして「お名前(漢字)」「お名前(かな)」を入力チェックエラー状態にした後、

f:id:ksby:20170902094531p:plain

マウスで「お名前(漢字)」の「姓」にカーソルを移動してから「姓」「名」を入力すると「お名前(かな)」にかな文字列が自動入力されて、かつ「お名前(漢字)」の「名」から「お名前(かな)」の「姓」にカーソルが移動した時に「お名前(かな)」も入力チェック OK の状態になります。

f:id:ksby:20170902094840p:plain

Ctrl+F5 を押してリロードした後、普通に「お名前(漢字)」を入力しても「お名前(かな)」が入力チェック OK の状態になります。

f:id:ksby:20170902095024p:plain

Ctrl+F5 を押してリロードした後、「お名前(漢字)」にかな文字列にできない英字を入力した場合には「お名前(かな)」の入力チェックは動作せず何も変わりません。

f:id:ksby:20170902095215p:plain

Ctrl+F5 を押してリロードした後、「お名前(漢字)」の「姓」だけ入力した場合には「お名前(漢字)」は入力チェックエラー状態になりますが「お名前(かな)」の入力チェックは動作せず何も変わりません。

f:id:ksby:20170902095443p:plain

問題なさそうです。次回は Form クラスを作成してセッションに保存する処理を実装します。

メモ書き

input01.js で関数を var xxx = function() { ... } で記述しているのは IntelliJ IDEA が見やすく表示してくれるからである

最初は function xxx() { ... } の形式で書いていたのですが、var xxx = function() { ... } の形式で書くと IntelliJ IDEA が関数名毎に色を変えて表示してくれるので、input01.js では var xxx = function() { ... } の形式で書くことにしました。

function xxx() { ... } の形式で書くと IntelliJ IDEA 上では以下のように関数名は全て同じ色で表示されますが、

f:id:ksby:20170902102427p:plain f:id:ksby:20170902102553p:plain

var xxx = function() { ... } の形式で書くと IntelliJ IDEA の Semantic highlighting の機能で関数名毎に色を変えて表示してくれます(変数名とみなされているため)。

f:id:ksby:20170902102902p:plain f:id:ksby:20170902103005p:plain

ただし var xxx = function() { ... } は上に書いておかないと下の関数では使用できないようなので、その点だけ注意が必要です。

履歴

2017/09/02
初版発行。