thymeleaf-view-component

While developing my side project over the last year with thymeleaf I noticed that the templates you serve with the Controller get quite large. You can split them up by using Thymeleaf fragments. But when you nest multiple fragments and use variable expression it is going to get hard to test.

That’s why the Idea of ViewComponents came along. There is a similar Library already available for Ruby on Rails which are inspired by react.

If you want to use the library with Java and Maven you can skip ahead

Kotlin

Installation with Gradle

Demo Repository with Kotlin and Gradle

Creating a ViewComponent

We just need to add the @ViewComponent annotation to a class and define a render() method that returns a ViewContext. We can pass the properties we want to use in our template.

// HomeViewComponent.kt
@ViewComponent
class HomeViewComponent(
    private val exampleService: ExampleService
    ) {
    fun render() = ViewContext(
        "helloWorld" toProperty "Hello World",
        "coffee" toProperty exampleService.getCoffee()
    )
}

Next we define the HTML in the HomeViewComponent.html in the same package as our ViewComponent Class.

// HomeViewComponent.html
<html xmlns:th="http://www.thymeleaf.org">
<body>
    <div th:text="${helloWorld}"></div>
    <br>
    <strong th:text="${coffee}"></strong>
</body>
</html>

We can then call the render method in our Controller

// Router.kt
@Controller
class Router(
    val homeViewComponent: HomeViewComponent
) {
    @GetMapping("/")
    fun homeComponent(): Any {
        return homeViewComponent.render()
    }
}

If we now access the root url path of our spring application we can see that the component renders properly: thymeleaf-view-component-example

Nesting components:

We can also embed components to our templates, either with expression inlining [(${{}})] or with the th:utext=${{}} property. It is important to use double curly bracelets {{}}.

<div>
    [(${{@navigationViewComponent.render()}})]
</div>
<div th:utext="${{@navigationViewComponent.render()}}"></div>

Parameter components:

We can also create components with parameters. We can either use default values when we pass a null value, get a property from a Service or we can throw a custom error.

// ParameterViewComponent.kt
@ViewComponent
class ParameterViewComponent{
    fun render(parameter: String?) = ViewContext(
        "office" toProperty (parameter ?: throw Error("You need to pass in a parameter")),
    )
}

// ParameterViewComponent.html
<h2>ParameterComponent:</h2>

<div th:text="${office}"></div>

This enables us to define the properties for our ParameterViewComponent in the HomeViewComponent:

// HomeViewComponent.kt
@ViewComponent
class HomeViewComponent(
    private val exampleService: ExampleService,
) {
    fun render() = ViewContext(
            "helloWorld" toProperty exampleService.getHelloWorld(),
            "office" toProperty exampleService.getOfficeHours()
        )
}

// HomeViewComponent.html
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<body>

<div th:text="${helloWorld}"></div>

<div th:utext="${{@parameterViewComponent.render(office)}}"></div>

</body>
</html>

If we now access the root url path of our spring application we can see that the parameter component renders properly:

viewcomponent-parameter

Gradle Installation

Add this snippet to your build.gradle.kts:

// build.gradle.kts
repositories {
    maven("https://jitpack.io")
}
dependencies {
    implementation("com.github.tschuehly:thymeleaf-view-component:0.1.3")
}
sourceSets {
    main {
        resources {
            srcDir("src/main/kotlin")
            exclude("**/*.kt")
        }
    }
}

Java

Installation with Maven

Demo Repository with Java and Maven

Creating a ViewComponent

We just need to add the @ViewComponent annotation to a class and define a render() method that returns a ViewContext. We can pass the properties we want to use in our template.

// HomeViewComponent.java
@ViewComponent
public class HomeViewComponent {
    private final ExampleService exampleService;

    public HomeViewComponent(ExampleService exampleService) {
        this.exampleService = exampleService;
    }

    public ViewContext render() {
        return new ViewContext(
                ViewProperty.of("helloWorld", "Hello World"),
                ViewProperty.of("coffee", exampleService.getCoffee())
        );
    }
}

Next we define the HTML in the HomeViewComponent.html in the same package as our ViewComponent Class.

// HomeViewComponent.html
<html xmlns:th="http://www.thymeleaf.org">
<body>
    <div th:text="${helloWorld}"></div>
    <br>
    <strong th:text="${coffee}"></strong>
</body>
</html>

We can then call the render method in our Controller

// Router.java
@Controller
public class Router {
    private final HomeViewComponent HomeViewComponent;

    public Router(HomeViewComponent HomeViewComponent) {
        this.HomeViewComponent = HomeViewComponent;
    }

    @GetMapping("/")
    ViewContext homeView(){
        return HomeViewComponent.render();
    }
}

If we now access the root url path of our spring application we can see that the component renders properly: thymeleaf-view-component-example

Nesting components:

We can also embed components to our templates, either with expression inlining [(${{}})] or with the th:utext=${{}} property. It is important to use double curly bracelets {{}}.

<div>
    [(${{@navigationViewComponent.render()}})]
</div>
<div th:utext="${{@navigationViewComponent.render()}}"></div>

Parameter components:

We can also create components with parameters. We can either use default values when we pass a null value, get a property from a Service or we can throw a custom error.

@ViewComponent
public class ParameterViewComponent {
    public ViewContext render(String parameter) throws Exception {
        if (parameter == null) {
            throw new Exception("You need to pass in a parameter");
        }
        return new ViewContext(
                ViewProperty.of("office", parameter)
                );
    }
}

// ParameterViewComponent.html
<h2>ParameterComponent:</h2>

<div th:text="${office}"></div>

This enables us to define the properties for our ParameterViewComponent in the HomeViewComponent:

// HomeViewComponent.java
@ViewComponent
public class HomeViewComponent {
    private final ExampleService exampleService;

    public HomeViewComponent(ExampleService exampleService) {
        this.exampleService = exampleService;
    }

    public ViewContext render() {
        return new ViewContext(
                ViewProperty.of("helloWorld", "Hello World"),
                ViewProperty.of("coffee", exampleService.getCoffee()),
                ViewProperty.of("office", exampleService.getOfficeHours())
        );
    }
}

// HomeViewComponent.html
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<body>

<div th:text="${helloWorld}"></div>

<div th:utext="${{@parameterViewComponent.render(office)}}"></div>

</body>
</html>

If we now access the root url path of our spring application we can see that the parameter component renders properly:

viewcomponent-parameter

Maven Installation

Add this to your pom.xml:

<project>
    <dependencies>
        <dependency>
            <groupId>de.github.tschuehly</groupId>
            <artifactId>thymeleaf-view-component</artifactId>
            <version>0.1.6</version>
        </dependency>
    </dependencies>
    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.html</include>
                </includes>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.3.0</version>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>Jitpack</id>
            <url>https://jitpack.io</url>
        </repository>
    </repositories>
</project>

GitHub

View Github