アレについて記す

Spring Boot(MVC)用のThymeleafカスタムタグ作成方法

Posted on April 03, 2015 at 17:23 (JST)

Spring Boot + Thymeleaf + Gradle アプリに、カスタムタグを追加する方法を記します。
今回追加したのは下記2点を実現するタグです。

  1. レンダリングした際にid属性とname属性両方出力するタグ
  2. 可変長引数で複数フィールドの一括操作が可能な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.javamy:idnameタグを定義。固定文言の処理はProcessorにて行う。
MyFields.javamyFields#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)の複数フィールド指定対応は、かなりゴリゴリやる必要があるのでオススメしません。。。

以上です。