【Java Web入門 #18】Thymeleafの利用(5) | 式と属性を利用した表示
2025.04.11
前回までの記事では、利用できるオブジェクトや式、属性といった Thymeleaf の基本的な構成要素について説明しました。当記事ではこれらを実際に組み合わせた利用例とポイントについて、サンプルコードを交えて説明します。
◆Java Web入門の過去記事はこちら
◆Java入門 記事一覧はこちら
目次
式・属性の利用例
条件に合致した場合にメッセージを表示
エラーメッセージや完了メッセージなど、特定の条件を満たした場合にメッセージ欄を表示する場合は、th:if 属性や th:switch/th:case 属性を使って条件判定を利用するのが簡単です。
以下のサンプルでは式ユーティリティオブジェクト #strings を利用して文字列が空かどうかを判定したり、Java のコード側の処理で設定した表示モードに応じて表示するメッセージを切り替えています。
<!-- エラーコード(errCd)が空ではないかどうかを th:if で判定 -->
<div class="errmsg" th:if="${not #strings.isEmpty(errCd)}" th:text="#{errmessage}" >
エラーメッセージを表示
</div>
<!-- 指定された表示モードでメッセージ欄を表示 -->
<div th:switch="${mode}">
<div class="errmsg" th:case="error" th:text="#{errmessage}" >
エラーメッセージを表示
</div>
<div class="confmsg" th:case="confirm" th:text="#{confmessage}" >
確認メッセージを表示
</div>
<div class="compmsg" th:case="complete" th:text="#{compmessage}" >
完了メッセージを表示
</div>
</div>
同一の判定条件に合致した場合/しない場合で別々の表示としたい場合は、th:if 属性と th:unless 属性を利用するのがわかりやすいケースも考えられます。どの属性を利用するかは、判定の内容や将来的な拡張(分岐が増えるような条件かどうか、など)を考慮して判断するのが良いでしょう。
<!-- エラーコードが空の場合は完了メッセージ -->
<div class="compmsg"
th:if="${#strings.isEmpty(errCd)}" th:text="#{compmessage}" >
完了メッセージを表示
</div>
<!-- エラーコードが空ではない場合はエラーメッセージ -->
<div class="errmsg"
th:unless="${#strings.isEmpty(errCd)}" th:text="#{errmessage}" >
エラーメッセージを表示
</div>
動的に外部メッセージを表示する
メッセージを取得するキーが動的に変わる場合は、メッセージの式ユーティリティオブジェクト #messages を利用して取得することができます。(式ユーティリティオブジェクトの利用時はメッセージ式 #{ ... } ではなく、変数式 ${ ... } となっていることに注意してください)
<div class="errmsg" th:if="${msgId ne null}" th:text="${#messages.msg(msgId)}" />
エラーメッセージを表示
</div>
実際のアプリケーション開発では、様々なチェックを同時に行い、エラーをまとめて出力するケースが多くあります。このような場合、置換パラメータの数が異なる複数の外部メッセージを動的に表示する必要があります。
外部メッセージに可変長の置換パラメーターを引き渡して表示するには、式ユーティリティオブジェクト #messages の mgeWithParams メソッドを利用することができます。
<ul class="errmsg">
<li th:text="${#messages.msgWithParams(msgId, msgParams)}">
エラーメッセージを表示
</li>
</ul>
繰り返し処理と合わせて、実際に複数個のメッセージを表示してみます。今回はメッセージによって置換パラメーターの数が異なるケースを想定して、以下のような外部メッセージ(messages.properties など)を準備しておきます。
errmsg.required={0}を入力してください。 errmsg.len={0}は{1}桁の{2}で入力してください。 errmsg.min={0}は{1}桁以上の{2}で入力してください。 errmsg.max={0}は{1}桁以下の{2}で入力してください。 errmsg.minmax={0}は{1}桁以上{2}桁以下の{3}で入力してください。 errmsg.select={0}を選択してください。 errmsg.fatal=申し訳ございません。システムエラーが発生しました。 |
次に、エラー情報を格納するBeanクラスを準備します。式ユーティリティオブジェクト #messages.msgWithParams の処理は置換パラメーターに対応する引数が配列であるため、パラメーターを配列で取得できるように getParams メソッドを準備しました。
このように「get」を先頭につけた名前のメソッドを準備しておくことで、Thymeleaf のテンプレート上でプロパティと同じように値を利用できます。
package com.example.demo.beans;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
@Data
public class ErrMsgInfo {
private String id;
private List<Object> paramList;
public void addParam(Object o) {
if(paramList == null) paramList = new ArrayList<>();
paramList.add(o);
}
public Object[] getParams() {
return paramList == null ? null : paramList.toArray();
}
}
コントローラークラスは以下のような感じです。チェック処理の部分は本来何らかの処理が入りますが、ここではメッセージを直接追加しておきます。SpringBoot のバリデーションチェックを行っているのであれば、チェック結果(BindingResult)の内容からメッセージ情報を作成しても良いでしょう。
package com.example.demo.controller;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import com.example.demo.beans.ErrMsgInfo;
@Controller
public class ExampleController {
@GetMapping("/example")
public String example(Model model) {
List<ErrMsgInfo> errMsgList = new ArrayList<>();
// 本来はここで何らかのチェック処理をして、エラー情報を格納 //
ErrMsgInfo err = new ErrMsgInfo();
err.setId("required");
err.addParam("氏名");
errMsgList.add(err);
err = new ErrMsgInfo();
err.setId("len");
err.addParam("郵便番号");
err.addParam(7);
err.addParam("数字");
errMsgList.add(err);
err = new ErrMsgInfo();
err.setId("max");
err.addParam("住所");
err.addParam(200);
err.addParam("文字列");
errMsgList.add(err);
err = new ErrMsgInfo();
err.setId("fatal");
errMsgList.add(err);
model.addAttribute("errList", errMsgList);
return "example";
}
}
Thymeleaf のテンプレート(example.html)は以下のとおり準備します。渡されたエラー情報の数だけ、外部メッセージを取得して表示します。
<ul class="errmsg" >
<li th:each="err : ${errList}"
th:text="${#messages.msgWithParams('errmsg.' + err.id, err.params)}">
氏名を入力してください。
</li>
</ul>
置換パラメーターが置き換えられて、メッセージが表示されました。置換パラメーターのないメッセージも正しく表示できています。

複数の画面で同じようにエラーメッセージを表示するのであれば、フラグメントとして定義しておき、各画面で挿入することもできます。
<!-- フラグメントとして定義しておく -->
<ul class="errmsg" th:fragment="errorMessages">
<li th:each="err : ${errList}"
th:text="#messages.msgWithParams('errmsg.' + ${err.id}, ${err.params})">
氏名を入力してください。
</li>
</ul>
<!-- フラグメントを共通部品(メッセージ表示)として利用 -->
<ul class="errmsg" th:replace="~{common :: errorMessages}">
<li>氏名を入力してください。</li>
</ul>
もしプロトタイプ(静的HTML)ではエラーメッセージは表示せず、アプリケーションとして動作時のみエラーメッセージを表示するのであれば、以下のように疑似ブロックを利用して、フラグメントを挿入することもできます。
<!-- フラグメントを共通部品(メッセージ表示)として利用 -->
<th:block th:replace="~{common :: errorMessages}" />
複数行に対し交互に別のスタイルを適用する
ECサイトの商品一覧のように、多くのデータを一覧表示する際には、奇数行・偶数行の背景色を変えて表示するケースが良くあります。このように、複数行のデータ表示に対して、交互に行のスタイルを変更して表示するには、th:each 属性による繰り返し処理で取得できるループの情報が利用できます。
下記のサンプルコードでは、ループ情報 loopInfo と th:classappend 属性を利用して、奇数行、偶数行ごとに異なるスタイルを適用しています。
<table>
<tr class="headerrow">
<th>#</th>
<th>商品名</th>
<th>在庫数</th>
<th>税込価格(円)</th>
</tr>
<tr class="datarow" th:each="item, loopInfo : ${items}" th:object="${item}"
th:classappend="${loopInfo.odd ? 'oddrow' : 'evenrow'}">
<td th:text="*{name}">有機ELテレビ 43V型</td>
<td th:text="*{color}">黒</td>
<td th:text="*{stock}">2</td>
<td th:text="*{price}">58,300</td>
</tr>
</table>
上記のサンプルコードでは、基本となる行のスタイル(datarow)を全行に適用したうえで、奇数行・偶数行各々に追加(または上書き)したいスタイル(oddrow・evenrow)を付加しています。th:class 属性ではなく th:classappend 属性を利用することで、基本となる行のスタイル(datarow)にスタイルを「追加して」適用することができます。


もちろん、スタイルを付加するのではなく書き換えてしまいたい場合は、th:style 属性を利用してもかまいません。また、奇数行または偶数行のどちらかにだけスタイルを付加して、付加しない行はデフォルトのスタイルのままにしたい場合などでは、付加しない側を空文字としておくことも可能です。
<!-- 偶数(even)行目だけ追加スタイルを付加 -->
<tr th:each="item, loopInfo : ${items}"
th:class="${loopInfo.even ? 'evenrow' : ''}">
プロトタイプに複数行のデータを表示
Thymeleaf で作成したテンプレートを、そのままプロトタイプとして(静的HTML表示して)確認する際、1ページあたりの最大行数で表示したい、など、データ行を複数行準備しておきたい場合もあります。
このような場合は、th:remove 属性が役立ちます。th:remove 属性の値を「all-but-first」とすることで、1番目の子要素だけを残し、以降の子要素を削除します。同時に、削除されずに残る1番目の子要素に th:each 属性を設定することで、繰り返し当該要素を繰り返し出力することができます。
<li th:remove="all-but-first">
<ul th:each="msg : ${messageList}" th:text="${msg}">メッセージ1</ul>
<ul>メッセージ2</ul>
<ul>メッセージ3</ul>
</ul>
Table要素でヘッダー行を表示して、以降のデータ行だけ繰り返し表示をしたい場合は、table要素の直下に tr 要素を記述せずに、tbody 要素を利用することで実現できます。
<div th:text="${areaName}">北海道・東北</div>
<table>
<thead>
<tr>
<th>県コード</th>
<th>都道府県</th>
<th>県庁所在地</th>
</tr>
</thead>
<tbody th:remove="all-but-first">
<tr th:each="pref: ${prefList}" th:object="${pref}">
<td th:text="*{code}">01</td>
<td th:text="*{name}">北海道</td>
<td th:text="*{capital}">札幌市</td>
</tr>
<tr>
<td>02</td><td>青森県</td><td>青森市</td>
</tr>
<tr>
<td>03</td><td>岩手県</td><td>盛岡市</td>
</tr>
<tr>
<td>04</td><td>宮城県</td><td>仙台市</td>
</tr>
<tr>
<td>05</td><td>秋田県</td><td>秋田市</td>
</tr>
<tr>
<td>06</td><td>山形県</td><td>山形市</td>
</tr>
<tr>
<td>07</td><td>福島県</td><td>福島市</td>
</tr>
</tbody>
</table>
tbody 要素以外に、 Thymeleaf の疑似ブロック要素 th:block とプロトタイプのみのコメント(<!--/*/ ... /*/-->)の組み合わせを利用することで「複数行の繰り返し」も可能です。
<table>
<tr>
<th>県コード</th>
<th>都道府県</th>
</tr>
<tr>
<th colspan="2">県庁所在地</th>
</tr>
<!--/*/ <th:block th:remove="all-but-first"> /*/-->
<!--/*/ <th:block th:each="pref: ${prefList}" th:object="${pref}"> /*/-->
<tr>
<td th:text="*{code}">01</td>
<td th:text="*{name}">北海道</td>
</tr>
<tr>
<td colspan="2" th:text="*{capital}">札幌市</td>
</tr>
<!--/*/ </th:block> /*/-->
<tr><td>02</td><td>青森県</td></tr>
<tr><td colspan="2">青森市</td></tr>
~中略~
<tr><td>07</td><td>福島県</td></tr>
<tr><td colspan="2">福島市</td></tr>
<!--/*/ </th:block> /*/-->
</table>
式とプリプロセッシング
Thymeleaf の属性値に式を利用する場合、「式自体を動的に指定したい」といった場合があります。例えば、同じ画面でも一覧に表示する項目を動的に変更したい、というようなケースです。
例えば、様々な商品を扱うショッピングサイトで、商品が「カテゴリ(category)」「商品名(name)」「税込価格(price)」の他に「色(color)」「サイズ(size)」「賞味期限(expiration)」「容量(volume)」・・・といった様々な情報を持っているものとします。
Java のコードとしては、商品情報 Bean が以下のような情報を持っているイメージです。便宜上、ここでは全ての情報をString型にしています。
(商品情報Bean クラス)
package com.example.demo.beans;
import lombok.Data;
@Data
public class ItemInfo {
private String category;
private String name;
private String price;
private String color;
private String size;
private String expiration;
private String volume;
private String stock;
・・・(以下略)
}
※getter・setter は lombok を利用して生成しています。
これらの商品は、「食品」「家具」など、カテゴリごとにポイントとなる情報が違うため、カテゴリごとに異なる項目で一覧表示を行いたいと考えた場合、どのような方法が考えられるでしょうか。
一番単純なのは、カテゴリごとに一覧のテンプレートを作成することです。例えば、食品カテゴリの一覧は以下の内容で作成します。
<!-- 食品の一覧 -->
<table>
<tr>
<th>商品名</th>
<th>容量</th>
<th>賞味期限</th>
<th>税込価格(円)</th>
</tr>
<tr th:each="item : ${itemList}" th:object="${item}">
<td th:text="*{name}">手作り味噌</td>
<td th:text="*{volume}">1kg</td>
<td th:text="*{expiration}">約6か月</td>
<td class="right" th:text="*{price}">1,620</td>
</tr>
</table>
これとは別に、家具カテゴリの一覧を以下の内容で作成します。それぞれ、表示するカテゴリに応じた情報の列を表示しています。
<!-- 家具の一覧 -->
<table>
<tr>
<th>商品名</th>
<th>色</th>
<th>サイズ</th>
<th>税込価格(円)</th>
</tr>
<tr th:each="item : ${itemList}" th:object="${item}">
<td th:text="*{name}">卓上ラック</td>
<td th:text="*{color}">ダークブラウン(木目調)</td>
<td th:text="*{size}">幅50×奥行20×高さ25(cm)</td>
<td class="right" th:text="*{price}">3,850</td>
</tr>
</table>
ただ、これだとカテゴリ毎にテンプレートを作成しなければならず、共通したデザインを変更したい場合などは全カテゴリのテンプレートの修正が必要になってしまいます。
他の方法としては、どのようなものが思いつくでしょうか。th:if 属性を利用して、カテゴリの条件に合致した場合だけ、列を表示してみることもできそうです。
<!-- カテゴリが一致した時だけ列を表示 -->
<table>
<tr>
<th>商品名</th>
<th th:if="${listCategory eq 'food'}">容量</th>
<th th:if="${listCategory eq 'food'}">賞味期限</th>
<th th:if="${listCategory eq 'furniture'}">色</th>
<th th:if="${listCategory eq 'furniture'}">サイズ</th>
<th>税込価格(円)</th>
</tr>
<tr th:each="item : ${itemList}" th:object="${item}">
<td th:text="*{name}">手作り味噌</td>
<td th:if="${listCategory eq 'food'}" th:text="*{volume}">1kg</td>
<td th:if="${listCategory eq 'food'}" th:text="*{expiration}">約6か月</td>
<td th:if="${listCategory eq 'furniture'}" th:text="*{color}">ダークブラウン(木目調)</td>
<td th:if="${listCategory eq 'furniture'}" th:text="*{size}">幅50×奥行20×高さ25(cm)</td>
<td class="right" th:text="*{price}">1,620</td>
</tr>
</table>
ちょっとごちゃごちゃした感じになってしまいました。各列を表示する条件を簡略化したり工夫はできそうですが、これだと静的HTMLでデザインを確認しようとした際に、全ての列が表示されてしまいます。
他の方法を考えてみます。商品情報 item のプロパティのうち、「表示するプロパティ」自体を動的に指定する方法があれば、これらの問題は解決できそうです。Thymeleaf では「プリプロセッシング」という仕組みを使って、式の文字列自体を動的に指定して実行することが可能です。
<!-- 商品一覧 -->
<table>
<tr>
<th>商品名</th>
<th th:text="#{thead.__${listfield1}__}">容量</th>
<th th:text="#{thead.__${listfield2}__}">賞味期限</th>
<th>税込価格(円)</th>
</tr>
<tr th:each="item : ${itemList}" th:object="${item}">
<td th:text="*{name}">手作り味噌</td>
<td th:text="*{__${listfield1}__}">1kg</td>
<td th:text="*{__${listfield2}__}">約6か月</td>
<td class="right" th:text="*{price}">1,620</td>
</tr>
</table>
Thymeleaf のプリプロセッシングは、「 __${ ... }__ 」や「 __#{ ... }__ 」のように、式の前後を「 __ 」(アンダースコア2つ)で挟んで記述した部分を、通常の式より先に実行する仕組みです。
例えば上記のテンプレートでは、通常の式の処理を行う前に、プリプロセッシングで呼び出している式「__${listfield1}__」「__${listfield2}__」を先に評価します。テンプレートを呼び出す Java 側のコード(Spring Boot であれば Controllerクラス)で、以下のように変数「listfield1」「listfield2」の値を設定しておきましょう。
model.addAttribute("listfield1", "volume");
model.addAttribute("listfield2", "expiration");
この場合、上記のテンプレートの「__${listfield1}__」「__${listfield2}__」の部分が先に評価され、通常の式の実行時には以下のように解釈されます(置き換えた後の文字列で式を評価します)。この仕組みによって、動的に表示するプロパティを切り替えることが可能です。
<!-- 商品一覧 -->
<table>
<tr>
<th>商品名</th>
<th th:text="#{thead.volume}">容量</th>
<th th:text="#{thead.expiration}">賞味期限</th>
<th>税込価格(円)</th>
</tr>
<tr th:each="item : ${itemList}" th:object="${item}">
<td th:text="*{name}">手作り味噌</td>
<td th:text="*{volume}">1kg</td>
<td th:text="*{expiration}">約6か月</td>
<td class="right" th:text="*{price}">1,620</td>
</tr>
</table>

このように、プリプロセッシングの評価結果を利用して、一覧のヘッダ行に外部リソースから項目名を動的に取得して表示したり、データ行に対象のプロパティの値を表示することができます。
パラメーターなどに応じて、一覧に表示する項目を Java のコードで指定してみましょう。
if("furniture".equals(cat)){
model.addAttribute("listfield1", "color");
model.addAttribute("listfield2", "size");
}

他にも多言語対応や、外部メッセージ(プロパティファイルやYAMLファイル)に記述した処理を実行させるなど、プリプロセッシングは様々な用途に利用できます。
いかがでしたでしょうか。今回の記事では、実際のWebアプリケーション開発などでも想定される利用例をご紹介しました。特に、静的HTMLなどプロトタイプとしての表示と、テンプレートとしての表示を両立させる仕組みは Thymeleaf の優れた特徴と言えると思います。
これらのケース以外にも、Thymeleaf では各種属性、式、オブジェクトを活用することで、様々な表現が可能となっていますので、是非色々試してみてください。