かんがるーさんの日記

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

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その12 )( HTML を Thymeleaf テンプレートファイルにする + Controller クラスを作成する )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その11 )( PostCSS で common.css を minify する + autoprefixer, stylelint を導入する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 作成した HTML ファイルを Thymeleaf テンプレートファイルにします。
    • Controller クラスを作成し、画面遷移に必要な js ファイルも実装して、画面の表示と単純な画面遷移ができるようにします。
    • 2回に分けます。今回は入力画面1~3まで実装します。

参照したサイト・書籍

  1. HTML5 & CSS3 デザインレシピ集

    HTML5 & CSS3 デザインレシピ集

    HTML5 & CSS3 デザインレシピ集

  2. HTML5 Form Validation のカスタマイズ
    http://www.girliemac.com/blog/2012/12/01/html5-form-validation-jp/

  3. WAI-ARIAを活用したフロントエンド実装 - 第1回 role属性、aria属性の基礎知識
    https://app.codegrid.net/entry/wai-aria-1

  4. Spring Bootでリダイレクト先のURLを組み立てる
    http://qiita.com/rubytomato@github/items/8d132dec042f695e50f6

  5. Guide to UriComponentsBuilder in Spring
    http://www.baeldung.com/spring-uricomponentsbuilder

  6. jQueryイベントハンドラでreturn falseするとイベントのバブリングが止まる
    http://webtech-walker.com/archive/2012/09/event_handler_return_false.html

目次

  1. HTML ファイルを少し修正する
  2. Thymeleaf を 3.0.6 → 3.0.7 へバージョンアップする
  3. HTML ファイルから Thymeleaf テンプレートファイルを作成する
  4. ksbysample-webapp-lending から config ディレクトリをコピーする
  5. URL を定義する定数クラスを作成する
  6. Controller クラスを作成する
  7. 画面遷移用の js ファイルを実装する
  8. 動作確認
  9. メモ書き

手順

HTML ファイルを少し修正する

最近画面を作っているからか、ふと書店で HTML5 & CSS3 デザインレシピ集 という本を見かけて気になって読んでみて、また家に帰ってから Web でいろいろ調べて HTML5 Form Validation のカスタマイズ という記事を読んで気づきましたが、入力エラー時に入力項目に吹き出しが出る画面を見かけることがありますがあれば HTML5 + CSS3 の機能だったのかとか、HTML5 + CSS3 だけでも結構入力チェックできるんだなとか、自分には HTML5 + CSS3 に関する知識が結構無いんだな。。。とか。

また試しに required 属性を付けて試してみようとしたところ aria-required という属性が候補に表示されました。

f:id:ksby:20170806120707p:plain

aria- って何?と思い調べると WAI-ARIAを活用したフロントエンド実装 - 第1回 role属性、aria属性の基礎知識 の記事が見つかりました。そんな仕様が出来ているとは。。。 全然知りませんでした。

知らないことが多いなあと思いつつ、HTML5 & CSS3 デザインレシピ集 の本は購入したのでさっと読んでどんなことが出来るのかは理解しておこうと思います。

今回の画面では入力チェックは全て Javascript で実装して HTML5 + CSS3 では行いませんが、作成した HTML で autofocus 属性を付けておくのを忘れていたので、一番最初の項目に追加します。

例えば static/inquiry/input01.html の場合には、placeholder="例)田中" の後に autofocus を追加します。

            <!-- お名前(漢字) -->
            <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;" value="" placeholder="例)田中" autofocus/>
                  <input type="text" name="firstname" id="firstname" class="form-control form-control-inline" style="width: 150px;" value="" placeholder="例)太郎"/>
                </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>

画面を表示すると、以下の画像のように autofocus 属性を付けた入力項目にフォーカスがセットされます。

f:id:ksby:20170806114945p:plain

Thymeleaf を 3.0.6 → 3.0.7 へバージョンアップする

http://www.thymeleaf.org/ を見たところ、いつの間にか 3.0.7 がリリースされていました。build.gradle には 3.0.6 を記述していたので 3.0.7 へバージョンアップします。

build.gradle の dependencyManagement 内の記述を以下のように変更します。

dependencyManagement {
    imports {
        // BOM は https://repo.spring.io/release/io/spring/platform/platform-bom/Brussels-SR3/
        // の下を見ること
        mavenBom("io.spring.platform:platform-bom:Brussels-SR3") {
            bomProperty 'guava.version', '22.0'
            bomProperty 'thymeleaf.version', '3.0.7.RELEASE'
            bomProperty 'thymeleaf-extras-springsecurity4.version', '3.0.2.RELEASE'
            bomProperty 'thymeleaf-layout-dialect.version', '2.2.2'
            bomProperty 'thymeleaf-extras-data-attribute.version', '2.0.1'
            bomProperty 'thymeleaf-extras-java8time.version', '3.0.0.RELEASE'
        }
    }
}
  • bomProperty 'thymeleaf.version', '3.0.6.RELEASE'bomProperty 'thymeleaf.version', '3.0.7.RELEASE' に変更します。

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

HTML ファイルから Thymeleaf テンプレートファイルを作成する

src/main/resources/templates の下に web/common, web/inquiry の2つのディレクトリを新規作成します。

まずは src/main/resources/templates/web/common の下に共通部分定義用の fragments.html を作成します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="common_header(title, links, style)">
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title th:replace="${title} ?: _">画面名</title>
  <!-- Tell the browser to be responsive to screen width -->
  <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
  <!-- Bootstrap 3.3.6 -->
  <link rel="stylesheet" href="/vendor/bootstrap/css/bootstrap.min.css">
  <!-- Font Awesome -->
  <link rel="stylesheet" href="/vendor/font-awesome/css/font-awesome.min.css">
  <!-- Ionicons -->
  <link rel="stylesheet" href="/vendor/ionicons/css/ionicons.min.css">
  <!-- Theme style -->
  <link rel="stylesheet" href="/vendor/admin-lte/css/AdminLTE.min.css">
  <link rel="stylesheet" href="/vendor/admin-lte/css/skins/skin-blue.min.css">

  <!-- ここに各htmlで定義された link タグが追加される -->
  <th:block th:replace="${links} ?: _"/>

  <!-- 画面共通で使用する css -->
  <link rel="stylesheet" href="/css/common.min.css">

  <!-- ここに各htmlで定義された style タグが追加される -->
  <th:block th:replace="${style} ?: _"/>
</head>

<body class="skin-blue layout-top-nav">
<div class="wrapper">
</div>
</body>
</html>
  • このファイルも Thymeleaf テンプレートファイルなので、<html> ではなく <html xmlns:th="http://www.thymeleaf.org"> を記述します。
  • <head> タグには th:fragment="common_header(title, links, style)" を追加します。
  • <title> タグには th:replace="${title} ?: _" を追加します。
  • <!-- ここに各htmlで定義された link タグが追加される --> <th:block th:replace="${links} ?: _"/> を追加します。
  • <!-- ここに各htmlで定義された style タグが追加される --> <th:block th:replace="${style} ?: _"/> を追加します。

次に、src/main/resources/templates/web/inquiry の下に static/inquiry の下の全てのファイルをコピーした後、以下の点を変更します。

  • <html><html xmlns:th="http://www.thymeleaf.org"> に変更します。
  • <head><head th:replace="~{web/common/fragments :: common_header(~{::title}, ~{::link}, ~{::style})}"> に変更します。
  • <head> ... </head> 内は以下の記述のみ残します。
    • <title> ... </title>
    • 独自に定義した <link ...>
    • 独自に定義した <style> ... </style>
  • complete.html 以外は <form> タグの最後に th:action="@{/inquiry/input/01/}" を追加します(@{...} の中は自分の画面の URL にします)。これにより CSRF対策用のトークンが自動で挿入されます。

各ファイルの body タグより上の記述は以下のようになります。

■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>

■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>

■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>

■src/main/resources/templates/web/inquiry/confirm.html

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

  <style>
    /* セルの上部の罫線を表示しないようようにし、セル内の余白を詰める */
    .table > tbody> tr > th,
    .table > tbody> tr > td {
      border-top: none;
      padding: 5px;
    }
  </style>
</head>

■src/main/resources/templates/web/inquiry/complete.html

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

ksbysample-webapp-lending から config ディレクトリをコピーする

Project 作成時に config ディレクトリの下の checkstyle, findbugs, pmd 用の設定ファイルをコピーするのを忘れていました。ksbysample-webapp-lending から以下のファイルを config ディレクトリごとコピーします。

また IntelliJ IDEA のメインメニューから「File」-「Settings」を選択して「Settings」ダイアログを表示した後、CheckstyleFindBugs-IDEA の設定を変更します。

画面左側の一覧から「Other Settings」-「Checkstyle」を選択した後、画面右側の「Configuration File」の右にある「+」ボタンをクリックします。

f:id:ksby:20170806193223p:plain

ダイアログが表示されますので、以下の画像のように入力した後「Next」ボタンをクリックします。

f:id:ksby:20170806193337p:plain

  • 「Description」に google_checks.xml を入力します。
  • 「Use a local Checkstyle file」の「File」に C:\project-springboot\ksbysample-boot-miscellaneous\boot-npm-geb-sample\config\checkstyle\google_checks.xml を入力し、「Store relative to project location」をチェックします。

“The rules file has been validated and is ready to add.” のメッセージの画面が表示されたら「Finish」ボタンをクリックします。

「Settings」ダイアログに戻ると「Configuration File」に “google_checks.xml” が追加されていますので、左側の「Active」のチェックボックスをチェックした後「Apply」ボタンをクリックします。

f:id:ksby:20170806193723p:plain

画面左側の一覧から「Other Settings」-「FindBugs-IDEA」を選択した後、画面右側の「Filter」タブをクリックしてから画面中央の「Exclude filter files」の「+」ボタンをクリックします。

f:id:ksby:20170807002949p:plain

「Exclude Filter Files」ダイアログが表示されますので、C:\project-springboot\ksbysample-boot-miscellaneous\boot-npm-geb-sample\config\findbugs\findbugs-exclude.xml を選択して「OK」ボタンをクリックします。

f:id:ksby:20170807003204p:plain

「Settings」ダイアログに戻ると「Exclude filter files」に C:\project-springboot\ksbysample-boot-miscellaneous\boot-npm-geb-sample\config\findbugs\findbugs-exclude.xml が表示されています。「OK」ボタンをクリックしてダイアログを閉じます。

f:id:ksby:20170807003338p:plain

URL を定義する定数クラスを作成する

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

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

package ksbysample.webapp.bootnpmgeb.constants;

/**
 * URL 定数定義用クラス
 */
public class UrlConst {

    public static final String URL_INQUIRY_BASE = "/inquiry";
    public static final String URL_INQUIRY_INPUT_01 = URL_INQUIRY_BASE + "/input/01/";
    public static final String URL_INQUIRY_INPUT_02 = URL_INQUIRY_BASE + "/input/02/";
    public static final String URL_INQUIRY_INPUT_03 = URL_INQUIRY_BASE + "/input/03/";
    public static final String URL_INQUIRY_CONFIRM = URL_INQUIRY_BASE + "/confirm/";
    public static final String URL_INQUIRY_COMPLETE = URL_INQUIRY_BASE + "/complete/";

}

Controller クラスを作成する

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

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

package ksbysample.webapp.bootnpmgeb.web.inquiry;

import ksbysample.webapp.bootnpmgeb.constants.UrlConst;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
import org.springframework.web.util.UriComponentsBuilder;

/**
 * 入力画面1~3用 Controller クラス
 */
@Controller
@RequestMapping("/inquiry/input")
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";

    /**
     * 入力画面1 初期表示処理
     *
     * @return 入力画面1の Thymeleaf テンプレートファイルのパス
     */
    @GetMapping("/01")
    public String input01() {
        return TEMPLATE_INPUT01;
    }

    /**
     * 入力画面1 「次へ」ボタンクリック時の処理
     *
     * @return 入力画面2の URL
     */
    @PostMapping(value = "/01", params = {"move=next"})
    public String input01MoveNext(UriComponentsBuilder builder) {
        return UrlBasedViewResolver.REDIRECT_URL_PREFIX
                + builder.path(UrlConst.URL_INQUIRY_INPUT_02).toUriString();
    }

    /**
     * 入力画面2 初期表示処理
     *
     * @return 入力画面2の Thymeleaf テンプレートファイルのパス
     */
    @GetMapping("/02")
    public String input02() {
        return TEMPLATE_INPUT02;
    }

    /**
     * 入力画面2 「前へ」ボタンクリック時の処理
     *
     * @return 入力画面1の URL
     */
    @PostMapping(value = "/02", params = {"move=back"})
    public String input02MoveBack(UriComponentsBuilder builder) {
        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(UriComponentsBuilder builder) {
        return UrlBasedViewResolver.REDIRECT_URL_PREFIX
                + builder.path(UrlConst.URL_INQUIRY_INPUT_03).toUriString();
    }

    /**
     * 入力画面3 初期表示処理
     *
     * @return 入力画面3の Thymeleaf テンプレートファイルのパス
     */
    @GetMapping("/03")
    public String input03() {
        return TEMPLATE_INPUT03;
    }

    /**
     * 入力画面3 「前へ」ボタンクリック時の処理
     *
     * @return 入力画面2の URL
     */
    @PostMapping(value = "/03", params = {"move=back"})
    public String input03MoveBack(UriComponentsBuilder builder) {
        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(UriComponentsBuilder builder) {
        return UrlBasedViewResolver.REDIRECT_URL_PREFIX
                + builder.path(UrlConst.URL_INQUIRY_CONFIRM).toUriString();
    }

}

画面遷移用の js ファイルを実装する

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

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"]
    },
    output: {
        path: __dirname + "/src/main/resources/static",
        publicPath: "/",
        filename: "[name].js"
    }
};
  • 以下の3行を追加します。
    • "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 ファイル実装前に npm run server コマンドを実行しておきます。

src/main/assets/js/inquiry の下に input01.js を新規作成し、以下の内容を記述します。

var $ = require("admin-lte/plugins/jQuery/jquery-2.2.3.min.js");

$(document).ready(function () {
    // 動作確認のために初期表示時に「次へ」ボタンをクリック可能にする
    $(".js-btn-next").prop("disabled", false);

    $(".js-btn-next").on("click", function (event) {
        $("#input01Form").attr("action", "/inquiry/input/01/?move=next");
        $("#input01Form").submit();

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

src/main/assets/js/inquiry の下に input02.js を新規作成し、以下の内容を記述します。

var $ = require("admin-lte/plugins/jQuery/jquery-2.2.3.min.js");

$(document).ready(function () {
    // 動作確認のために初期表示時に「次へ」ボタンをクリック可能にする
    $(".js-btn-next").prop("disabled", false);

    $(".js-btn-back").on("click", function (event) {
        $("#input02Form").attr("action", "/inquiry/input/02/?move=back");
        $("#input02Form").submit();
        return false;
    })

    $(".js-btn-next").on("click", function (event) {
        $("#input02Form").attr("action", "/inquiry/input/02/?move=next");
        $("#input02Form").submit();
        return false;
    })
});

src/main/assets/js/inquiry の下に input03.js を新規作成し、以下の内容を記述します。

var $ = require("admin-lte/plugins/jQuery/jquery-2.2.3.min.js");

$(document).ready(function () {
    // 動作確認のために初期表示時に「確認画面へ」ボタンをクリック可能にする
    $(".js-btn-confirm").prop("disabled", false);

    $(".js-btn-back").on("click", function (event) {
        $("#input03Form").attr("action", "/inquiry/input/03/?move=back");
        $("#input03Form").submit();
        return false;
    })

    $(".js-btn-confirm").on("click", function (event) {
        $("#input03Form").attr("action", "/inquiry/input/03/?move=next");
        $("#input03Form").submit();
        return false;
    })
});

動作確認

Tomcat を起動します。

ブラウザを起動し http://localhost:8080/inquiry/input/01/ にアクセスすると入力画面1が表示されて、「次へ」ボタンもクリックできるようになっています。

f:id:ksby:20170811004724p:plain

「次へ」ボタンをクリックすると入力画面2へ遷移します。アドレスバーに表示される URL は http://localhost:8080/inquiry/input/02/ に変わっています。

f:id:ksby:20170811004855p:plain

「次へ」ボタンをクリックすると入力画面3へ遷移します。アドレスバーに表示される URL は http://localhost:8080/inquiry/input/03/ に変わっています。

f:id:ksby:20170811005023p:plain

「前の画面へ戻る」ボタンをクリックすると入力画面3→入力画面2→入力画面1へ戻り、URL もきちんと変わりました。

メモ書き

  • 現在の設定では、js ファイルを修正すると webpack の watch 機能で検知されて自動でバンドルされた js ファイルが出力されますが、IntelliJ IDEA で Ctrl+F9 を押して build しないとブラウザに反映されませんでした。browser-sync で html を作成していた時と比較してかなり手間がかかるようになってしまいました。もう少し開発しやすくできないものでしょうか。。。

履歴

2017/08/11
初版発行。
2017/09/02
<html xmlns:th="http://www.w3.org/1999/xhtml"> と間違えて記述していた箇所があったので <html xmlns:th="http://www.thymeleaf.org"> へ修正した。