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:
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-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:
Copy
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:
Copy
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:
Copy
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:
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.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:
Copy
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:
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 settings,
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 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:
Copy
<!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:
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.
{{ 'Comments (%count%)' | trans {count:count} }}
{{ 'Comments are closed.' | trans }}