かんがるーさんの日記

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

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

概要

記事一覧はこちらです。

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

  • 今回の手順で確認できるのは以下の内容です。
    • 入力画面3の作成
    • 「前の画面へ戻る」ボタン、「確認画面へ」ボタンが正常に動作していないので原因を調査します。
    • その後にサーバ側の処理を実装します。

参照したサイト・書籍

目次

  1. 「前の画面へ戻る」ボタンを押すと固まる原因を調査する
  2. 「確認画面へ」ボタンを押すとエラーになる原因を調査する
  3. Form クラスを作成する
  4. input03.html, input03.js を変更する
  5. SessionData クラスを変更する
  6. 画面表示時と「前の画面へ戻る」「確認画面へ」ボタンクリック時の処理を実装する
  7. 動作確認

手順

「前の画面へ戻る」ボタンを押すと固まる原因を調査する

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その41 )( IntelliJ IDEA で Javascript を debug する ) に書いた手順で debug してみたら form タグの id 属性の文字列が input03.js に書いている id と一致していないことが原因でした。

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

          <form id="inquiryInput03Form" class="form-horizontal" method="post" action=""
                th:action="@{/inquiry/input/03/}">
  • input03ForminquiryInput03Form に変更します。

入力画面3を表示して「前の画面へ戻る」ボタンを押すと、今度は入力画面2に戻りました。

f:id:ksby:20180509223058p:plain f:id:ksby:20180509223402p:plain

「確認画面へ」ボタンを押すとエラーになる原因を調査する

上の調査で debug していた時に気づきましたが、原因は「確認画面へ」ボタンの class 属性に記述しているクラス名が js-btn-confirm なのに input03.js に書いているクラス名が js-btn-next だったからでした。

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

var btnBackOrNextClickHandler = function (event, url, ignoreCheckRequired) {
    ..........

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

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

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

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

    ..........
});
  • .js-btn-next.js-btn-confirm に変更します。

動作確認します。入力画面3を表示して、

f:id:ksby:20180509230213p:plain

何も選択・入力せずに「確認画面へ」ボタンを押すと、入力画面3のままで必須項目にエラーメッセージが表示されました。

f:id:ksby:20180509230338p:plain

必須項目全てに選択・入力してから「確認画面へ」ボタンを押すと、

f:id:ksby:20180509230451p:plain

今度は確認画面が表示されました。

f:id:ksby:20180509230707p:plain

Form クラスを作成する

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

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


import lombok.Data;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

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

    private static final long serialVersionUID = -2818250124844174764L;

    private String type1;

    private List<String> type2 = new ArrayList<>();

    private String inquiry;

    private List<String> survey;

    private boolean copiedFromSession = false;

}

必須チェックだけ実行するためのクラスも作ります。src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form の下に InquiryInput03FormNotEmptyRule.java を新規作成し、以下の内容を記述します。

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


import lombok.Data;
import org.hibernate.validator.constraints.NotEmpty;

import java.util.ArrayList;
import java.util.List;

/**
 * 入力画面3 必須チェック用クラス
 */
@Data
public class InquiryInput03FormNotEmptyRule {

    @NotEmpty
    private String type1;

    @NotEmpty
    private List<String> type2 = new ArrayList<>();

    @NotEmpty
    private String inquiry;

    private List<String> survey;

    private boolean copiedFromSession = false;

}

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

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

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

  <style>
    /* 「チェックボックス複数行」の入力項目のチェックボックスを複数行に書くので、 */
    /* 異なる行のチェックボックスの位置を左揃えにする                         */
    @media (min-width: 768px) {
      #multiline-checkbox .checkbox label {
        display: block;
        float: left;
        width: 180px;
      }
    }
    @media (max-width: 767px) {
      #multiline-checkbox .checkbox label {
        display: block;
        float: left;
        width: 100%;
      }
    }
  </style>
</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>
        入力画面3
      </h1>
    </section>

    <!-- Main content -->
    <section class="content">
      <div class="row">
        <div class="col-xs-12">
          <!--/*@thymesVar id="inquiryInput03Form" type="ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput03Form"*/-->
          <form id="inquiryInput03Form" class="form-horizontal" method="post" action=""
                th:action="@{/inquiry/input/03/}"
                th:object="${inquiryInput03Form}">
            <input type="hidden" name="copiedFromSession" id="copiedFromSession" th:value="*{copiedFromSession}"/>

            <!-- お問い合わせの種類1 -->
            <div class="form-group" id="form-group-type1">
              <div class="control-label col-sm-2">
                <label class="float-label">お問い合わせの種類1</label>
                <div class="label label-required">必須</div>
              </div>
              <div class="col-sm-10">
                <div class="row"><div class="col-sm-10">
                  <select name="type1" id="type1" class="form-control" style="width: 250px;" autofocus>
                    <th:block th:each="type1Value,iterStat : ${@vh.values('Type1Values')}">
                      <option value="" th:if="${iterStat.first}">選択してください</option>
                      <option th:value="${type1Value.value}" th:text="${type1Value.text}"
                        th:field="*{type1}">
                      </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>

            <!-- お問い合わせの種類2 -->
            <div class="form-group" id="form-group-type2">
              <div class="control-label col-sm-2">
                <label class="float-label">お問い合わせの種類2</label>
                <div class="label label-required">必須</div>
              </div>
              <div class="col-sm-10">
                <div class="row"><div class="col-sm-10">
                  <div class="checkbox">
                    <th:block th:each="type2Value : ${@vh.values('Type2Values')}">
                      <label>
                        <input type="checkbox" name="type2" th:value="${type2Value.value}"
                          th:field="*{type2}">
                        <th:block th:text="${type2Value.text}">見積が欲しい</th:block>
                      </label>
                    </th:block>
                  </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-inquiry">
              <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">
                  <textarea rows="5" name="inquiry" id="inquiry" class="form-control" maxlength="500" placeholder="お問い合わせ内容を入力して下さい"
                    th:field="*{inquiry}"></textarea>
                </div></div>
                <div class="row"><div class="col-sm-10"><p class="form-control-static"><small>※最大500文字</small></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-survey">
              <div class="control-label col-sm-2">
                <label class="float-label">アンケート</label>
              </div>
              <div class="col-sm-10" id="multiline-checkbox">
                <th:block th:each="surveyOptions,iterStat : ${@soh.selectItemList('survey')}">
                  <th:block th:if="${iterStat.index % 3 == 0}"
                            th:utext="'&lt;div class=&quot;row&quot;&gt;&lt;div class=&quot;col-sm-12&quot;&gt;&lt;div class=&quot;checkbox&quot;&gt;'"/>

                  <label>
                    <input type="checkbox" name="survey" th:value="${surveyOptions.itemValue}"
                      th:field="*{survey}">
                    <th:block th:text="${surveyOptions.itemName}">選択肢1だけ長くしてみる</th:block>
                  </label>

                  <th:block th:if="${iterStat.index % 3 == 2 || iterStat.last}"
                            th:utext="'&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;'"/>
                </th:block>
              </div>
            </div>

            <div class="text-center">
              <button class="btn bg-blue js-btn-back"><i class="fa fa-arrow-left"></i> 前の画面へ戻る</button>
              <button class="btn bg-green js-btn-confirm"><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/input03.js"></script>

</body>
</html>
  • form タグの上に <!--/*@thymesVar id="inquiryInput03Form" type="ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput03Form"*/--> を追加します。
  • form タグの末尾に th:object="${inquiryInput03Form}" を追加します。
  • <input type="hidden" name="copiedFromSession" id="copiedFromSession" th:value="*{copiedFromSession}"/> を追加します。
  • <select name="type1" id="type1" class="form-control" style="width: 250px;" autofocus><select name="type1" id="type1" class="form-control" style="width: 250px;"> に変更します。
  • 入力/選択項目のタグに th:field="*{...}"(... には入力項目に対応した変数を記述) を追加します。

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

$(document).ready(function (event) {
    // 入力チェック用の validator 関数をセットする
    $("#type1").on("blur", type1Validator);
    $("input:checkbox[name='type2']").on("blur", type2Validator);
    $("#inquiry").on("blur", inquiryValidator);

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

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

    // 「お問い合わせの種類1」にフォーカスをセットする
    $("#type1").focus().select();
});
  • $(document).ready(function () {$(document).ready(function (event) { に変更します。
  • 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 InquiryInput03Form inquiryInput03Form;

}
  • private InquiryInput03Form inquiryInput03Form; を追加します。

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

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

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

    ..........

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

        return TEMPLATE_INPUT03;
    }

    /**
     * 入力画面3 「前へ」ボタンクリック時の処理
     *
     * @return 入力画面2の URL
     */
    @PostMapping(value = "/03", params = {"move=back"})
    public String input03MoveBack(@Validated InquiryInput03Form inquiryInput03Form
            , BindingResult bindingResult
            , SessionData sessionData
            , UriComponentsBuilder builder) {
        if (bindingResult.hasErrors()) {
            bindingResult.getAllErrors().stream().forEach(e -> log.warn(e.getCode()));
            throw new IllegalArgumentException("セットされるはずのデータがセットされていません");
        }

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

        return UrlBasedViewResolver.REDIRECT_URL_PREFIX
                + builder.path(UrlConst.URL_INQUIRY_INPUT_02).toUriString();
    }

    /**
     * 入力画面3 「確認画面へ」ボタンクリック時の処理
     *
     * @return 確認画面の URL
     */
    @PostMapping(value = "/03", params = {"move=next"})
    public String input03MoveNext(@Validated InquiryInput03Form inquiryInput03Form
            , BindingResult bindingResult
            , InquiryInput03FormNotEmptyRule inquiryInput03FormNotEmptyRule
            , SessionData sessionData
            , UriComponentsBuilder builder) {
        // 必須チェックをする
        mvcValidator.validate(inquiryInput03FormNotEmptyRule, bindingResult);
        if (bindingResult.hasErrors()) {
            bindingResult.getAllErrors().stream().forEach(e -> log.warn(e.getCode()));
            throw new IllegalArgumentException("セットされるはずのデータがセットされていません");
        }

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

        return UrlBasedViewResolver.REDIRECT_URL_PREFIX
                + builder.path(UrlConst.URL_INQUIRY_CONFIRM).toUriString();
    }

}
  • input03 メソッドの以下の点を変更します。
    • 引数に InquiryInput03Form inquiryInput03FormSessionData sessionData を追加します。
    • if (sessionData.getInquiryInput02Form() != null) { ... } の処理を追加します。
  • input03MoveBack メソッドの以下の点を変更します。
    • 引数に @Validated InquiryInput03Form inquiryInput03FormBindingResult bindingResultSessionData sessionData を追加します。
    • if (bindingResult.hasErrors()) { ... } の処理を追加します。
    • sessionData.setInquiryInput03Form(inquiryInput03Form); を追加します。
  • input03MoveNext メソッドの以下の点を変更します。
    • 引数に @Validated InquiryInput03Form inquiryInput03FormBindingResult bindingResultInquiryInput03FormNotEmptyRule inquiryInput03FormNotEmptyRuleSessionData sessionData を追加します。
    • mvcValidator.validate(inquiryInput03FormNotEmptyRule, bindingResult); を追加します。
    • if (bindingResult.hasErrors()) { ... } を追加します。
    • sessionData.setInquiryInput03Form(inquiryInput03Form); を追加します。

動作確認

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

データを入力してから、「次へ」ボタンをクリックして入力画面3へ遷移します。

f:id:ksby:20180519180336p:plain

何も入力せずに「前の画面へ戻る」ボタンをクリックすると、入力画面2へ戻ります。サーバ側でも必須チェックは行われません。

f:id:ksby:20180519180651p:plain

「次へ」ボタンをクリックして入力画面3へ戻った後、データを入力します。

f:id:ksby:20180519180943p:plain

「前の画面へ戻る」ボタンをクリックして入力画面2へ戻ってから、

f:id:ksby:20180519181410p:plain

「次へ」ボタンをクリックして入力画面3へ戻ると、前に入力したデータが表示されます。

f:id:ksby:20180519181723p:plain

入力したデータを変更してから、

f:id:ksby:20180519182114p:plain

「確認画面へ」ボタンをクリックして確認画面へ遷移した後、

f:id:ksby:20180519182247p:plain

一番下の「修正する」ボタンをクリックすると、入力画面3へ戻り変更したデータが表示されます。

f:id:ksby:20180519182600p:plain

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

履歴

2018/05/19
初版発行。