アレについて記す

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)の複数フィールド指定対応は、かなりゴリゴリやる必要があるのでオススメしません。。。

以上です。