Spring Boot Thymeleaf jQuery MultiSteps Form

Table of Contents

Introduction

In this example I am going to show you how to build multisteps form using Spring Boot thymeleaf jquery. Multi steps form is required when users have to fill a significant number of fields in the online registration form or some kind of registration or kyc related form and the form goes long at the bottom if the form is not broken into multi-steps.

Having multi-steps form provides some benefits from the end users’ perspective, because input fields on the form are grouped according to their nature of information; another thing is end users do not need to scroll down to fill up the form. So, the users just fill up the information and goes to the next step and fill up the information until the last step. This way also it hides the input fields in the next step.

Related Posts:

Prerequisites

Java 1.8+ (11 – 12), Spring Boot 2.6.7, MySQL 8.0.26, jQuery 3.6.0, Maven 3.8.5, Hibernate Validator 8.0.0.Alpha3

Project Setup

The following pom.xml file can be used to build the Spring Boot project.

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

	<groupId>com.roytuts</groupId>
	<artifactId>spring-boot-jquery-multi-step-form</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.source>11</maven.compiler.source>
		<maven.compiler.target>11</maven.compiler.target>
	</properties>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.7</version>
	</parent>

	<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.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
		</dependency>-->

		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-validator</artifactId>
			<version>8.0.0.Alpha3</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

MySQL Table

The following MySQL table can be used for this example. The table is created under roytuts database.

CREATE TABLE `users` (
	`id` INT COLLATE utf8mb4_unicode_ci NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(100) COLLATE utf8mb4_unicode_ci NOT NULL,
	`password` VARCHAR(255) COLLATE utf8mb4_unicode_ci NULL,
	`email` VARCHAR(100)COLLATE utf8mb4_unicode_ci NULL,
	`phone` INT COLLATE utf8mb4_unicode_ci NOT NULL,
	`gender` VARCHAR(6) COLLATE utf8mb4_unicode_ci NOT NULL,
	`dob` VARCHAR(10) COLLATE utf8mb4_unicode_ci NOT NULL,
	`address` VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL,
	PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

DataSource Config

The following content is written into src/main/resources/application.properties file with the standard naming conventions, so you don’t need to create a datasource manually and Spring will create one for you.

spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/roytuts
spring.datasource.username=root
spring.datasource.password=root

Entity Class

I am using Spring Data JPA to query the database and get benefits of Spring framework to use the built-in API for performing database activities. The following entity class maps to the corresponding table. I have also used validation for each field using Hibernate validator.

package com.roytuts.spring.boot.jquery.multistep.form;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.Email;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

import org.springframework.format.annotation.DateTimeFormat;

@Entity
@Table(name = "users")
public class User {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;

	@NotEmpty
	@Column
	private String name;

	@Email
	@NotBlank
	@Column
	private String email;

	@NotNull
	@Column
	private String password;

	@Min(10)
	@Column
	private Long phone;

	@NotEmpty
	@Column
	private String gender;

	@NotEmpty
	@Column
	@DateTimeFormat(pattern = "dd-MM-yyyy")
	private String dob;

	@NotNull
	@Column
	private String address;

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public Long getPhone() {
		return phone;
	}

	public void setPhone(Long phone) {
		this.phone = phone;
	}

	public String getGender() {
		return gender;
	}

	public void setGender(String gender) {
		this.gender = gender;
	}

	public String getDob() {
		return dob;
	}

	public void setDob(String dob) {
		this.dob = dob;
	}

	public String getAddress() {
		return address;
	}

	public void setAddress(String address) {
		this.address = address;
	}

}

Repository Interface

The repository interface (JpaRepository or CrudRepository) provides built-in methods for querying database.

package com.roytuts.spring.boot.jquery.multistep.form;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Integer> {

}

Service Class

The service class is supposed to handle the business logic for the application.

package com.roytuts.spring.boot.jquery.multistep.form;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

	@Autowired
	private UserRepository userRepository;

	public void save(final User user) {
		userRepository.save(user);
	}

}

Spring Web Controller

The web controller class that handles requests and responses for the clients or end users.

package com.roytuts.spring.boot.jquery.multistep.form;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class AppController {

	@Autowired
	private UserService userService;

	@GetMapping("/")
	public String showUserForm(User user) {
		return "index";
	}

	@PostMapping("/")
	public String validateUserForm(@Valid User user, BindingResult bindingResult) {
		if (bindingResult.hasErrors()) {
			return "index";
		}

		userService.save(user);

		return "redirect:/success";
	}

	@ResponseBody
	@GetMapping("/success")
	public String success() {
		return "User data successfully saved!";
	}

}

Spring Boot Main Class

A class is having a method method with @SpringBootApplication annotation will deploy the app into embedded Tomcat server.

package com.roytuts.spring.boot.jquery.multistep.form;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
//@EntityScan(basePackages = "com.roytuts.spring.boot.jquery.multistep.form")
//@EnableJpaRepositories(basePackages = "com.roytuts.spring.boot.jquery.multistep.form")
public class App {

	public static void main(String[] args) {
		SpringApplication.run(App.class, args);
	}

}

UI (User Interface)

The standard directory for keeping the static resources is src/main/resources/templates folder from where Spring Boot application will pick up the static content and serve. The following content is written into the file index.html. I am using Spring’s Thymeleaf framework for the UI purpose.

<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring Boot Multi Step Form</title>
<style>
body {
	font-family: tahoma;
	font-size: 12px;
}

#signup-step {
	margin: auto;
	padding: 0;
	width: 53%
}

#signup-step li {
	list-style: none;
	float: left;
	padding: 5px 10px;
	border-top: #004C9C 1px solid;
	border-left: #004C9C 1px solid;
	border-right: #004C9C 1px solid;
	border-radius: 5px 5px 0 0;
}

.active {
	color: #FFF;
}

#signup-step li.active {
	background-color: #004C9C;
}

#signup-form {
	clear: both;
	border: 1px #004C9C solid;
	padding: 20px;
	width: 50%;
	margin: auto;
}

.demoInputBox {
	padding: 10px;
	border: #CDCDCD 1px solid;
	border-radius: 4px;
	background-color: #FFF;
	width: 50%;
}

.signup-error {
	color: #FF0000;
	padding-left: 15px;
}

.message {
	color: #00FF00;
	font-weight: bold;
	width: 100%;
	padding: 10;
}

.btnAction {
	padding: 5px 10px;
	background-color: #F00;
	border: 0;
	color: #FFF;
	cursor: pointer;
	margin-top: 15px;
}

label {
	line-height: 35px;
}
</style>

<script src="https://code.jquery.com/jquery-3.6.0.min.js"
	integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
	crossorigin="anonymous"></script>

<script>
	function validate() {
		var output = true;
		$(".signup-error").html('');

		if ($("#personal-field").css('display') != 'none') {
			if (!($("#name").val())) {
				output = false;
				$("#name-error").html("Name required!");
			}

			if (!($("#dob").val())) {
				output = false;
				$("#dob-error").html("Date of Birth required!");
			}
		}

		if ($("#password-field").css('display') != 'none') {
			if (!($("#user-password").val())) {
				output = false;
				$("#password-error").html("Password required!");
			}

			if (!($("#confirm-password").val())) {
				output = false;
				$("#confirm-password-error").html("Confirm password required!");
			}

			if ($("#user-password").val() != $("#confirm-password").val()) {
				output = false;
				$("#confirm-password-error").html("Password not matched!");
			}
		}

		if ($("#contact-field").css('display') != 'none') {
			if (!($("#phone").val())) {
				output = false;
				$("#phone-error").html("Phone required!");
			}

			if (!($("#email").val())) {
				output = false;
				$("#email-error").html("Email required!");
			}

			if (!$("#email").val().match(/^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/)) {
				$("#email-error").html("Invalid Email!");
				output = false;
			}

			if (!($("#address").val())) {
				output = false;
				$("#address-error").html("Address required!");
			}
		}

		return output;
	}

	$(document).ready(function() {
		$("#next").click(function() {
			var output = validate();
			if (output === true) {
				var current = $(".active");
				var next = $(".active").next("li");
				if (next.length > 0) {
					$("#" + current.attr("id") + "-field").hide();
					$("#" + next.attr("id") + "-field").show();
					$("#back").show();
					$("#finish").hide();
					$(".active").removeClass("active");
					next.addClass("active");
					if ($(".active").attr("id") == $("li").last().attr("id")) {
						$("#next").hide();
						$("#finish").show();
					}
				}
			}
		});

		$("#back").click(function() {
			var current = $(".active");
			var prev = $(".active").prev("li");
			if (prev.length > 0) {
				$("#" + current.attr("id") + "-field").hide();
				$("#" + prev.attr("id") + "-field").show();
				$("#next").show();
				$("#finish").hide();
				$(".active").removeClass("active");
				prev.addClass("active");
				if ($(".active").attr("id") == $("li").first().attr("id")) {
					$("#back").hide();
				}
			}
		});

		$("input#finish").click(function(e) {
			var output = validate();
			var current = $(".active");

			if (output === true) {
				return true;
			} else {
				//prevent refresh
				e.preventDefault();
				$("#" + current.attr("id") + "-field").show();
				$("#back").show();
				$("#finish").show();
			}
		});
	});
</script>

</head>
<body>
	<ul id="signup-step">
		<li id="personal" class="active">Personal Detail</li>
		<li id="password">Password</li>
		<li id="contact">Contact</li>
	</ul>

	<form name='frmRegistration' id='signup-form' th:action="@{/}" th:object="${user}" method="post">
		<div id="personal-field">
			<label>Name</label><span id="name-error" class="signup-error"></span>
			<div>
				<input type="text" name="name" id="name" th:field="*{name}" class="demoInputBox" />
			</div>
			<label>Date of Birth</label><span id="dob-error" class="signup-error"></span>
			<div>
				<input type="text" name="dob" id="dob" th:field="*{dob}" class="demoInputBox" />
			</div>
			<label>Gender</label>
			<div>
				<select name="gender" id="gender" th:field="*{gender}" class="demoInputBox">
					<option value="male">Male</option>
					<option value="female">Female</option>
				</select>
			</div>
		</div>

		<div id="password-field" style="display: none;">
			<label>Enter Password</label><span id="password-error"
				class="signup-error"></span>
			<div>
				<input type="password" name="password" th:field="*{password}" id="user-password"
					class="demoInputBox" />
			</div>
			<label>Re-enter Password</label><span id="confirm-password-error"
				class="signup-error"></span>
			<div>
				<input type="password" name="confirm-password" id="confirm-password"
					class="demoInputBox" />
			</div>
		</div>

		<div id="contact-field" style="display: none;">
			<label>Phone</label><span id="phone-error" class="signup-error"></span>
			<div>
				<input type="text" name="phone" th:field="*{phone}" id="phone" class="demoInputBox" />
			</div>
			<label>Email</label><span id="email-error" class="signup-error"></span>
			<div>
				<input type="text" name="email" th:field="*{email}" id="email" class="demoInputBox" />
			</div>
			<label>Address</label><span id="address-error" class="signup-error"></span>
			<div>
				<textarea name="address" th:field="*{address}" id="address" class="demoInputBox" rows="5"
					cols="50"></textarea>
			</div>
		</div>

		<div>
			<input class="btnAction" type="button" name="back" id="back"
				value="Back" style="display: none;"> <input
				class="btnAction" type="button" name="next" id="next" value="Next">
			<input class="btnAction" type="submit" name="finish" id="finish"
				value="Finish" style="display: none;">
		</div>
	</form>
</body>
</html>

Testing Multi Steps Form

Now you can run the main class to deploy the application and accessing the URL at http://localhost:8080 will show you the following page in the browser:

multi steps registration form

You cannot go to the next step until you fill up all required fields. Once you reach the last step and fill all inputs, you can save the information. The information will be saved to the table users.

Source Code

Download

Leave a Reply

Your email address will not be published. Required fields are marked *