Working with Thymeleaf dialects

July 02, 2020 No comments Thymeleaf Dialect

1. Introduction

Thymeleaf dialects are a set of features that allows you to extend default Thymeleaf engine functionality. Basic Thymeleaf dialect is called Standard Dialect, all common features come from it.

We can extend standard dialect with:

  • processing logic - processors that apply to attributes in Thymeleaf tags,
  • preprocessing and postprocessing logic - logic that can be applied before or after processing,
  • expression objects - used to perform the specialized operations in Thymeleaf expressions.

2. Maven dependencies

To show how Thymeleaf dialects works we used a sample Spring Boot application built as Maven project with the following dependencies:

The pom.xml file has the following structure:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <artifactId>thymeleaf-custom-dialect</artifactId>

    <!-- Inherit defaults from Spring Boot -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
    </parent>

    <!-- Add typical dependencies for a web application -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>

    <!-- Package as an executable jar -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

3. Project files structure

The project structure is as follows:

├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── frontbackend
│   │   │           └── thymeleaf
│   │   │               ├── Application.java
│   │   │               ├── controller
│   │   │               │   └── IndexController.java
│   │   │               └── dialect
│   │   │                   ├── WelcomeDialect.java
│   │   │                   ├── WelcomeHeaderTagProcessor.java
│   │   │                   └── WelcomeToAttributeTagProcessor.java
│   │   └── resources
│   │       └── templates
│   │           └── index.html
  • com.frontbackend.thymeleaf is our base package,
  • Application is a main Spring Boot application class that starts the server,
  • WelcomeHeaderTagProcessor and WelcomeToAttributeTagProcessor are tag processors,
  • WelcomeDialect is a custom dialect that will extend Standard Thymeleaf Dialect,
  • IndexController handles GET requests to the root context,
  • index.html is a main Thymeleaf template where we use custom tag and attribute.

4. Extend Thymeleaf with custom dialect

This custom dialect will extend default one with two new processors:

  • WelcomeHeaderTagProcessor - this processor is creating custom header element,
  • WelcomeToAttributeTagProcessor - this processor is creating custom attribute.

4.1. Header tag processor

Let's check the implementation of the first processor, that extends AbstractElementTagProcessor, the base class to be used for tag processors that do not match on a specific attribute:

package com.frontbackend.thymeleaf.dialect;

import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.model.IModel;
import org.thymeleaf.model.IModelFactory;
import org.thymeleaf.model.IProcessableElementTag;
import org.thymeleaf.processor.element.AbstractElementTagProcessor;
import org.thymeleaf.processor.element.IElementTagStructureHandler;
import org.thymeleaf.templatemode.TemplateMode;
import org.unbescape.html.HtmlEscape;

public class WelcomeHeaderTagProcessor extends AbstractElementTagProcessor {

    private static final String TAG_NAME = "header";
    private static final int PRECEDENCE = 1000;

    public WelcomeHeaderTagProcessor(final String dialectPrefix) {
        super(TemplateMode.HTML, dialectPrefix, TAG_NAME, true, null, false, PRECEDENCE);
    }

    @Override
    protected void doProcess(final ITemplateContext context, final IProcessableElementTag tag, final IElementTagStructureHandler structureHandler) {
        final String title = tag.getAttributeValue("title");

        final IModelFactory modelFactory = context.getModelFactory();
        final IModel model = modelFactory.createModel();

        model.add(modelFactory.createOpenElementTag("h1"));
        model.add(modelFactory.createText(HtmlEscape.escapeHtml5(title)));
        model.add(modelFactory.createCloseElementTag("h1"));

        structureHandler.replaceWith(model, false);
    }
}

In WelcomeHeaderTagProcessor contructor we call base class contructor with the following parameters:

public WelcomeHeaderTagProcessor(final String dialectPrefix) {
    super(
            TemplateMode.HTML, // processor will apply only to HTML mode
            dialectPrefix,     // prefix to be applied to name for matching (ex frontbackend:)
            TAG_NAME,          // the tag name that match specifically this tag
            true,              // if true then apply dialect prefix to tag name
            null,              // empty attribute name: will match by tag name
            false,             // empty prefix to be applied to attribute name
            PRECEDENCE);       // precedence
}

If dialectPrefix is fbe then we can create tag that will match with this specific processor like in the following example:

<fbe:header title="This is the headline"></fbe:header>

This code will be rendered to (how the processor will transform the HTML tag is provided in doProcess(...) method:

<h1>This is the headline</h1>

4.2. Welcome attribute tag processor

The attribute processor is implemented as a subclass of AbstractAttributeTagProcessor. This processor will be displaying the welcome message.

package com.frontbackend.thymeleaf.dialect;

import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.engine.AttributeName;
import org.thymeleaf.model.IProcessableElementTag;
import org.thymeleaf.processor.element.AbstractAttributeTagProcessor;
import org.thymeleaf.processor.element.IElementTagStructureHandler;
import org.thymeleaf.templatemode.TemplateMode;
import org.unbescape.html.HtmlEscape;

public class WelcomeToAttributeTagProcessor extends AbstractAttributeTagProcessor {

    private static final String ATTR_NAME = "welcome";
    private static final int PRECEDENCE = 10000;

    public WelcomeToAttributeTagProcessor(final String dialectPrefix) {
        super(TemplateMode.HTML, dialectPrefix, null, false, ATTR_NAME, true, PRECEDENCE, true);
    }

    protected void doProcess(final ITemplateContext context, final IProcessableElementTag tag, final AttributeName attributeName,
            final String attributeValue, final IElementTagStructureHandler structureHandler) {

        structureHandler.setBody("Welcome, " + HtmlEscape.escapeHtml5(attributeValue) + "! Nice to see you.", false);
    }
}

This processor will be triggered by a specific attribute.

<p fbe:welcome="John">Hello</p>

will be rendered to

<p>Welcome John ! Nice to see you.</p>

4.3. Dialect class

To register our processors we need to create the Dialect class that extends an abstract class called AbstractProcessorDialect. This class has the following structure:

package com.frontbackend.thymeleaf.dialect;

import java.util.HashSet;
import java.util.Set;

import org.springframework.stereotype.Component;
import org.thymeleaf.dialect.AbstractProcessorDialect;
import org.thymeleaf.processor.IProcessor;

@Component
public class WelcomeDialect extends AbstractProcessorDialect {

    public WelcomeDialect() {
        super("FrontBackend Dialect", // Dialect name
                "frontbackend", // Dialect prefix (frontbackend:*)
                1000); // Dialect precedence
    }

    @Override
    public Set<IProcessor> getProcessors(String s) {
        final Set<IProcessor> processors = new HashSet<>();
        processors.add(new WelcomeToAttributeTagProcessor(getPrefix()));
        processors.add(new WelcomeHeaderTagProcessor(getPrefix()));
        return processors;
    }
}

Note that the dialect class should be annotated with Spring @Component in order to tell the Spring framework that it should be processed. We register processors attached to this dialect in the getProcessors(...) method.

Now, let's check sample Thymeleaf template that used custom tags and attributes:

<!DOCTYPE HTML>
<html lang="en" xmlns:frontbackend="https://frontbackend.com">
<head>
    <meta charset="UTF-8"/>
    <title>Spring Boot Thymeleaf Application - Custom Dialect</title>
</head>
<body>

<frontbackend:header title="This is the headline">Headline</frontbackend:header>

<p frontbackend:welcome="John">Hello</p>

</body>
</html>

5. Conclusion

This article presents how to extend Thymeleaf Standard Dialect with new processors for custom tags and attributes.

Code used in this article is available under our GitHub repository

{{ message }}

{{ 'Comments are closed.' | trans }}