Spring Boot(MVC)用のThymeleafカスタムタグ作成方法
Posted on April 03, 2015 at 17:23 (JST)
Spring Boot + Thymeleaf + Gradle アプリに、カスタムタグを追加する方法を記します。
今回追加したのは下記2点を実現するタグです。
- レンダリングした際にid属性とname属性両方出力するタグ
- 可変長引数で複数フィールドの一括操作が可能なhasErrorsタグ
GitHubに置いてあるサンプル(simple-mvc-app)に今回の設定を取り込んでいるので、 興味がある方はどうぞ。
概要
作成方法を記載する前に、イメージをしやすいように使い方から記載します。
まずはレンダリングした際にid属性とname属性両方出力するタグです。
<input type=“text” my:idname=“name” th:field=”*{name}” class=“form-control” value=“山田”/>
レンダリングされたHTMLは下記のようになります。
<input type=“text” id=“name” name=“name” class=“form-control” value=“山田”/>
続いて、可変長引数で複数フィールドの一括操作が可能なhasErrorsタグの使用例は下記になります。
<div th:if=”${#myFields.hasErrors(‘name’,‘email’)}” class=“alert alert-danger”>
<ul><li th:each=“error : ${#myFields.errors(‘name’,‘email’)}” th:text=”${error}” /></ul>
</div>
レンダリングされたHTMLは下記のようになります。
<div class=“alert alert-danger”>
<ul><li>1〜50文字で入力してください</li><li>入力必須項目です</li></ul>
</div>
上記を実現するために、今回は以下の4つのファイルを用意しました。
MyDialect.java | 作成したExpressionやProcessorを登録する |
IdNameProcessor.java | my:idnameタグを定義。固定文言の処理はProcessorにて行う。 |
MyFields.java | myFields#hasErrorsと#errorsを定義。Requestの値など動的なものの処理はExpressionで行う。 |
ThymeleafConfig.java | 作成したDialectをSpringにDIするために使用する。 |
ファイル作成
作成したソースは下記になります。
[ MyDialect.java ]
public class MyDialect extends AbstractDialect
implements IExpressionEnhancingDialect {
private final Configuration configuration;
@Override
public Map<String, Object> getAdditionalExpressionObjects(
IProcessingContext processingContext) {
Map<String, Object> map = new HashMap<>();
map.put("myFields",
new MyFields(configuration, processingContext));
return map;
}
public MyDialect(Configuration configuration) {
super();
this.configuration = configuration;
}
public String getPrefix() {
return "my";
}
@Override
public Set<IProcessor> getProcessors() {
final Set<IProcessor> processors = new HashSet<>();
processors.add(new IdNameProcessor());
return processors;
}
}
Expressionを登録する場合はIExpressionEnhancingDialect#getAdditionalExpressionObjectsの実装が必要です。
また、Expressionで使用するConfigurationを渡しています。
[ IdNameProcessor.java ]
public class IdNameProcessor extends AbstractAttributeModifierAttrProcessor {
public static final int ATTR_PRECEDENCE = 10000;
public static final String ATTR_NAME = "idname";
public IdNameProcessor() {
super(ATTR_NAME);
}
@Override
public int getPrecedence() {
return ATTR_PRECEDENCE;
}
@Override
protected Map<String, String> getModifiedAttributeValues(
Arguments arguments, Element element, String attributeName) {
final String attributeValue = element.getAttributeValue(attributeName);
final Configuration configuration = arguments.getConfiguration();
final IStandardExpressionParser expressionParser =
StandardExpressions.getExpressionParser(configuration);
final IStandardExpression expression =
expressionParser.parseExpression(configuration, arguments, attributeValue);
final Set<String> newAttributeNames =
new HashSet<String>(Arrays.asList("id", "name"));
final Object valueForAttributes = expression.execute(configuration, arguments);
final Map<String,String> result =
new HashMap<String,String>(newAttributeNames.size() + 1, 1.0f);
for (final String newAttributeName : newAttributeNames) {
result.put(newAttributeName,
(valueForAttributes == null? "" : valueForAttributes.toString()));
}
return result;
}
@Override
protected ModificationType getModificationType(
final Arguments arguments, final Element element,
final String attributeName, final String newAttributeName) {
return ModificationType.SUBSTITUTION;
}
@Override
protected boolean removeAttributeIfEmpty(
final Arguments arguments, final Element element,
final String attributeName, final String newAttributeName) {
return false;
}
@Override
protected boolean recomputeProcessorsAfterExecution(
Arguments arguments, Element element, String attributeName) {
return false;
}
}
AbstractStandardSingleValueMultipleAttributeModifierAttrProcessorを参考に実装しましたが、コレの子クラスベースにすればもっとシンプルに書けるかも。(探してません)
[ MyFields.java ]
public class MyFields {
private final Configuration configuration;
private final IProcessingContext processingContext;
public boolean hasErrors(final String... fields) {
return Stream.of(fields)
.anyMatch(field ->
FieldUtils.hasErrors(
configuration, processingContext, field));
}
public List<String> errors(final String... fields) {
return Stream.of(fields)
.flatMap(field ->
FieldUtils.errors(
configuration, processingContext, field)
.stream())
.collect(Collectors.toList());
}
public MyFields(final Configuration configuration,
final IProcessingContext processingContext) {
super();
this.configuration = configuration;
this.processingContext = processingContext;
}
}
Fields(th:fields)を参考に実装しました。
[ ThymeleafConfig.java ]
@Configuration
public class ThymeleafConfig {
@Autowired
public TemplateEngine templateEngine;
@Bean
public TemplateEngine addDialect() {
templateEngine.addDialect(
new MyDialect(templateEngine.getConfiguration()));
return templateEngine;
}
}
TemplateEngineをnewして動かすのが難しかったので、DIされたものにdialectを追加する実装にしてみました。
もっとエレガントな実装方法をご存知でしたら、教えてください。
なお、本記事には記載していないSpringErrorsAttrProcessor(th:errors)の複数フィールド指定対応は、かなりゴリゴリやる必要があるのでオススメしません。。。
以上です。