Spring Boot + Bootstrap + Thymeleaf Checkbox Tree

July 06, 2020 2 Comments Spring Boot Bootstrap Thymeleaf Checkbox Tree

Spring Boot + Bootstrap + Thymeleaf Checkbox Tree

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:

<?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:

Thymeleaf bootstrap time picker

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.

{{ message }}

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