Spring Boot + npm + Geb で入力フォームを作ってテストする ( その24 )( 入力画面2を作成する3 )
概要
記事一覧はこちらです。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その23 )( 入力画面2を作成する2 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 入力画面2の作成
- サーバ側の処理を実装します。
参照したサイト・書籍
目次
- 電話番号に必須チェックを追加する
- Form クラスを作成する
- FormValidator クラスを作成する
- input02.html, input02.js を変更する
- SessionData クラスを変更する
- 画面表示時と「前の画面へ戻る」「次へ」ボタンクリック時の処理を実装する
- 動作確認
- 次回は。。。
手順
電話番号に必須チェックを追加する
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その23 )( 入力画面2を作成する2 ) で checkRequired 関数以外は値が空の場合に入力チェックを行わないように変更したのですが、電話番号の市外局番+市内局番だけ入力しても入力チェックOKになる等の問題があることに気づきました。
電話番号のいずれかが入力されている場合には、市外局番、市内局番、加入者番号で必須チェックを行うようにします。
ignoreCheckRequired === true
の時でも必須チェックする関数が欲しいので、src/main/assets/js/lib/util/validator.js を以下のように変更します。
/** * 必須チェック用関数 (ignoreCheckRequired によるスキップ機能あり) * @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; this.forceCheckRequired(form, idFormGroup, idList, errmsg); }, /** * 必須チェック用関数 * @param {Form} form - Form オブジェクト * @param {string} idFormGroup - Validation の SUCCESS/ERROR の結果を反映する要素の id * @param {Array} idList - チェックを行う要素の id の配列 * @param {string} errmsg - チェックエラー時に表示するエラーメッセージ */ forceCheckRequired: function (form, idFormGroup, idList, errmsg) { var isValid = !form.isAnyEmpty(idList); setSuccessOrError(form, idFormGroup, errmsg, isValid); },
forceCheckRequired
関数を追加します。checkRequired
関数内の必須チェック処理をforceCheckRequired
関数を呼び出すように変更します。
src/main/assets/js/inquiry/input02.js を以下のように変更します。
// 「電話番号」に1つでも値が入力されていたら入力チェックする if (form.isAnyNotEmpty(telIdList)) { try { validator.forceCheckRequired(form, telIdFormGroup, 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桁の数字を入力してください"); } catch (e) { errmsg = e.message; } }
telAndEmailValidator
関数の中で定義しているvalidateFunction
関数内にvalidator.forceCheckRequired(form, telIdFormGroup, telIdList, "市外局番、市内局番、加入者番号は全て入力してください");
を追加します。
Form クラスを作成する
src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form の下に InquiryInput02Form.java を新規作成し、以下の内容を記述します。
package ksbysample.webapp.bootnpmgeb.web.inquiry.form; import lombok.Data; import javax.validation.constraints.Size; import java.io.Serializable; /** * 入力画面2用 Form クラス */ @Data public class InquiryInput02Form implements Serializable { private static final long serialVersionUID = -2484970766971811218L; @Size(max = 3) private String zipcode1; @Size(max = 4) private String zipcode2; @Size(max = 256) private String address; @Size(max = 5) private String tel1; @Size(max = 4) private String tel2; @Size(max = 4) private String tel3; @Size(max = 256) private String email; private boolean copiedFromSession = false; }
必須チェックだけ実行するためのクラスも作ります。src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form の下に InquiryInput02FormNotEmptyRule.java を新規作成し、以下の内容を記述します。
package ksbysample.webapp.bootnpmgeb.web.inquiry.form; import lombok.Data; import org.hibernate.validator.constraints.NotEmpty; /** * 入力画面2 必須チェック用クラス */ @Data public class InquiryInput02FormNotEmptyRule { private static final long serialVersionUID = -2484970766971811218L; @NotEmpty private String zipcode1; @NotEmpty private String zipcode2; @NotEmpty private String address; private String tel1; private String tel2; private String tel3; private String email; private boolean copiedFromSession = false; }
FormValidator クラスを作成する
最初にメールアドレスチェック用のクラスを作成します。src/main/java/ksbysample/webapp/bootnpmgeb/util の下に validator パッケージを新規作成します。
src/main/java/ksbysample/webapp/bootnpmgeb/validator の下に EmailValidator.java を新規作成し、以下の内容を記述します。
package ksbysample.webapp.bootnpmgeb.util.validator; import org.apache.commons.lang3.StringUtils; import java.util.Arrays; import java.util.regex.Pattern; /** * メールアドレスチェック用 Util クラス */ public class EmailValidator { private static final Pattern PATTERN_EMAIL = Pattern.compile("^[\\x21-\\x7E]+$"); /** * メールアドレスが正しいフォーマットかチェックする * * @param email チェックするメールアドレス * @return チェック結果 true:OK, false:NG */ public static boolean validate(String email) { // 値が入力されていなければチェックしない if (StringUtils.isEmpty(email)) { return true; } // @で分割して要素数が2つかどうかチェックする String[] elements = email.split("@"); if (elements.length != 2) { return false; } // 1つ目及び2つ目の要素に空白、制御文字、非ASCII文字が含まれていないかチェックする return Arrays.stream(elements) .map(e -> PATTERN_EMAIL.matcher(e).matches()) .reduce(true, (prs, prv) -> prs && prv); } }
src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form の下に InquiryInput02FormValidator.java を新規作成し、以下の内容を記述します。
package ksbysample.webapp.bootnpmgeb.web.inquiry.form; import ksbysample.webapp.bootnpmgeb.util.validator.EmailValidator; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import java.util.regex.Pattern; /** * 入力画面2入力チェック用クラス */ @Component public class InquiryInput02FormValidator implements Validator { private static final Pattern PATTERN_ZIPCODE = Pattern.compile("^[0-9]{7}$"); private static final Pattern PATTERN_TEL1 = Pattern.compile("^0.*"); private static final Pattern PATTERN_TEL1_TEL2 = Pattern.compile("^[0-9]{6}$"); private static final Pattern PATTERN_TEL3 = Pattern.compile("^[0-9]{4}$"); @Override public boolean supports(Class<?> clazz) { return clazz.equals(InquiryInput02Form.class); } @Override public void validate(Object target, Errors errors) { InquiryInput02Form inquiryInput02Form = (InquiryInput02Form) target; checkZipcode(inquiryInput02Form.getZipcode1(), inquiryInput02Form.getZipcode2(), errors); checkTelAndEmail(inquiryInput02Form.getTel1() , inquiryInput02Form.getTel2() , inquiryInput02Form.getTel3() , inquiryInput02Form.getEmail() , errors); } private void checkZipcode(String zipcode1, String zipcode2, Errors errors) { if (StringUtils.isEmpty(zipcode1) && StringUtils.isEmpty(zipcode2)) { return; } if (!PATTERN_ZIPCODE.matcher(zipcode1 + zipcode2) .matches()) { errors.reject("InquiryInput02Form.zipcode.UnmatchPattern"); } } @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ConfusingTernary"}) private void checkTelAndEmail(String tel1, String tel2, String tel3, String email, Errors errors) { if (StringUtils.isEmpty(tel1) && StringUtils.isEmpty(tel2) && StringUtils.isEmpty(tel3) && StringUtils.isEmpty(email)) { return; } if (StringUtils.isEmpty(tel1 + tel2 + tel3) && StringUtils.isEmpty(email)) { errors.reject("InquiryInput02Form.telOrEmail.NotEmpty"); } else { // 電話番号に1つでも入力されていたら入力チェックする if (StringUtils.isNoneEmpty(tel1) || StringUtils.isNotEmpty(tel2) || StringUtils.isNotEmpty(tel3)) { if (StringUtils.isEmpty(tel1) || StringUtils.isEmpty(tel2) || StringUtils.isEmpty(tel3)) { errors.reject("InquiryInput02Form.tel1AndTel2AndTel3.AnyEmpty"); } else if (!PATTERN_TEL1.matcher(tel1).matches()) { errors.reject("InquiryInput02Form.tel1.UnmatchPattern"); } else if (!PATTERN_TEL1_TEL2.matcher(tel1 + tel2).matches()) { errors.reject("InquiryInput02Form.tel1AndTel2.UnmatchPattern"); } else if (!PATTERN_TEL3.matcher(tel3).matches()) { errors.reject("InquiryInput02Form.tel3.UnmatchPattern"); } } // メールアドレスが入力されていたら入力チェックする if (StringUtils.isNotEmpty(email)) { if (!EmailValidator.validate(email)) { errors.reject("InquiryInput02Form.email.Invalid"); } } } } }
src/main/resources/messages_ja_JP.properties に以下の記述を追加します。
InquiryInput02Form.zipcode.UnmatchPattern=郵便番号が数字7桁ではありません。 InquiryInput02Form.telOrEmail.NotEmpty=郵便番号が数字7桁ではありません。 InquiryInput02Form.tel1AndTel2AndTel3.AnyEmpty=市外局番、市内局番、加入者番号は全て入力してください。 InquiryInput02Form.tel1.UnmatchPattern=市外局番の先頭には 0 の数字を入力してください。 InquiryInput02Form.tel1AndTel2.UnmatchPattern=市外局番+市内局番の組み合わせが数字6桁になるように入力してください。 InquiryInput02Form.tel3.UnmatchPattern=加入者番号には4桁の数字を入力してください。 InquiryInput02Form.email.Invalid=メールアドレスを入力してください
input02.html, input02.js を変更する
src/main/resources/templates/web/inquiry/input02.html の以下の点を変更します。
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head th:replace="~{web/common/fragments :: common_header(~{::title}, ~{::link}, ~{::style})}"> <title>入力フォーム - 入力画面2</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> 入力画面2 </h1> </section> <!-- Main content --> <section class="content"> <div class="row"> <div class="col-xs-12"> <!--/*@thymesVar id="inquiryInput02Form" type="ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput02Form"*/--> <form id="inquiryInput02Form" class="form-horizontal" method="post" action="" th:action="@{/inquiry/input/02/}" th:object="${inquiryInput02Form}"> <input type="hidden" name="copiedFromSession" id="copiedFromSession" th:value="*{copiedFromSession}" /> <!-- 郵便番号 --> <div class="form-group" id="form-group-zipcode"> <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"> <p class="form-control-static-inline">〒</p> <input type="text" name="zipcode1" id="zipcode1" class="form-control form-control-inline" style="width: 60px;" maxlength="3" value="" placeholder="" autofocus th:field="*{zipcode1}"/> <p class="form-control-static-inline">-</p> <input type="text" name="zipcode2" id="zipcode2" class="form-control form-control-inline" style="width: 80px;" maxlength="4" value="" placeholder="" th:field="*{zipcode2}"/> </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-address"> <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="address" id="address" class="form-control" maxlength="256" value="" placeholder="例)東京都千代田区飯田橋1-1" th:field="*{address}"/> </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-tel"> <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"> <input type="text" name="tel1" id="tel1" class="form-control form-control-inline" style="width: 100px;" maxlength="5" value="" placeholder="" th:field="*{tel1}"/> <p class="form-control-static-inline">-</p> <input type="text" name="tel2" id="tel2" class="form-control form-control-inline" style="width: 100px;" maxlength="4" value="" placeholder="" th:field="*{tel2}"/> <p class="form-control-static-inline">-</p> <input type="text" name="tel3" id="tel3" class="form-control form-control-inline" style="width: 100px;" maxlength="4" value="" placeholder="" th:field="*{tel3}"/> </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-email"> <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"> <input type="text" name="email" id="email" class="form-control" style="width: 250px;" maxlength="256" value="" placeholder="例)taro.tanaka@sample.co.jp" th:field="*{email}"/> </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"> <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> </form> </div> </div> </section> <!-- /.content --> </div> <!-- /.content-wrapper --> </div> <!-- ./wrapper --> <!-- REQUIRED JS SCRIPTS --> <script src="/js/inquiry/input02.js"></script> </body> </html>
- form タグの上に
<!--/*@thymesVar id="inquiryInput02Form" type="ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput02Form"*/-->
を追加します。 <form id="input02Form" ...
→<form id="inquiryInput02Form" ...
に変更します。- form タグの末尾に
th:object="${inquiryInput02Form}"
を追加します。 <input type="hidden" name="copiedFromSession" id="copiedFromSession" th:value="*{copiedFromSession}"/>
を追加します。- 入力/選択項目のタグに
th:field="*{...}"
(... には入力項目に対応した変数を記述) を追加します。
form タグの id 属性を変更したので src/main/assets/js/inquiry/input02.js の以下の点を変更します。
var btnBackOrNextClickHandler = function (event, url, ignoreCheckRequired) { .......... // サーバにリクエストを送信する $("#inquiryInput02Form").attr("action", url); $("#inquiryInput02Form").submit(); .......... }; .......... $(document).ready(function (event) { .......... // 初期画面表示時にセッションに保存されていたデータを表示する場合には // 入力チェックを実行して画面の表示を入力チェック後の状態にする if ($("#copiedFromSession").val() === "true") { executeAllValidator(event); } // 「お名前(漢字)」の「姓」にフォーカスをセットする $("#lastname").focus().select(); });
$("#input02Form")
→$("#inquiryInput02Form")
に変更します。if ($("#copiedFromSession").val() === "true") { ... }
を追加します。
SessionData クラスを変更する
src/main/java/ksbysample/webapp/bootnpmgeb/session/SessionData.java の以下の点を変更します。
@Data public class SessionData implements Serializable { private static final long serialVersionUID = -2673191456750655164L; private InquiryInput01Form inquiryInput01Form; private InquiryInput02Form inquiryInput02Form; }
private InquiryInput02Form inquiryInput02Form;
を追加します。
画面表示時と「前の画面へ戻る」「次へ」ボタンクリック時の処理を実装する
src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryInputController.java の以下の点を変更します。
@Slf4j @Controller @RequestMapping("/inquiry/input") @SessionAttributes("sessionData") public class InquiryInputController { private static final String TEMPLATE_BASE = "web/inquiry"; private static final String TEMPLATE_INPUT01 = TEMPLATE_BASE + "/input01"; private static final String TEMPLATE_INPUT02 = TEMPLATE_BASE + "/input02"; private static final String TEMPLATE_INPUT03 = TEMPLATE_BASE + "/input03"; private final ModelMapper modelMapper; private final InquiryInput02FormValidator inquiryInput02FormValidator; private final Validator mvcValidator; /** * コンストラクタ * * @param modelMapper {@link ModelMapper} オブジェクト * @param inquiryInput02FormValidator {@link InquiryInput02FormValidator} オブジェクト * @param mvcValidator {@link Validator} オブジェクト */ public InquiryInputController(ModelMapper modelMapper , InquiryInput02FormValidator inquiryInput02FormValidator , Validator mvcValidator) { this.modelMapper = modelMapper; this.inquiryInput02FormValidator = inquiryInput02FormValidator; this.mvcValidator = mvcValidator; } /** * inquiryInput02Form 用 InitBinder * * @param binder {@link WebDataBinder} オブジェクト */ @InitBinder(value = "inquiryInput02Form") public void inquiryInput02FormInitBinder(WebDataBinder binder) { binder.addValidators(inquiryInput02FormValidator); } .......... /** * 入力画面2 初期表示処理 * * @return 入力画面2の Thymeleaf テンプレートファイルのパス */ @GetMapping("/02") public String input02(InquiryInput02Form inquiryInput02Form , SessionData sessionData) { // セッションに保存されているデータがある場合にはコピーする if (sessionData.getInquiryInput02Form() != null) { modelMapper.map(sessionData.getInquiryInput02Form(), inquiryInput02Form); inquiryInput02Form.setCopiedFromSession(true); } return TEMPLATE_INPUT02; } /** * 入力画面2 「前の画面へ戻る」ボタンクリック時の処理 * * @return 入力画面1の URL */ @PostMapping(value = "/02", params = {"move=back"}) public String input02MoveBack(@Validated InquiryInput02Form inquiryInput02Form , BindingResult bindingResult , SessionData sessionData , UriComponentsBuilder builder) { if (bindingResult.hasErrors()) { bindingResult.getAllErrors().stream().forEach(e -> log.warn(e.getCode())); throw new IllegalArgumentException("セットされるはずのないデータがセットされています"); } // 入力されたデータをセッションに保存する sessionData.setInquiryInput02Form(inquiryInput02Form); return UrlBasedViewResolver.REDIRECT_URL_PREFIX + builder.path(UrlConst.URL_INQUIRY_INPUT_01).toUriString(); } /** * 入力画面2 「次へ」ボタンクリック時の処理 * * @return 入力画面3の URL */ @PostMapping(value = "/02", params = {"move=next"}) public String input02MoveNext(@Validated InquiryInput02Form inquiryInput02Form , BindingResult bindingResult , InquiryInput02FormNotEmptyRule inquiryInput02FormNotEmptyRule , SessionData sessionData , UriComponentsBuilder builder) { // 必須チェックをする mvcValidator.validate(inquiryInput02FormNotEmptyRule, bindingResult); if (bindingResult.hasErrors()) { bindingResult.getAllErrors().stream().forEach(e -> log.warn(e.getCode())); throw new IllegalArgumentException("セットされるはずのないデータがセットされています"); } // 入力されたデータをセッションに保存する sessionData.setInquiryInput02Form(inquiryInput02Form); return UrlBasedViewResolver.REDIRECT_URL_PREFIX + builder.path(UrlConst.URL_INQUIRY_INPUT_03).toUriString(); } ..........
- 以下のフィールド変数を追加します。また追加したフィールドにセットする処理をコンストラクタに追加します。
private final InquiryInput02FormValidator inquiryInput02FormValidator;
private final Validator mvcValidator;
inquiryInput02FormInitBinder
メソッドを追加します。input02
メソッドの以下の点を変更します。- 引数に
InquiryInput02Form inquiryInput02Form
、SessionData sessionData
を追加します。 if (sessionData.getInquiryInput02Form() != null) { ... }
の処理を追加します。
- 引数に
input02MoveBack
メソッドの以下の点を変更します。- 引数に
@Validated InquiryInput02Form inquiryInput02Form
、BindingResult bindingResult
、SessionData sessionData
を追加します。 if (bindingResult.hasErrors()) { ... }
の処理を追加します。sessionData.setInquiryInput02Form(inquiryInput02Form);
を追加します。
- 引数に
input02MoveNext
メソッドの以下の点を変更します。- 引数に
@Validated InquiryInput02Form inquiryInput02Form
、BindingResult bindingResult
、InquiryInput02FormNotEmptyRule inquiryInput02FormNotEmptyRule
、SessionData sessionData
を追加します。 mvcValidator.validate(inquiryInput02FormNotEmptyRule, bindingResult);
を追加します。if (bindingResult.hasErrors()) { ... }
を追加します。sessionData.setInquiryInput02Form(inquiryInput02Form);
を追加します。
- 引数に
動作確認
動作確認します。npm run springboot コマンドを実行し Tomcat を起動した後、ブラウザで http://localhost:9080/inquiry/input/01/ にアクセスします。
データを入力してから、「次へ」ボタンをクリックして入力画面2へ遷移します。
何も入力せずに「前の画面へ戻る」ボタンをクリックすると、入力画面1へ戻ります。サーバ側でも必須チェックは行われません。
「次へ」ボタンをクリックして入力画面2へ戻った後、データを入力します。
「前の画面へ戻る」ボタンをクリックして入力画面1へ戻ってから、
「次へ」ボタンをクリックして入力画面2へ戻ると、前に入力したデータが表示されます。
入力したデータを変更してから、
「次へ」ボタンをクリックして入力画面3へ遷移した後、
「前の画面へ戻る」ボタンをクリックすると、入力画面2へ戻り変更したデータが表示されます。
最後に各入力項目に最大文字数のデータを入力して、
「前の画面へ戻る」ボタンをクリックすると、入力画面1へ戻ります。サーバ側の最大文字数の入力チェックでエラーになりません。
「次へ」ボタンをクリックして入力画面2へ戻ると、入力した最大文字数のデータが表示されます。
問題なく動作しているようです。
次回は。。。
履歴
2017/10/07
初版発行。