1. Introduction
In this article, we are going to present a way to build the Thymeleaf Checkbox Tree component using Thymeleaf fragments and a Vanilla JavaScript code.
If you need some more information about Thymeleaf and Forms check below links:
Thymeleaf configuration with Spring Boot
Working with Forms in Thymeleaf
2. Dependencies
2.1. Maven dependencies
The sample application use three main Maven dependencies:
- org.springframework.boot:spring-boot-starter-web:2.1.5.RELEASE - Spring Boot server, beans, controllers,
- org.springframework.boot:spring-boot-starter-thymeleaf:2.1.5.RELEASE - Thymeleaf template engine dependency,
- org.projectlombok:lombok:1.18.2 - setters/getters/constructor generator,
- org.webjars:bootstrap:4.0.0-2 - webjar with Bootstrap framework.
2.2. Front-End libraries
- bootstrap - a framework for creating responsive layouts.
Maven pom.xml
file have 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-bootstrap-checkbox-tree</artifactId>
<properties>
<bootstrap.version>4.0.0-2</bootstrap.version>
<webjars-locator.version>0.30</webjars-locator.version>
<lombok.version>1.18.2</lombok.version>
</properties>
<!-- 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>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>${bootstrap.version}</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator</artifactId>
<version>${webjars-locator.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</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. Model, Controller, Service and Main Application class
The model layer contains a single object BooleanNode
that represents the checkbox tree node and allows us to build complex hierarchic structures:
package com.frontbackend.thymeleaf.bootstrap.model;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BooleanNode {
private Boolean value;
private String label;
private List<BooleanNode> children;
}
The BooleanNodeService
prepare a sample tree structure, using BooleanNode
objects, that will be displayed on the website:
package com.frontbackend.thymeleaf.bootstrap.service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.stereotype.Service;
import com.frontbackend.thymeleaf.bootstrap.model.BooleanNode;
@Service
public class BooleanNodeService {
public BooleanNode buildTree() {
BooleanNode root = new BooleanNode();
List<BooleanNode> children = new ArrayList<>();
children.add(BooleanNode.builder()
.label("Checkbox1")
.value(false)
.children(Arrays.asList(BooleanNode.builder()
.value(false)
.label("Checkboxa1")
.build(),
BooleanNode.builder()
.value(false)
.label("Checkboxb1")
.build(),
BooleanNode.builder()
.value(false)
.label("Checkboxc1")
.build()))
.build());
children.add(BooleanNode.builder()
.label("Checkbox2")
.value(false)
.children(Arrays.asList(BooleanNode.builder()
.value(false)
.label("Checkboxa2")
.build(),
BooleanNode.builder()
.value(false)
.label("Checkboxb2")
.build(),
BooleanNode.builder()
.value(false)
.label("Checkboxc2")
.build()))
.build());
children.add(BooleanNode.builder()
.label("Checkbox3")
.value(false)
.children(Arrays.asList(BooleanNode.builder()
.value(false)
.label("Checkboxa3")
.build(),
BooleanNode.builder()
.value(false)
.label("Checkboxb3")
.build(),
BooleanNode.builder()
.value(false)
.label("Checkboxc3")
.children(Arrays.asList(BooleanNode.builder()
.label("Checkbox31")
.value(false)
.build(),
BooleanNode.builder()
.label("Checkbox32")
.value(false)
.build(),
BooleanNode.builder()
.label("Checkbox33")
.value(false)
.build()))
.build()))
.build());
root.setChildren(children);
return root;
}
}
All GET and POST requests to the root context are handled by IndexController
class that has the following structure:
package com.frontbackend.thymeleaf.bootstrap.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.frontbackend.thymeleaf.bootstrap.model.BooleanNode;
import com.frontbackend.thymeleaf.bootstrap.service.BooleanNodeService;
@Controller
@RequestMapping({ "/", "/index" })
public class IndexController {
private final BooleanNodeService booleanNodeService;
@Autowired
public IndexController(BooleanNodeService booleanNodeService) {
this.booleanNodeService = booleanNodeService;
}
@GetMapping
public String main(Model model) {
model.addAttribute("root", booleanNodeService.buildTree());
return "index";
}
@PostMapping
public String save(BooleanNode root, Model model) {
model.addAttribute("root", root);
return "saved";
}
}
The Application
is annotated with @SpringBootApplication to become the main class that starts the Spring Boot application server:
package com.frontbackend.thymeleaf.bootstrap;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
4. Templates
The presentation layer contains two Thymeleaf templates:
- index.html - template where user can provide values for the sample checkbox tree,
- saved.html - template for presenting submitted values from the previous view.
The index.html
template has the following structure:
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Spring Boot Thymeleaf Application - Bootstrap Checkbox Tree</title>
<link th:rel="stylesheet" th:href="@{webjars/bootstrap/4.0.0-2/css/bootstrap.min.css} "/>
<link th:rel="stylesheet" th:href="@{webjars/font-awesome/5.11.2/css/all.css} "/>
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top">
<div class="container">
<a class="navbar-brand" href="/">Thymeleaf - Bootstrap Checkbox Tree</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive"
aria-controls="navbarResponsive"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
<li class="nav-item active">
<a class="nav-link" href="#">Home
<span class="sr-only">(current)</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">About</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Services</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Contact</a>
</li>
</ul>
</div>
</div>
</nav>
<div th:fragment="checkboxChildren(children, path)">
<div th:class="${#strings.equals('children', path) ? '' : 'ml-5'}" th:each="child, stat : ${children}"
th:with="valuePath=${path + '[' + stat.index + '].value'}, labelPath=${path + '[' + stat.index + '].label'}">
<label>
<th:block th:text="${child.label}">Label</th:block>
<input type="hidden" th:name="${labelPath}" th:value="${child.label}"/>
<input type="checkbox" th:name="${valuePath}" th:checked="${child.value}"
th:value="${child.value}"
onclick="this.value = this.checked"/>
</label>
<div th:if="${!#lists.isEmpty(child.children)}"
th:replace="index :: checkboxChildren(${child.children}, ${path + '[' + stat.index + '].children'})"></div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-sm-12 mt-5">
<form method="post" th:object="${root}">
<div th:if="${!#lists.isEmpty(children)}"
th:replace="index :: checkboxChildren(${root.children}, 'children')"></div>
<button class="btn btn-primary" type="submit">Submit form</button>
</form>
</div>
</div>
</div>
<script th:src="@{/webjars/jquery/jquery.min.js}"></script>
<script th:src="@{/webjars/popper.js/umd/popper.min.js}"></script>
<script th:src="@{/webjars/bootstrap/js/bootstrap.min.js}"></script>
</body>
</html>
To display tree structure we use a Thymeleaf fragment functionality.
Fragment with checkboxes for child nodes (th:fragment="checkboxChildren(children, path)"
) takes two parameters: children
and the path
:
- children - the list of the
CheckboxNode
objects (children), - path - path for naming inputs (for example on the second level we will have
children[0].children[0].value
path).
This fragment is called recursively, so we can present any kind of hierarchic trees, even multilevel ones.
We used two local variables to keep this template clean and readable:
valuePath - this stores path to the value attribute (eg.
children[0].value
,children[1].value
,children[0].children[2].value
etc),labelPath - this stores path to the label attribute (eg.
children[0].label
,children[2].label
,children[1].children[2].children[3].label
etc).
Unfortunately th:field
doesn't work in that case, because of the complex of the field naming in a tree structure, so we have to handle that by ourselves. That's why we have one hidden input for a label and another one for a checkbox. JS code puts value true
or false
according to the checkbox state:
<input type="hidden" th:name="${labelPath}" th:value="${child.label}"/>
<input type="checkbox" th:name="${valuePath}" th:checked="${child.value}"
th:value="${child.value}"
onclick="this.value = this.checked"/>
The saved.html
view simple presents submitted values:
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Spring Boot Thymeleaf Application - Bootstrap Checkbox Tree</title>
<link th:rel="stylesheet" th:href="@{webjars/bootstrap/4.0.0-2/css/bootstrap.min.css} "/>
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top">
<div class="container">
<a class="navbar-brand" href="/">Thymeleaf - Bootstrap Checkbox Tree</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive"
aria-controls="navbarResponsive"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
<li class="nav-item active">
<a class="nav-link" href="#">Home
<span class="sr-only">(current)</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">About</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Services</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Contact</a>
</li>
</ul>
</div>
</div>
</nav>
<div th:fragment="printNode(children, root)">
<div th:class="${root ? '' : 'ml-5'}" th:each="child, stat : ${children}">
<label th:text="${child.label}">Label</label> <strong
th:class="${child.value == true ? 'badge badge-success' : 'badge badge-secondary'}"
th:text="${child.value == true ? 'checked' : 'not checked'}">checked/not checked</strong>
<div th:if="${!#lists.isEmpty(child.children)}"
th:replace="saved :: printNode(${child.children}, false)"></div>
</div>
</div>
<div class="container">
<div class="container">
<div class="row">
<div class="col-sm-12 mt-5">
</div>
</div>
<div th:if="${!#lists.isEmpty(root.children)}"
th:replace="saved :: printNode(${root.children}, true)">
</div>
</div>
<a th:href="@{/}" class="btn btn-primary">Go back</a>
</div>
</body>
</html>
5. The output
The running application is available under http://locahost:8080
URL and presents the following functionality:
6. Conclusion
In this article, we presented how to create Thymeleaf Checkbox Tree using th:fragments
. Thymeleaf has difficulties with handling such complex tree structures using th:field
attribute so we handled that by ourselves.
As usual, the code used in this article is available under our GitHub repository.
{{ 'Comments (%count%)' | trans {count:count} }}
{{ 'Comments are closed.' | trans }}