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:
2.2. Front-End libraries
bootstrap - a framework for creating responsive layouts.
Maven pom.xml
file have the following structure:
Copy
<?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:
Copy
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:
Copy
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:
Copy
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:
Copy
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:
Copy
<!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:
Copy
<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:
Copy
<!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 }}