Help us understand the problem. What is going on with this article?

Thymeleaf 3.0を使用した入力フォームのサンプル

More than 1 year has passed since last update.

概要

2015年に投稿したThymeleafを使用した入力フォームのサンプルコードという記事の改定版です。
Spring Bootが標準サポートするテンプレートエンジンのThymeleafを使用した入力フォームのサンプルコードになります。

ソースコードはrubytomato/demo-bootstrap4-thymeleaf-spring2にあります。

環境

  • Windows 10 professional
  • Java 1.8.0_162
  • Thymeleaf 3.0.9
    • Spring Boot 2.0.1
  • Bootstrap 4.1

参考

サンプルコード

入力画面

simple input form.png

テンプレートファイル

template
<form action="#" th:action="@{/simple}" th:object="${simpleForm}" method="post">
    <!-- text -->
    <div class="col-md-9 mb-3">
        <label class="control-label" for="singleLineText">Single Line Text <span class="text-muted">(2 - 120)</span></label>
        <input type="text" class="form-control" th:field="*{singleLineText}" />
        <div th:if="${#fields.hasErrors('singleLineText')}" th:errors="*{singleLineText}" class="help-block">error</div>
    </div>
    <!-- date type text -->
    <div class="col-md-9 mb-3">
        <label class="control-label" for="textDate">Date (text type) <span class="text-muted">(yyyy/mm/dd)</span></label>
        <input type="datetime" class="form-control" th:field="*{textDate}" placeholder="yyyy/mm/dd" />
        <div th:if="${#fields.hasErrors('textDate')}" th:errors="*{textDate}" class="help-block">error</div>
    </div>
    <!-- date -->
    <div class="col-md-9 mb-3">
        <label class="control-label" for="date">Date <span class="text-muted">(yyyy/mm/dd)</span></label>
        <input type="datetime" class="form-control" th:field="*{date}" placeholder="yyyy/mm/dd" />
        <div th:if="${#fields.hasErrors('date')}" th:errors="*{date}" class="help-block">error</div>
    </div>
    <!-- number type text -->
    <div class="col-md-9 mb-3">
        <label class="control-label" for="textNum">Number (text type) <span class="text-muted">(0 - 999999999)</span></label>
        <input type="number" class="form-control" th:field="*{textNum}" />
        <div th:if="${#fields.hasErrors('textNum')}" th:errors="*{textNum}" class="help-block">error</div>
    </div>
    <!-- number -->
    <div class="col-md-9 mb-3">
        <label class="control-label" for="num">Number <span class="text-muted">(0 - 999999999)</span></label>
        <input type="number" class="form-control" th:field="*{num}" />
        <div th:if="${#fields.hasErrors('num')}" th:errors="*{num}" class="help-block">error</div>
    </div>
    <!-- text area -->
    <div class="col-md-9 mb-3">
        <label class="control-label" for="multiLineText">Text Area <span class="text-muted">(Optional, 10 - 600)</span></label>
        <textarea rows="3" cols="80" class="form-control" th:field="*{multiLineText}"></textarea>
        <div th:if="${#fields.hasErrors('multiLineText')}" th:errors="*{multiLineText}" class="help-block">error</div>
    </div>
    <!-- email -->
    <div class="col-md-9 mb-3">
        <label class="control-label" for="email">Email <span class="text-muted">(Optional)</span></label>
        <input type="email" class="form-control" th:field="*{email}" placeholder="you@example.com" />
        <div th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="help-block">error</div>
    </div>
    <!-- password -->
    <div class="col-md-9 mb-3">
        <label class="control-label" for="password">Password <span class="text-muted">(6 - 99)</span></label>
        <input type="password" class="form-control" th:field="*{password}" />
        <div th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="help-block">error</div>
    </div>

    <hr class="mb-4">

    <!-- select single -->
    <div class="col-md-9 mb-3">
        <label class="control-label" for="singleSelect">Single Select <span class="text-muted">(Optional)</span></label>
        <select class="custom-select d-block w-100" th:field="*{singleSelect}">
            <option value="">---</option>
            <option th:each="item : ${selectItems}" th:value="${item.value}" th:text="${item.key}">pulldown</option>
        </select>
        <div th:if="${#fields.hasErrors('singleSelect')}" th:errors="*{singleSelect}" class="help-block">error</div>
    </div>

    <!-- select multi -->
    <div class="col-md-9 mb-3">
        <label class="control-label" for="multiSelects">Multi Select</label>
        <select class="form-control" th:field="*{multiSelects}" multiple="multiple" size="4">
            <option th:each="item : ${selectItems}" th:value="${item.value}" th:text="${item.key}">pulldown</option>
        </select>
        <div th:if="${#fields.hasErrors('multiSelects')}" th:errors="*{multiSelects}" class="help-block">error</div>
    </div>

    <hr class="mb-4">

    <!-- checkbox single -->
    <div class="col-md-9 mb-3">
        <label class="control-label">Single Checkbox</label>
        <div class="custom-control custom-checkbox">
            <input type="checkbox" class="custom-control-input" th:id="${#ids.seq('singleCheck')}" th:field="*{singleCheck}" value="on" />
            <label class="custom-control-label" th:for="${#ids.prev('singleCheck')}">Active</label>
        </div>
        <div th:if="${#fields.hasErrors('singleCheck')}" th:errors="*{singleCheck}" class="help-block">error</div>
    </div>

    <!-- checkbox multi -->
    <div class="col-md-9 mb-3">
        <label class="control-label">Multi Checkbox</label>
        <div class="custom-control custom-checkbox" th:each="item : ${checkItems}">
            <input type="checkbox" class="custom-control-input" th:value="${item.value}" th:field="*{multiChecks}" />
            <label class="custom-control-label" th:for="${#ids.prev('multiChecks')}" th:text="${item.key}"></label>
        </div>
        <div th:if="${#fields.hasErrors('multiChecks')}" th:errors="*{multiChecks}" class="help-block">error</div>
    </div>

    <hr class="mb-4">

    <!-- radio -->
    <div class="col-md-9 mb-3">
        <label class="control-label">Radio Button</label>
        <div class="custom-control custom-radio" th:each="item : ${radioItems}">
            <input type="radio" class="custom-control-input" th:value="${item.value}" th:field="*{radio}" />
            <label class="custom-control-label" th:for="${#ids.prev('radio')}" th:text="${item.key}"></label>
        </div>
        <div th:if="${#fields.hasErrors('radio')}" th:errors="*{radio}" class="help-block">error</div>
    </div>
    <button class="btn btn-primary btn-lg btn-block" type="submit">confirm</button>
</form>

Thymeleafで入力フォームを組み立てるときにth:field属性を利用すると便利です。なお、この属性はThymeleafの標準機能ではなくThymeleaf-Spring5で提供されている拡張機能です。

input

th:fieldにはフォームBeanのフィールドを指定します。
以下の例のようにid、name、value属性が自動的に設定されます。

template
<input type="text" th:field="*{singleLineText}" />
render
<input type="text" id="singleLineText" name="singleLineText" value="">

checkbox

checkboxでもth:fieldを利用することができます。ただしid属性は一意になるように#idsを使って上書きします。
フォームBeanに初期値が設定されていればchecked="checked"が自動的に付加されます。
またチェックの無いチェックボックスを送信しないブラウザの問題を避けるためにThymeleafによって自動的にhiddenフィールドが挿入されます。

Don’t worry about those hidden inputs with name="_features": they are automatically added in order to avoid problems with browsers not sending unchecked checkbox values to the server upon form submission.

template
<div th:each="item : ${checkItems}">
  <input type="checkbox" th:value="${item.value}" th:field="*{multiChecks}" />
  <label th:for="${#ids.prev('multiChecks')}" th:text="${item.key}" />
</div>
render
<div>
  <input type="checkbox" id="multiChecks1" value="A" name="multiChecks">
  <input type="hidden" name="_multiChecks" value="on">
  <label for="multiChecks1">checkbox_A</label>
</div>
<div>
  <input type="checkbox" id="multiChecks2" value="B" name="multiChecks">
  <input type="hidden" name="_multiChecks" value="on">
  <label for="multiChecks2">checkbox_B</label>
</div>
<div>
  <input type="checkbox" id="multiChecks3" value="C" name="multiChecks">
  <input type="hidden" name="_multiChecks" value="on">
  <label for="multiChecks3">checkbox_C</label>
</div>
<div>
  <input type="checkbox" id="multiChecks4" value="D" name="multiChecks">
  <input type="hidden" name="_multiChecks" value="on">
  <label for="multiChecks4">checkbox_D</label>
</div>
<div>
  <input type="checkbox" id="multiChecks5" value="E" name="multiChecks">
  <input type="hidden" name="_multiChecks" value="on">
  <label for="multiChecks5">checkbox_E</label>
</div>

radio

checkboxと同様です。

template
<div th:each="item : ${radioItems}">
  <input type="radio" th:value="${item.value}" th:field="*{radio}" />
  <label th:for="${#ids.prev('radio')}" th:text="${item.key}" />
</div>
render
<div>
  <input type="radio" value="A" id="radio1" name="radio">
  <label for="radio1">radio_A</label>
</div>
<div>
  <input type="radio" value="B" id="radio2" name="radio">
  <label for="radio2">radio_B</label>
</div>
<div>
  <input type="radio" value="C" id="radio3" name="radio">
  <label for="radio3">radio_C</label>
</div>
<div>
  <input type="radio" value="D" id="radio4" name="radio">
  <label for="radio4">radio_D</label>
</div>
<div>
  <input type="radio" value="E" id="radio5" name="radio">
  <label for="radio5">radio_E</label>
</div>

select

th:field属性を利用する場合はselectタグに指定します。checkboxやradioと同様にフォームBeanに初期値が設定されているとselected="selected"が自動的に付加されます。

template
<select th:field="*{multiSelects}" multiple="multiple" size="4">
  <option th:each="item : ${selectItems}" th:value="${item.value}" th:text="${item.key}" />
</select>
render
<select multiple="multiple" size="4" id="multiSelects" name="multiSelects">
  <option value="A">select_A</option>
  <option value="B">select_B</option>
  <option value="C">select_C</option>
  <option value="D">select_D</option>
  <option value="E">select_E</option>
</select>

確認画面

simple input confirm.png

テンプレートファイル

<table class="table table-striped table-bordered" th:object="${simpleForm}">
    <thead class="thead-dark">
    <tr>
        <th scope="col">field</th>
        <th scope="col">value</th>
    </tr>
    </thead>
    <tbody>
    <tr>
        <th scope="row">Single Line Text</th>
        <td th:text="*{singleLineText}"></td>
    </tr>
    <tr>
        <th scope="row">Date (text type)</th>
        <td th:text="*{textDate}"></td>
    </tr>
    <tr>
        <th scope="row">Date</th>
        <td th:text="*{#temporals.format(date, 'yyyy/MM/dd')}"></td>
    </tr>
    <tr>
        <th scope="row">Number (text type)</th>
        <td th:text="*{textNum}"></td>
    </tr>
    <tr>
        <th scope="row">Number</th>
        <td th:text="*{#numbers.formatInteger(num, 0, 'COMMA')}"></td>
    </tr>
    <tr>
        <th scope="row">Text Area</th>
        <td th:utext="*{multiLineText} ?: _">(Not entered)</td>
    </tr>
    <tr>
        <th scope="row">Text Area (new line to br)</th>
        <td th:utext="*{multiLineTextNl2br} ?: _">(Not entered)</td>
    </tr>
    <tr>
        <th scope="row">Email</th>
        <td th:text="*{email} ?: _">(Not entered)</td>
    </tr>
    <tr>
        <th scope="row">Password</th>
        <td th:text="*{password}"></td>
    </tr>
    <tr>
        <th scope="row">Single Select</th>
        <td>
            <p class="badge badge-primary" th:text="*{singleSelect}"></p>
        </td>
    </tr>
    <tr>
        <th scope="row">Multi Select</th>
        <td>
            <p class="badge badge-primary" th:each="c : *{multiSelects}" th:text="${c}"></p>
        </td>
    </tr>
    <tr>
        <th scope="row">Single Checkbox</th>
        <td>
            <p class="badge badge-primary" th:text="*{singleCheck}"></p>
        </td>
    </tr>
    <tr>
        <th scope="row">Multi Checkbox</th>
        <td>
            <p class="badge badge-primary" th:each="c : *{multiChecks}" th:text="${c}"></p>
        </td>
    </tr>
    <tr>
        <th scope="row">Radio Button</th>
        <td>
            <p class="badge badge-primary" th:text="*{radio}"></p>
        </td>
    </tr>
    </tbody>
</table>

日付のフォーマット

Date型の場合は#datesを使いますが、LocalDateやLocalDateTime型の場合は#temporalsを使う必要があります。
この機能を使うには以下のライブラリが必要です。

<dependency>
  <groupId>org.thymeleaf.extras</groupId>
  <artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>

サーバー側コード

フォームクラス

public class SimpleForm implements Serializable {

    @NotNull
    @Size(min = 2, max = 120)
    private String singleLineText;

    @NotNull
    @Pattern(regexp = "((19|[2-9][0-9])[0-9]{2})/(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])")
    private String textDate;

    @NotNull
    @DateTimeFormat(pattern = "yyyy/MM/dd")
    private LocalDate date;

    @NotNull
    @Digits(integer = 9, fraction = 0)
    private String textNum;

    @NotNull
    @Min(0)
    @Max(999999999)
    private Integer num;

    // Optional
    @Size(min = 10, max = 600)
    private String multiLineText;

    // Optional
    @Email
    private String email;

    @NotNull
    @Size(min = 6, max = 99)
    private String password;

    // Optional
    @Pattern(regexp = "A|B|C|D|E")
    private String singleSelect;

    // Optional
    @Size(min = 0, max = 5, message = "{custom.validation.constraints.SelectSize.message}")
    private String[] multiSelects;

    @Pattern(regexp = "on")
    private String singleCheck;

    @NotNull
    @Size(min = 1, max = 5, message = "{custom.validation.constraints.SelectSize.message}")
    private String[] multiChecks;

    @NotNull
    @Pattern(regexp = "A|B|C|D|E")
    private String radio;

    public String getMultiLineTextNl2br() {
        if (this.multiLineText == null || this.multiLineText.length() == 0) {
            return null;
        }
        return this.multiLineText.replaceAll("\n", "<br/>");
    }

}

コントローラ

@Controller
@RequestMapping("simple")
public class SimpleFormController {

    /**
     * selectの表示に使用するアイテム
     */
    final static Map<String, String> SELECT_ITEMS =
            Collections.unmodifiableMap(new LinkedHashMap<String, String>() {
                {
                    put("select_A", "A");
                    put("select_B", "B");
                    put("select_C", "C");
                    put("select_D", "D");
                    put("select_E", "E");
                }
            });

    /**
     * check boxの表示に使用するアイテム
     */
    final static Map<String, String> CHECK_ITEMS =
            Collections.unmodifiableMap(new LinkedHashMap<String, String>() {
                {
                    put("checkbox_A", "A");
                    put("checkbox_B", "B");
                    put("checkbox_C", "C");
                    put("checkbox_D", "D");
                    put("checkbox_E", "E");
                }
            });

    /**
     * radio buttonの表示に使用するアイテム
     */
    final static Map<String, String> RADIO_ITEMS =
            Collections.unmodifiableMap(new LinkedHashMap<String, String>() {
                {
                    put("radio_A", "A");
                    put("radio_B", "B");
                    put("radio_C", "C");
                    put("radio_D", "D");
                    put("radio_E", "E");
                }
            });

    @GetMapping
    public String input(SimpleForm form, Model model) {
        model.addAttribute("selectItems", SELECT_ITEMS);
        model.addAttribute("checkItems", CHECK_ITEMS);
        model.addAttribute("radioItems", RADIO_ITEMS);

        // default set
        form.setRadio("E");
        form.setMultiSelects(new String[]{"A", "B"});
        form.setMultiChecks(new String[]{"B", "D"});

        return "simpleInput";
    }

    @PostMapping
    public String conform(@Validated @ModelAttribute SimpleForm form, BindingResult result, Model model) {
        if (result.hasErrors()) {
            model.addAttribute("validationError", "不正な値が入力されました");
            return input(form, model);
        }
        return "simpleConfirm";
    }

}

補足

Thymeleaf 3.0

Thymeleaf 3 ten-minute migration guide

Thymeleaf 3.0は2.0と100%の互換があるということなので2.0のテンプレートファイルをそのまま利用できます。

Your existing Thymeleaf templates are almost 100% compatible with Thymeleaf 3 so you will only have to do a few modifications in your configuration.

Full HTML5 markup support

デフォルトのテンプレートモードがxhtmlからhtmlに変わりました。

Template modes

6つのテンプレートモード(html, xhtml, text, javascript, css, raw)があります。
htmlはバージョン4と5をサポートします。

  • markup template
    • html 4/5, xhtml
  • textual template
    • text, javascript, css
  • no-op template
    • raw

Fragment Expressions

Fragment式が強化されました。
Thymeleaf 3.0ではフラグメントの引数に~{テンプレート名::DOMセレクタ}という書式でDOMを渡すことができます。

次の例は_header.htmlというテンプレートファイルのheaderフラグメントで、headタグを置き換えています。
headerフラグメントの引数に渡しているのはhello.htmlテンプレートファイルのtitle属性です。
自分自身の属性を渡す場合はテンプレート名を省略してheader(~{::title})のように記述できます。

hello.html
<head th:replace="_header::header(~{hello::title})">
  <title>thymeleaf simple demonstration</title>
</head>
_header.html
<head th:fragment="header(title)">
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
  <link href="css/starter-template.css" rel="stylesheet">
  <link rel="icon" href="icon/favicon.ico">
  <title th:replace="${title}"></title>
</head>
render
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
  <link href="css/starter-template.css" rel="stylesheet">
  <link rel="icon" href="icon/favicon.ico">
  <title>thymeleaf simple demonstration</title>
</head>

No-Operation token

_(アンダースコア)で表すNO-OP(No Operation)トークンが追加されました。

次の例の通り、user.nameがnullの場合spanタグのテキスト("no user authenticated")が出力されます。

<span th:text="${user.name} ?: _">no user authenticated</span>

2.0まではDefault expressions (Elvis operator)を使用していました。

<span th:text="${user.name} ?: 'no user authenticated'"></span>

Decoupled Template Logic

テンプレートファイルからロジックとマークアップを分離させる機能が追加されました。

[MAJOR FEAT] Decoupled template logic #465

  • Spring Boot 2.0で動作確認しようとしましたが、うまく動かなかったので未確認です。

Performance improvements

SpELコンパイルを有効にしてパフォーマンスを向上させることができます。
Spring Bootでは設定ファイルにSpELコンパイルを有効にする設定値が追加されています。(デフォルトはfalse)

spring:
  thymeleaf:
    enable-spring-el-compiler: true

ログ

開発時にThymeleafのログを出力するようにしておくと便利です。
ログの設定を行うとThymeleafのコンフィグレーションの内容やテンプレートファイルの処理時間などが出力されます。

logging:
  level:
    org.thymeleaf: DEBUG
    org.thymeleaf.TemplateEngine.CONFIG: TRACE
    org.thymeleaf.TemplateEngine.TIMER: TRACE
    org.thymeleaf.TemplateEngine.cache.TEMPLATE_CACHE: TRACE

テンプレートモードがHTMLでDialectが2つ使用されていることがわかります。
またJava8TimeDialectで#temporalsが利用できるようになったことが確認できます。

[nio-9000-exec-1] org.thymeleaf.TemplateEngine             : [THYMELEAF] INITIALIZING TEMPLATE ENGINE
[nio-9000-exec-1] o.t.TemplateEngine.cache.TEMPLATE_CACHE  : [THYMELEAF][CACHE_INITIALIZE] Initializing cache TEMPLATE_CACHE. Max size: 200. Soft references are used.
[nio-9000-exec-1] org.thymeleaf.TemplateEngine.CONFIG      : Initializing Thymeleaf Template engine configuration...
[THYMELEAF] TEMPLATE ENGINE CONFIGURATION:
[THYMELEAF] * Thymeleaf version: 3.0.9.RELEASE (built 2017-11-05T00:10:15+0000)
[THYMELEAF] * Cache Manager implementation: org.thymeleaf.cache.StandardCacheManager
[THYMELEAF] * Template resolvers:
[THYMELEAF]     * org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver
[THYMELEAF] * Message resolvers:
[THYMELEAF]     * org.thymeleaf.spring5.messageresolver.SpringMessageResolver
[THYMELEAF] * Link builders:
[THYMELEAF]     * org.thymeleaf.linkbuilder.StandardLinkBuilder
[THYMELEAF] * Dialect [1 of 2]: SpringStandard (org.thymeleaf.spring5.dialect.SpringStandardDialect)
[THYMELEAF]     * Prefix: "th"
[THYMELEAF]     * Processors for Template Mode: HTML

// 省略

[THYMELEAF] * Dialect [2 of 2]: java8time (org.thymeleaf.extras.java8time.dialect.Java8TimeDialect)
[THYMELEAF]     * Expression Objects:
[THYMELEAF]         * #temporals
[THYMELEAF] TEMPLATE ENGINE CONFIGURED OK
rubytomato@github
今までJavaをメインにやってきましたが、JavaScript(Node.js)の習得に取り組み始めました。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした