Thymeleaf custom dialect with fragments: how to keep th: attributes

Viewed 73

I'm creating a custom dialect that replaces <myprefix:mytag> with a <div th:replace="myTagFragment::myfrag"> and it works with no attributes or "standard" attributes.

It stops working if I use th: attributes in the custom tag.

This works:

<myprefix:mytag required id="myid">

This does not work:

<myprefix:mytag th:field="*{name}">

TagProcessor code snippet:

protected void doProcess(ITemplateContext context, IModel model, IElementModelStructureHandler structureHandler) {

final IOpenElementTag customTag = (IOpenElementTag) model.get(0);
//
// I create a comma-separated list of the original attributes and add them in the fragment with th:attr
Map<String, String> attributes = sourceTag.getAttributeMap();
String attributeString = attributes.toString(); // "{attr1=val1, attr2=val2}"
attributeString = StringUtils.chop(attributeString); // Remove "}"
attributeString = StringUtils.removeStart(attributeString, "{"); // Remove "{"
structureHandler.setLocalVariable("attributeString", attributeString); // "attr1=val1, attr2=val2"
//
Map<String, String> newtagAttributes = new HashMap<>();
newtagAttributes.put("th:replace", "myTagFragment::myfrag");
IOpenElementTag replacementTag = modelFactory.createOpenElementTag("div", newtagAttributes, AttributeValueQuotes.DOUBLE, false);
model.replace(0, replacementTag);

Fragment snippet from "myTagFragment.html":

<th:block th:fragment="myfrag">
    <div>somestuffhere</div>
    <input th:attr="__${attributeString}__">

When I use

<myprefix:mytag type="password" maxlength="70" id="titolo">

I correctly get the replaced fragment:

<div>somestuffhere</div>
<input type="password" maxlength="70" id="titolo">

but if I want to use th: attributes in the original tag, they fail because something like th:field is not processed properly when added with th:attr="__${attributeString}__" of course.

Maybe there is a way to retrieve the fragment model from inside the TagProcessor and add the th: attributes by code? Any other idea to replace a custom tag with a template fragment while keeping the original attributes and have them processed when needed?

2 Answers

The (partial) solution I found so far is to process th: attributes myself in the TagProcessor: most of them just need to be replaced with the equivalent plain version after evaluation, some others need a special trearment, some more I just ignore.

(pseudo)Code snippet:

Map<String, String> attributes = sourceTag.getAttributeMap();
Map<String, String> targetAttributes = new HashMap<>();
for (Map.Entry<String,String> sourceAttribute : attributes.entrySet()) {
    String attributeName = sourceAttribute.getKey();
    String attributeValue = sourceAttribute.getValue();
    if (attributeName.startsWith("th:") && !attributeName.endsWith("append")) {
      String thAttributeName = ... remove "th:" from the name
      String parsedValue = null;
      if (!"attr".equals(thAttributeName)) {
        try {
          final IStandardExpression expression = parser.parseExpression(context, attributeValue);
          parsedValue = (String) expression.execute(context);
        } catch (Exception e) {
          log.debug("Can't parse \"{}\" for tag \"{}\" - skipping", attributeValue, attributeName);
          continue; // Next attribute
        }
        switch (thAttributeName) {
          case "field":
              targetAttributes.put("name", parsedValue);
              targetAttributes.put("value", parsedValue);
              break;
          ... also handle "attr", "alt-title", "lang-xmllang"
          default:
              targetAttributes.put(thAttributeName, parsedValue);
    } else {
        // Plain attributes with no "th:"
        targetAttributes.put(attributeName, attributeValue);
    }
}
... repeat the loop to handle "append" and "prepend" attributes here
String attributeString = targetAttributes.toString();
... same cose as in the question follows

The problem with this approach is that all conditional and looping th tags are lost, so I can't use th:each on my custom tag for example.

Another solution is to give a higher precedence to my custom dialect so that all th: attributes are executed before my tag processor. This way I can use th:each and other stuff on my custom tag (see discussion here).

The only problem seems to be with th:field because it checks the tag before adding name/value attributes and skips them on any custom tag.

public class YadaDialect extends AbstractProcessorDialect {
    public YadaDialect() {
        super("Yada Dialect", "yada", StandardDialect.PROCESSOR_PRECEDENCE+1);
    }
Related