(2015/11/26修正)Reflection による実装から Generics による実装へ変更しました。
(2016/01/23修正)ValuesHelper の実装で <T extends Enum<T>
を記述する位置を変更し、メソッドを2つ追加しました。
(2016/03/20修正)コンストラクタで呼び出すメソッドを ClassPath#getTopLevelClasses → ClassPath#getTopLevelClassesRecursive へ変更し、サブパッケージに作成された Values クラスも呼び出せるようにしました。また Values インターフェースを実装した Enum(列挙型)だけが呼び出し可能になるよう filter メソッド内の処理を変更しました。
Spring Boot + Thymeleaf の構成において、Thymeleaf のテンプレートファイルでドロップダウンリストを Enum(列挙型)を利用して表示する方法をまとめてみます。シンプルな記述で実装する方法がないか考えた現時点での自分なりの結論です。
概要
手順をまとめると以下の2点になります。
- 値とテキストの組み合わせを定義する Enum(列挙型)の Values クラスを実装する。
- Thymeleaf テンプレートファイル内で ValuesHelper クラスを利用して記述する。
ライブラリとして Lombok, Guava を利用しています。
パッケージ構成
今回説明する方法は以下のパッケージ・ファイル構成の想定です。今回の説明に必要なファイルしか記述していません(Spring Bootで必要なファイルがありますが省略しています)。
src
└ main
├ java
│ └ webapp
│ ├ values
│ │ ├ TicketStatusValues.java
│ │ ├ Values.java
│ │ └ ValuesHelper.java
│ └ web
│ ├ SampleController.java
│ └ SampleForm.java
└ resources
└ templates
└ sample
└ index.html
Enum(列挙型)の Values クラスを作成する
最初にドロップダウンリストに表示する値とテキストの組み合わせを定義した Enum(列挙型)を作成します。以降このEnum(列挙型)を Values クラスと呼びます。
フォーマットを決めているので、それに従って記述します。
package webapp.values;
// ↓ここから下の部分はそのままコピーしてください
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
// ↑ここから上の部分はそのままコピーしてください
// 独自に記述する必要があるのは enum の後の列挙名(ここでは TicketStatusValues) と
public enum TicketStatusValues implements Values {
// 定数部分の2ヶ所です
WAITING("1", "未着手")
, IN_PROGRESS("2", "作業中")
, COMPLETE("3", "完了");
// ↓ここから下の部分はそのままコピーしてください
private final String value;
private final String text;
}
作成時のポイントは、
- values パッケージの中に Values クラスを作成します。Values クラスは必ず Values.java, ValuesHelper.java と同じパッケージ(サブパッケージは不可)に作成し、Values インターフェースを実装します。
- Values クラスで独自に記述するのは列挙名と定数部分の2ヶ所だけです。それ以外の部分は固定のフォーマットです。他の Values クラスを作成する時もこのままコピーします。
- Values クラスを作成する際に独自に記述する部分を減らすために、必要なコンストラクタ及び getter メソッドを Lombok のアノテーションで自動生成するようにしています。
Values インターフェースは以下の実装です。
package webapp.values;
public interface Values {
public String getValue();
public String getText();
}
- getValue, getText メソッドは実装クラスの Values クラス側では Lombok の
@Getter
アノテーションで自動生成しますが、処理上必要なためインターフェースで定義します。後述の ValuesHelper クラスで利用します。
SampleForm クラスを作成する
以下の内容の Form クラスを作成します。
package webapp.web;
import lombok.Data;
@Data
public class SampleForm {
private String ticketStatus;
}
SampleController クラスを作成する
以下の内容の Controller クラスを作成します。
package webapp.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import static webapp.values.TicketStatusValues.*;
@Controller
@RequestMapping("/sample")
public class SampleController {
@RequestMapping
public String index(SampleForm sampleForm) {
// 試しに tikcetStatus に IN_PROGRESS(作業中) をセットします
sampleForm.setTicketStatus(IN_PROGRESS.getValue());
return "sample/index";
}
}
Thymeleaf テンプレートファイルを作成する
ドロップダウンリストを表示する Thymeleaf テンプレートファイルには以下のように記述します。
<form id="sampleForm" method="post" action="/sample/..." th:action="@{/sample/...}" th:object="${sampleForm}">
.....
<select class="..."
th:field="*{ticketStatus}">
<option th:each="value : ${@vh.values('TicketStatusValues')}"
th:value="${value.getValue()}"
th:text="${value.getText()}">未着手</option>
</select>
.....
</form>
- form タグに関連付ける Form クラスを th:object に記述します。
- select タグに関連付ける Form クラスのフィールドを th:field に記述します。
${...}
ではなく*{...}
を使用します。 - option タグでは値の一覧を
th:each="value : ${@vh.values('TicketStatusValues')}"
で取得します。vh は ValuesHelper クラスの Bean 名です ( ValuesHelper クラスはこの後説明します )。@vh
と記述することで Bean を呼び出すことが出来、@vh.values
に使用する Values クラスのクラス名を渡すと その Values クラスの定数一覧を取得できます。 - option タグの values 属性とテキスト文字列は
th:value="${value.getValue()}"
,th:text="${value.getText()}"
で出力します。
このように記述すると以下の HTML 文が生成されドロップダウンリストが表示されます。Thymeleaf の機能により値が同じものに selected="selected"
が自動で出力されます。
<form id="sampleForm" method="post" action="/sample/...">
.....
<select class="..." id="ticketStatus" name="ticketStatus">
<option value="1">未着手</option>
<option value="2" selected="selected">作業中</option>
<option value="3">完了</option>
</select>
.....
</form>
ValuesHelper クラスとは
ValuesHelper クラスは Values クラスを利用しやすくするためのクラスで、以下の実装です。
package ksbysample.webapp.lending.values;
import com.google.common.reflect.ClassPath;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
import java.util.stream.Collectors;
@Component("vh")
public class ValuesHelper {
private final Map<String, String> valuesObjList;
private ValuesHelper() throws IOException {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
valuesObjList = ClassPath.from(loader).getTopLevelClassesRecursive(this.getClass().getPackage().getName())
.stream()
.filter(classInfo -> {
try {
Class<?> clazz = Class.forName(classInfo.getName());
return !clazz.equals(Values.class) && Values.class.isAssignableFrom(clazz);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
})
.collect(Collectors.toMap(ClassPath.ClassInfo::getSimpleName, ClassPath.ClassInfo::getName));
}
@SuppressWarnings("unchecked")
public <T extends Enum<T> & Values> String getValue(String classSimpleName, String valueName)
throws ClassNotFoundException {
Class<T> enumType = (Class<T>) Class.forName(this.valuesObjList.get(classSimpleName));
T val = Enum.valueOf(enumType, valueName);
return val.getValue();
}
public <T extends Enum<T> & Values> String getValue(Class<T> enumType, String valueName) {
T val = Enum.valueOf(enumType, valueName);
return val.getValue();
}
@SuppressWarnings("unchecked")
public <T extends Enum<T> & Values> String getText(String classSimpleName, String value)
throws ClassNotFoundException {
Class<T> enumType = (Class<T>) Class.forName(this.valuesObjList.get(classSimpleName));
String result = "";
for (T val : enumType.getEnumConstants()) {
if (val.getValue().equals(value)) {
result = val.getText();
break;
}
}
return result;
}
public <T extends Enum<T> & Values> String getText(Class<T> enumType, String value) {
String result = "";
for (T val : enumType.getEnumConstants()) {
if (val.getValue().equals(value)) {
result = val.getText();
break;
}
}
return result;
}
@SuppressWarnings("unchecked")
public <T extends Enum<T> & Values> T[] values(String classSimpleName)
throws ClassNotFoundException {
Class<T> enumType = (Class<T>) Class.forName(this.valuesObjList.get(classSimpleName));
return enumType.getEnumConstants();
}
}
- Thymeleaf テンプレートファイル内で簡単に記述するために Bean 名として
@Component
アノテーションにvh
を記述しています。 - コンストラクタで ValuesHelper クラスと同じ package にある Values クラスを Map 型のフィールド valuesObjList に格納します。
- メソッドが3つ定義されていますが、ドロップダウンリストで使用するのは values メソッドです。引数で渡された Values クラスのクラス名から Class クラスを取得し、Class#getEnumConstants メソッドを呼び出して Values クラスのオブジェクト一覧を取得して呼び出し元に返します。
ValuesHelper#getValue, getText メソッドについては以下のように使用します。Enum(列挙型)で定義した定数名を Thymeleaf テンプレートファイル内でも使用できるようにするために用意したメソッドです。
<p th:text="${@vh.getText('TicketStatusValues', sampleForm.ticketStatus)}"
th:if="*{ticketStatus} == ${@vh.getValue('TicketStatusValues', 'COMPLETE')}"></p>
以上、Enum(列挙型)の Values クラスとヘルパークラスを利用して Thymeleaf でドロップダウンリストを簡単に表示してみようという内容でした。