Spring Boot + Bootstrap + Thymeleaf Toggle

July 06, 2020 No comments Spring Boot Bootstrap Thymeleaf Toggle

Spring Boot + Bootstrap + Thymeleaf Toggle

1. Introduction

In this article, we are going to present a Thymeleaf Toggle component used in a sample HTML form and handled by the Spring Boot application. We will use external library bootstrap4-toggle that allows creating responsive switch buttons for Bootstrap applications.

To start with 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 presented in this article will use three common Maven dependencies:

2.2. Front-End libraries
  • bootstrap - a GUI framework for building responsive websites,
  • bootstrap4-toggle - custom responsive switch buttons that works as HTML checkboxes.

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-toggle</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 sample application will simulate user settings form with options aggregated in categories. The model layer will contain three objects:

  • Setting - responsible for storing key->value pairs,
  • SettingsGroup - responsible for aggregating pairs in groups,
  • Settings - contains a list of aggregated settings.

The Settings object has the following structure:

package com.frontbackend.thymeleaf.bootstrap.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Setting {

    private String label;
    private boolean value;
}

The SettingsGroup object has the following structure:

package com.frontbackend.thymeleaf.bootstrap.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.List;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SettingsGroup {

    private String group;

    private List<Setting> settingList;
}

The Settings object has the following structure:

package com.frontbackend.thymeleaf.bootstrap.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Settings {

    private List<SettingsGroup> groupList;
}

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.Settings;
import com.frontbackend.thymeleaf.bootstrap.service.SettingsService;

@Controller
@RequestMapping({ "/", "/index" })
public class IndexController {

    private final SettingsService settingsService;

    @Autowired
    public IndexController(SettingsService settingsService) {
        this.settingsService = settingsService;
    }

    @GetMapping
    public String main(Model model) {
        model.addAttribute("settings", settingsService.defaultUserProfileSettings());
        return "index";
    }

    @PostMapping
    public String save(Settings settings, Model model) {
        model.addAttribute("settings", settings);
        return "saved";
    }
}

SettingsService is a simple service that prepares aggregated settings that will be presented on the main website:

package com.frontbackend.thymeleaf.bootstrap.service;

import java.util.Arrays;

import org.springframework.stereotype.Service;

import com.frontbackend.thymeleaf.bootstrap.model.Setting;
import com.frontbackend.thymeleaf.bootstrap.model.Settings;
import com.frontbackend.thymeleaf.bootstrap.model.SettingsGroup;

@Service
public class SettingsService {

    public Settings defaultUserProfileSettings() {
        return Settings.builder()
                       .groupList(Arrays.asList(customer(), reports(), members()))
                       .build();
    }

    private SettingsGroup customer() {
        return SettingsGroup.builder()
                            .group("Customer")
                            .settingList(Arrays.asList(Setting.builder()
                                                              .label("Enable Notifications")
                                                              .value(false)
                                                              .build(),
                                    Setting.builder()
                                           .label("Enable Case Tracking")
                                           .value(false)
                                           .build(),
                                    Setting.builder()
                                           .label("Support")
                                           .value(false)
                                           .build()))
                            .build();
    }

    private SettingsGroup reports() {
        return SettingsGroup.builder()
                            .group("Reports")
                            .settingList(Arrays.asList(Setting.builder()
                                                              .label("Generate Reports")
                                                              .value(false)
                                                              .build(),
                                    Setting.builder()
                                           .label("Enable Report Export")
                                           .value(false)
                                           .build(),
                                    Setting.builder()
                                           .label("Allow Data Collection")
                                           .value(false)
                                           .build()))
                            .build();
    }

    private SettingsGroup members() {
        return SettingsGroup.builder()
                            .group("Members")
                            .settingList(Arrays.asList(Setting.builder()
                                                              .label("Enable Member Sign Up")
                                                              .value(false)
                                                              .build(),
                                    Setting.builder()
                                           .label("Allow User Feedback")
                                           .value(false)
                                           .build(),
                                    Setting.builder()
                                           .label("Enable Customer Portal")
                                           .value(false)
                                           .build()))
                            .build();
    }
}

The Application is 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 settings,
  • 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 Toggle</title>

    <link th:rel="stylesheet" th:href="@{assets/bootstrap4-toggle/bootstrap4-toggle.min.css}"/>
    <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 Toggle</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 class="container">

    <div class="row">
        <div class="col-md-4 mt-5">
            <form method="post" th:object="${settings}">

                <fieldset th:each="group, groupStat : ${settings.groupList}">
                    <h2 th:text="${group.group}">Group</h2>

                    <input type="hidden" th:value="${group.group}"
                           th:name="${'groupList[' + groupStat.index + '].group'}"/>

                    <th:block th:each="setting, settingStat : ${group.settingList}">
                        <div class="row form-group">
                            <div class="col-sm-8">
                                <label th:text="${setting.label}">Label</label>
                            </div>
                            <div class="col-sm-4">
                                <input type="hidden" th:value="${setting.label}"
                                       th:name="${'groupList[' + groupStat.index + '].settingList[' + settingStat.index + '].label'}"/>
                                <input type="checkbox" data-toggle="toggle"
                                       th:checked="${setting.value}"
                                       th:name="${'groupList[' + groupStat.index + '].settingList[' + settingStat.index + '].value'}"/>
                            </div>
                        </div>

                    </th:block>
                </fieldset>

                <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>
<script th:src="@{assets/bootstrap4-toggle/bootstrap4-toggle.min.js}"></script>

</body>
</html>

The generate custom toggle buttons we used bootstrap4-toggle library specially designed for responsive switch components.

Users choose which setting will be on, and submitted values will be presented on the separate page saved.html.

The saved.html file 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 Toggle</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 Toggle</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 class="container">
    <div class="row">
        <div class="col-md-4">
            <h2 class="mt-5 mb-4">Submitted settings</h2>

            <fieldset th:each="group, groupStat : ${settings.groupList}">
                <h2 th:text="${group.group}">Group</h2>

                <th:block th:each="setting, settingStat : ${group.settingList}">
                    <div class="row form-group">
                        <div class="col-sm-8">
                            <label th:text="${setting.label}">Label</label>
                        </div>
                        <div class="col-sm-4">
                            <strong th:class="${setting.value ? 'badge badge-success' : 'badge badge-secondary'}"
                                    th:text="${setting.value ? 'TRUE' : 'FALSE'}"></strong>
                        </div>
                    </div>

                </th:block>
            </fieldset>

            <a th:href="@{/}" class="btn btn-primary">Go back</a>
        </div>
    </div>
</div>

</body>
</html>

5. The result

The running application is available under http://locahost:8080 address and presents the following functionality:

Thymeleaf bootstrap toggle

6. Conclusion

In this article, we presented Thymeleaf Toggle component using a sample application that simulates user account settings.

As usual, the code used in this article is available under our GitHub repository.

{{ message }}

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