結論
共通部品で「フィールド名」と「値」を必要とした場合、呼び出し元でその2つを渡せば機能するが、共通部品内でフィールド名をもとに値を扱うようにすれば引数は1つですむ。
※場合によっては工夫が必要なことがある
<!-- 共通部品にフィールド名と値の2つを渡す場合 -->
<div th:replace="parts/common-parts :: common-parts('fieldName', value)"></div>
<!-- 共通部品内でフィールド名をもとに値を扱うようにした場合 -->
<div th:replace="parts/common-parts :: common-parts('fieldName')"></div>
経緯
Thymeleafを使った画面実装において、入力項目や表示項目についてデザインが共通している部分を共通部品にしたくなった。
例えばテキストボックスをThymeleafの記法を気にせず実装するとこうなる。
<input type="text" name="fieldName" value="初期値"/>
部品化する場合、name
属性とvalue
属性に入る値を可変にする必要がある。
用意する値の数が多いと書く量が増え、可読性が低くなるので、できるだけ用意する値の数が少なくすむように共通部品を作りたい。
そこで、name
属性に入る値をもとにvalue
属性の値を指定できるようにすればよいと考え、
「共通部品内でフィールド名をもとに値を扱う」ことについてメモ程度にまとめるに至った。
目的
- 共通部品内でフィールド名をもとに値を扱う方法を目的別にまとめる
どの目的でも後述する「基本的な考え方」を応用すればよいため、書き方が大きく変わることはない。
ただ、Thymeleafの記法をよく理解しておかなければ指定箇所の前後の繋がりについてややこしく感じたり、一部微妙に指定内容が変わるものもあるため、目的別にまとめることにした。
Thymeleafとは
- Javaのテンプレートエンジンライブラリ
- テンプレート作成のためにエレガントでメンテナンス性の高い方法を提供することが目的
環境
- OS:Windows10
- Java:openjdk 11.0.13 2021-10-19
- Spring Boot:2.7.1
- Thymeleaf:2.7.1
※Spring + Thymeleafの環境はSpring Boot Thymeleaf Web 画面の作成 - 公式サンプルコード (pleiades.io)を利用した。
ディレクトリ構成
initial
├── (中略)
└── src
└─ main
├── java
│ └── com.example
│ ├── helper
│ │ └── UserNameHelper.java
│ └── servingwebcontent
│ ├── GreetingController.java
│ └── ServingWebContentApplication.java
└── resources
├── staic
├── templates
│ ├── parts
│ │ └── common-parts.html
│ └── greeting.html
└── messages.properties
- サンプルプロジェクトのディレクトリ構成に加えて、「
helper
」や「messages.properties
」などを追加している
基本的な考え方
共通部品においては値に紐づくフィールド名が可変になるため、
単に"${fieldName}"
や"*{fieldName}"
と指定するだけでは対応できない。
(↑の場合、「fieldName」というフィールド名を指定していることになる)
そこで、プリプロセッシングの仕組みを使ってフィールド名をもとに値を扱えるようにする。
-
4.15 Preprocessing
書き方と出力結果は以下表の通り。
※例としてオブジェクト名を「form」、フィールド名を「name」、値を「ユーザ001」とする。
呼び出し元 | 呼び出し先(共通部品) | 出力結果 |
---|---|---|
‘name’ | "\${__${fieldName}__}" | name |
‘form.name’ | "\${__${fieldName}__}" | ユーザ001 |
‘name’ | "*{__${fieldName}__}" | ユーザ001 |
そのため、上記「経緯」のうちvalue
属性に値を指定したい場合はth:value=*{__${fieldName}__}
とすればよく、基本的にこの考え方に従えばフィールド名をもとに値を扱うことが可能となる。
※ただし、*{...}
構文(選択変数式)を使って値を表示したい場合、th:object
属性でオブジェクトを選択しておく必要がある。選択していない場合、${...}
構文(変数式)の出力結果と同じになる。
目的別:共通部品内でフィールド名をもとに値を扱う
前提
Spring MVC及びFormのそれぞれの関係について以下に記す。
以降の目的別の説明を通して記載したコードについては以下コードに差分として追記されていくイメージとする。
package com.example.servingwebcontent;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.validation.constraints.NotBlank;
@Controller
public class GreetingController {
@GetMapping("/greeting")
public String greeting(@ModelAttribute ExampleForm form, BindingResult bindingResult, Model model) {
//目的別に値表示するために適宜formに値を設定していく
model.addAttribute("form", form);
return "greeting";
}
@PostMapping("/post")
public String post(@Validated @ModelAttribute("form") ExampleForm form, BindingResult bindingResult, Model model) {
model.addAttribute("form", form);
return "greeting";
}
public static class ExampleForm {
// 単項目チェックエラーの確認のため指定
@NotBlank
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
- サンプルプロジェクトのControllerを利用した
- フォームをExampleFormとして定義した
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>共通部品の呼び出し元</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<form th:action="@{/post}" th:object="${form}" method="post">
<div th:replace="parts/common-parts:: common-parts('name')"></div>
<input type="submit" th:value="提出"/>
</form>
</body>
</html>
-
th:object
属性でオブジェクト名を指定しているため、選択変数式でフィールド名をすることで値を扱えるようになる
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>共通部品</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div th:fragment="common-parts(fieldName)">
<input type="text" th:field="*{__${fieldName}__}" />
<span style="color:red;" th:errors="*{__${fieldName}__}"></span>
<div th:if="${#fields.hasErrors('__${fieldName}__')}">trueだったら表示される</div>
</div>
</body>
</html>
-
th:fragment
属性でフラグメントの名前を定義している - 今回は名前を
common-parts
としているが、実際はテキストボックスであればtextbox
など適した名前にする - 以降の目的別の実装において、必要に応じて追記していく
テキストボックス
<input type="text" th:field="*{__${fieldName}__}" />
- 呼び出し元の引数に
’name’
が指定されているので、プリプロセッシングの仕組みにより、*{name}
として評価される -
th:field
はid, name, valueに分けられ、ブラウザでページがレンダリングされるさいのHTMLは以下のようになる - セレクトボックスやチェックボックスなど、他の入力部品でも
th:field
の考え方は同じ。ただし、選択中の値と表示用の値とを分けて表現するため等、必要に応じて指定する属性が増える
<input type="text" id="name" name="name" value="">
- Java側でFormに入力値を設定するようにしておけば、valueに入力値が入る
ちなみに、th:field
を使わず、th:id
、th:name
、th:value
を使う場合の実装は以下の通り。
<input type="text" th:id="${fieldName}" th:name="${fieldName}" th:value="*{__${fieldName}__}"/>
単項目チェックエラーメッセージを表示する
<span style="color:red;" th:errors="*{__${fieldName}__}"></span>
ちなみに、単項目チェックエラーの有無を判断したい場合は以下のように実装する。
以下はフィールド名にアクセスしている。
<div th:if="${#fields.hasErrors('__${fieldName}__')}">trueだったら表示される</div>
値を表示する
<div th:text="*{__${fieldName}__}"></div>
<div th:text="${#object.__${fieldName}__}"></div>
[[*{__${fieldName}__}]]
- 内側の3行とも同じ結果になる
- 1行目は選択変数式を利用して表示している
- 2行目はThymeleafの記法として用意されている
#object
を利用してオブジェクトのフィールドにアクセスしている - 3行目はインライン処理として
th:text
がなくても表示できる
ネストされたフィールドの値を表示する
以下のようにFormのフィールドがネストしている場合は呼び出し元をネストの構造に沿って指定すればよく、呼び出し先は上記と同じでよい。
public static class ExampleForm {
private User user;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
public static class User {
private Integer age;
public User(int userId, int age) {
this.userId = userId;
this.age = age;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
<div th:replace="parts/common-parts :: common-parts('user.age')"></div>
- 呼び出し元で引数に
User
オブジェクトのage
フィールドの値にアクセスするために’user.age’
を指定している
ネストされた固定のフィールドの値を表示する
フィールド名が可変の場合は呼び出し元でネストしている・されている両方の名前を指定すればよいが、場合によっては名前が一部固定の場合もある。
<div th:text="*{__${fieldName}__.value}"></div>
<div th:text="*{user.__${fieldName}__}"></div>
- 1行目はネストされたフィールドの名前が
value
で固定 - 2行目は
user
フィールドのうち、指定したフィールドの値を表示する
値を連結して表示する
<div th:text="'私の名前は' + *{__${fieldName}__} +'です。'"></div>
<div th:text="|私の名前は *{__${fieldName}__} です。|"></div>
<div th:text="*{'私の名前は' + __${fieldName}__ + 'です。'}"></div>
- 表示結果は全て同じ
条件式に真偽値を使い、結果に応じて表示/非表示する
<th:div th:if="*{__${fieldName}__ == '値に対する条件'}">trueだったら表示される</th:div>
<th:div th:unless="*{__${fieldName}__ == '値に対する条件'}">falseだったら表示される</th:div>
<th:div th:if="*{__${fieldName}__ % 100 == 0}">この数値は100で割り切れます</th:div>
<th:div th:text="*{__${fieldName}__ % 100 == 0} ? 'この数値は100で割り切れます' : 'この数値は100で割り切れません'"></th:div>
-
*{...}
構文で囲っているため、プリプロセッシングの仕組みによりフィールド名に紐づく値が条件に使われる
ユーティリティメソッドの引数に値を指定する
<div th:text="*{#strings.isEmpty(__${fieldName}__)}"></div>
<div th:text="*{#numbers.formatInteger(__${fieldName}__,3)}"></div>
<div th:text="*{#messages.msg(__${messageKey}__ +'.message', __${fieldName}__)}"></div>
<div th:text="*{#dates.format(__${fieldName}__, 'yyyy年MM月dd日')}"></div>
<div th:text="*{#bools.isTrue(__${fieldName}__=='値に対する条件')}"></div>
<div th:text="*{#arrays.isEmpty(__${fieldName}__)}"></div>
- 各オブジェクトに対するユーティリティメソッド(一部抜粋)の引数に指定している
-
#messages.msgはmessages.properties
で定義しているメッセージIDと指定された引数の値を通してメッセージを表示する。以下はmessages.properties
の定義内容
welcome.message=ようこそ、{0}さん
-
{0}
に()
内で指定した値が入る
自分で定義したメソッドの引数に値を指定する
public class UserNameHelper {
public static String createSelfIntroduction(String name) {
return "私の名前は" + name + "です。";
}
}
- 指定されたnameの値と固定の文を連結させて返す
<div th:text="#{*{T(com.example.helper.UserNameHelper).createSelfIntroduction(__${fieldName}__)}}"></div>
-
*
の位置は通常式であれば$
だった場所になる
URLに可変部分を設け、フィールド名や値を指定する
public static class User {
private Integer userId;
private Integer age;
public User(int userId, int age) {
this.userId = userId;
this.age = age;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
- URLのクエリパラメータを指定できるように、UserクラスにuserIdを追加
<!-- 埋め込みパラメータ無しのurlを指定する場合 -->
<a th:href="@{*{__${url}__}}">リンク</a>
<a th:href="@{*{__url__}}">リンク</a> <!-- 上に同じ -->
<!-- クエリパラメータを指定する場合 -->
<a th:href="@{*{__${url}__}(userId=*{__${fieldName}__})}">リンク</a>
<a th:href="@{*{__url__}(userId=*{__${fieldName}__})}">リンク</a> <!-- 上に同じ -->
<!-- クエリ名とクエリパラメータを指定する場合 -->
<a th:href="@{*{__${url}__}(${parameterName}=*{__${fieldName}__})}">リンク</a>
<a th:href="@{*{__url__}(${parameterName}=*{__${fieldName}__})}">リンク</a> <!-- 上に同じ -->
- urlの指定において、通常式を省略してもリンクとして認識される
- クエリパラメータを指定した場合のレンダリング結果の例
<a href="/detail?userId=1">リンク</a>
備考
ネストされたフィールドにアクセスしたい場合と、ネストされているフィールド名にアクセスしたい場合は以下のように「値」用(user.userId
)と「フィールド名」用(userId
)として別々の指定となる。
<div th:replace="parts/common-parts :: common-parts('url', 'user.userId', 'userId')"></div>
が、呼び出し先でネストの親の部分を省く実装をすれば、引数を減らすことができる。
<div th:with="parameterName=#{${T(com.example.helper.UserNameHelper).extractFieldName(fieldName)}}">
<a th:href="@{*{__${url}__}(${parameterName}=*{__${fieldName}__})}">リンク</a>
</div>
-
th:with
は左辺に定義するローカル変数に右辺の値を代入し、ブロック内で使用できる。ここでは「parameterName」という名前の変数を定義し、右辺で処理した結果を代入している
// 先頭から「.」までの文字列を空文字に置換する(今回でいえば『user.』の部分)
public static String extractFieldName(String name) {
return name.replaceAll("^.*\\.", "");
}
- ここではuser.userIdという文字列を受け取り、userIdという文字列にして返している
<div th:replace="parts/common-parts :: common-parts('url', 'user.userId')"></div>
- 引数を3つから2つに減らすことができた
感想
Thymeleafは複数の構文を組み合わせて一つの処理を実現する場合があり、読み解くのに苦労する。
ここまでで確認したように目的別で確認することで、フィールド名をもとに値を取り扱う方法について理解を深めることができた。