Spring Boot + npm + Geb で入力フォームを作ってテストする ( その20 )( 入力画面1を作成する4 )
概要
記事一覧はこちらです。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その19 )( 入力画面1を作成する3 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 入力画面1の作成
- サーバ側の処理を実装します。また実装後に見つかった HTML, Javascript 側の問題点も修正します。
参照したサイト・書籍
ModelMapper
http://modelmapper.org/Spring Boot ModelMapper Starter
https://github.com/rozidan/modelmapper-spring-boot-starter
目次
- Form クラスを作成する
- セッション保存用のクラスを作成する
- input01.html, input01.js を変更する
- ModdelMapper をインストールする
- 画面表示時と「次へ」ボタンクリック時の処理を実装する
- 動作確認
- HTML5 の autofocus 属性では項目にデータが入っている時に選択状態にしてくれない問題を修正する(Firefox の autokana の問題も「お名前(漢字)」の「名」が文字化けする問題もこれで解決!)
- 入力チェック前の状態ではなく入力チェックした後の状態で表示されるようにする
- autokana で自動入力すると maxlength 以上の文字列が入力される問題を修正する
- 次回は。。。
- メモ書き
手順
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/ にアクセスします。
データを入力してから、
「次へ」ボタンをクリックして入力画面2へ遷移します。
「前の画面へ戻る」ボタンをクリックして入力画面1へ戻ると、
入力画面1へ戻りデータが表示されるのですが、以下の問題がありました。
- 「お名前(漢字)」の「姓」にカーソルが当たるのですが選択状態で表示されません。
- 入力チェックした後の状態ではなく入力チェック前の状態で表示されています。
- なぜか「お名前(漢字)」の「名」だけ文字化けしています。何度か試したところ、この現象は IE だけ出て Firefox と Chrome では出ず、IE でも最初から出る時と最初は出ずに入力画面2と何度か画面遷移させると出る時がありました。謎です。。。
また上記以外にもいろいろ試していて、以下の問題にも気づきました。
- Firefox だと一番最初の「お名前(漢字)」の「姓」から1度カーソルを移動しないと autokana が機能しません。
- 「お名前(漢字)」にかなを20文字入力すると「お名前(かな)」にもかなが20文字入力されるのですが、その後に「お名前(漢字)」にカーソルを移動して末尾の1文字を削除してから1文字追加すると「お名前(かな)」の maxlength は 20 で設定しているのに21文字入力されてしまいます。
あと、「お名前(漢字)」に何も入力せずに「お名前(かな)」を先に入力して、その後に「お名前(漢字)」にカーソルを移動すると「お名前(かな)」に入力していたデータが消えるのですが、これは autokana を入れている場合には正常な動作だと思うのでこのままにします。
HTML5 の autofocus 属性では項目にデータが入っている時に選択状態にしてくれない問題を修正する(Firefox の autokana の問題も「お名前(漢字)」の「名」が文字化けする問題もこれで解決!)
HTML5 の autofocus はフォーカスを移動してくれるだけで、選択状態にはしてくれないようです。Firefox や Chrome でもダメでした。
画面初期表示時に 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 が機能しない問題も解決していました。
さらになぜか「お名前(漢字)」の「名」だけ文字化けする問題も解決していました!(入力画面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/ にアクセスしてデータを入力します。
「次へ」ボタンをクリックして入力画面2へ遷移し、
「前の画面へ戻る」ボタンをクリックすると全ての項目が緑で表示されました。
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) { ... }
動作確認します。「お名前(漢字)」に あいうえおかきくけこさしすせそたちつてと
を入力すると「お名前(かな)」にも あいうえおかきくけこさしすせそたちつてと
が入力されて、
「お名前(漢字)」で末尾の と
を削除して な
を入力しても「お名前(かな)」は あいうえおかきくけこさしすせそたちつてと
のままでした。
次回は。。。
サーバ側のテストを書きます。入力画面1の実装は次で最後の予定です。
メモ書き
IntelliJ IDEA の正規表現チェック機能を使ってみる
今回 src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput01Form.java に以下の画像の正規表現(ひらがなだけかチェックする)を記述しましたが、
正規表現のところにカーソル移動してから Alt+Enter を押してコンテキストメニューを表示した後、「Check RegExp」を選択すると、
入力された文字が正規表現と一致するのか否かを検証できるダイアログが表示されます。
あいうえお
と入力すると右下に Matches!
と表示されて入力フィールドも緑色になりますが、
アイウエオ
に変えると No match
と表示されて赤色に変わります。
確か機能が追加されたのは 2017.2 ではなくもう少し前のはずですが、変更内容をきちんと見ていませんでした。正規表現の動作確認をしたい時はよくあるので、これは嬉しい機能です。
履歴
2017/09/07
初版発行。