Learn how to create a responsive signup form with error handling and cross-field validation written in pure Java. Last updated September 2024.
In this guide, we create a standard registration form with error handling and cross-field validation. This signup form is developed entirely in pure Java using Vaadin Flow (no HTML or JavaScript involved). What’s more, the form is responsive, meaning that it automatically adjusts its content to fit different screen sizes. You can explore the full source code on GitHub.
What you need
- About 15 minutes
- JDK 17 or later
Import a starter project
- Download a starter project by clicking the following button:
- Unzip the starter and open the project in your favorite IDE.
Define the Domain Class
Let’s start by creating a UserDetails
class, which is a simple Java Bean that represents the user details that the registration form will collect. The UserDetails
class implements the validation logic using the standard JSR-303 Bean Validation specification of the Java API. For example, the email field has the @NotBlank
and @Email
annotations, ensuring that this field will only hold a well-formed email address.
Please note: In order to simplify this tutorial, UserDetails
class stores passwords as clear text. However, in production, passwords should always be encoded.
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class UserDetails {
@NotBlank
private String firstName;
@NotBlank
private String lastName;
@NotBlank
private String email;
private boolean allowsMarketing;
// FIXME Passwords should never be stored in plain text!
@Size(min = 8, max = 64, message = "Password must be 8-64 char long")
private String password;
public String getFirstName() {return firstName;}
public void setFirstName(String firstName) {this.firstName = firstName;}
public String getLastName() {return lastName;}
public void setLastName(String lastName) {this.lastName = lastName;}
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 boolean isAllowsMarketing() {return allowsMarketing;}
public void setAllowsMarketing(boolean allowsMarketing) {this.allowsMarketing = allowsMarketing;}
}
Create a Responsive Signup Form
Next, we create the visual elements of the registration form and make this form responsive. We can do this in pure Java using Vaadin, which comes with an extensive set of UI components that we can use as the building blocks for any application.
The RegistrationForm
class extends Vaadin FormLayout
. The FormLayout
doesn't yet have any logic (validation, etc.), but it allows us to configure responsiveness from Java code, and its defaults look nicer than just placing our content inside a div. Like with other Vaadin layouts, we add the components to a FormLayout
by first declaring the desired component (e.g. TextField firstName = new TextField("First name");
), we then use the add()
method to add this component to the layout.
Note that we customize the looks of the form using methods inherited from the FormLayout
API such as setMaxWidth()
, setResponsiveSteps()
, and setColspan()
.
import com.vaadin.flow.component.HasValueAndElement;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.textfield.EmailField;
import com.vaadin.flow.component.textfield.PasswordField;
import com.vaadin.flow.component.textfield.TextField;
import java.util.stream.Stream;
public class RegistrationForm extends FormLayout {
private H3 title;
private TextField firstName;
private TextField lastName;
private EmailField email;
private PasswordField password;
private PasswordField passwordConfirm;
private Checkbox allowMarketing;
private Span errorMessageField;
private Button submitButton;
public RegistrationForm() {
title = new H3("Signup form");
firstName = new TextField("First name");
lastName = new TextField("Last name");
email = new EmailField("Email");
allowMarketing = new Checkbox("Allow Marketing Emails?");
allowMarketing.getStyle().set("margin-top", "10px");
password = new PasswordField("Password");
passwordConfirm = new PasswordField("Confirm password");
setRequiredIndicatorVisible(firstName, lastName, email, password,
passwordConfirm);
errorMessageField = new Span();
submitButton = new Button("Join the community");
submitButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
add(title, firstName, lastName, email, password,
passwordConfirm, allowMarketing, errorMessageField,
submitButton);
// Max width of the Form
setMaxWidth("500px");
// Allow the form layout to be responsive.
// On device widths 0-490px we have one column.
// Otherwise, we have two columns.
setResponsiveSteps(
new ResponsiveStep("0", 1, ResponsiveStep.LabelsPosition.TOP),
new ResponsiveStep("490px", 2, ResponsiveStep.LabelsPosition.TOP));
// These components always take full width
setColspan(title, 2);
setColspan(email, 2);
setColspan(errorMessageField, 2);
setColspan(submitButton, 2);
}
public PasswordField getPasswordField() { return password; }
public PasswordField getPasswordConfirmField() { return passwordConfirm; }
public Span getErrorMessageField() { return errorMessageField; }
public Button getSubmitButton() { return submitButton; }
private void setRequiredIndicatorVisible(HasValueAndElement<?, ?>... components) {
Stream.of(components).forEach(comp -> comp.setRequiredIndicatorVisible(true));
}
}
Add Data Binding and Validation
In order to bind our domain class with the UI, we now create the logic by which we bind the UserDetails
bean with the RegistrationForm
component that we've just created.
The following RegistrationFormBinder
class abstracts this binding logic. It makes use of Vaadin’s BeanValidationBinder.bindInstanceFields()
method, which facilitates automatic data binding and validation by automatically matching the fields of the RegistrationForm object to the properties of the UserDetails
object based on their names. For instance, this method automatically binds the firstName
String field of the UserDetails
class with the firstName
text field component of the RegistrationForm
class.
"What about the validation?" you ask. Well, that is conveniently provided at no extra effort based on the JSR-303 Bean Validation annotations that were added to the UserDetails
class. For instance, since the firstName
field of the UserDetails
class is annotated with @NotBlank
, Vaadin will automatically ensure that the associated TextField
in the registration form will not be blank (informing the user with an error message if it is so).
Of course, there are cases where the automatic binding mechanism is insufficient. For those cases, we can easily provide our own custom binding and validation logic. The password field of the registration form represents such a case: we have to do the validation for this field manually since its logic is tied to one extra UI component, namely the password-confirmation field.
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.data.binder.BeanValidationBinder;
import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.data.binder.ValidationResult;
import com.vaadin.flow.data.binder.ValueContext;
public class RegistrationFormBinder {
private RegistrationForm registrationForm;
/**
* Flag for disabling first run for password validation
*/
private boolean enablePasswordValidation;
public RegistrationFormBinder(RegistrationForm registrationForm) {
this.registrationForm = registrationForm;
}
/**
* Method to add the data binding and validation logics
* to the registration form
*/
public void addBindingAndValidation() {
BeanValidationBinder<UserDetails> binder = new BeanValidationBinder<>(UserDetails.class);
binder.bindInstanceFields(registrationForm);
// A custom validator for password fields
binder.forField(registrationForm.getPasswordField())
.withValidator(this::passwordValidator).bind("password");
// The second password field is not connected to the Binder, but we
// want the binder to re-check the password validator when the field
// value changes. The easiest way is just to do that manually.
registrationForm.getPasswordConfirmField().addValueChangeListener(e -> {
// The user has modified the second field, now we can validate and show errors.
// See passwordValidator() for how this flag is used.
enablePasswordValidation = true;
binder.validate();
});
// Set the label where bean-level error messages go
binder.setStatusLabel(registrationForm.getErrorMessageField());
// And finally the submit button
registrationForm.getSubmitButton().addClickListener(event -> {
try {
// Create empty bean to store the details into
UserDetails userBean = new UserDetails();
// Run validators and write the values to the bean
binder.writeBean(userBean);
// Typically, you would here call backend to store the bean
// Show success message if everything went well
showSuccess(userBean);
} catch (ValidationException exception) {
// validation errors are already visible for each field,
// and bean-level errors are shown in the status label.
// We could show additional messages here if we want, do logging, etc.
}
});
}
/**
* Method to validate that:
* <p>
* 1) Password is at least 8 characters long
* <p>
* 2) Values in both fields match each other
*/
private ValidationResult passwordValidator(String pass1, ValueContext ctx) {
/*
* Just a simple length check. A real version should check for password
* complexity as well!
*/
if (pass1 == null || pass1.length() < 8) {
return ValidationResult.error("Password should be at least 8 characters long");
}
if (!enablePasswordValidation) {
// user hasn't visited the field yet, so don't validate just yet, but next time.
enablePasswordValidation = true;
return ValidationResult.ok();
}
String pass2 = registrationForm.getPasswordConfirmField().getValue();
if (pass1 != null && pass1.equals(pass2)) {
return ValidationResult.ok();
}
return ValidationResult.error("Passwords do not match");
}
/**
* We call this method when form submission has succeeded
*/
private void showSuccess(UserDetails userBean) {
Notification notification =
Notification.show("Data saved, welcome " + userBean.getFirstName());
notification.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
// Here you'd typically redirect the user to another view
}
}
Create the Registration View
Now we define the main view that holds the registration form. This view is itself a component (specifically a VerticalLayout
) to which the registration form is added. This view is made accessible to the end user via the @Route
annotation (in this case, it would be accessible via the empty route).
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
@Route("")
public class RegistrationView extends VerticalLayout {
public RegistrationView() {
RegistrationForm registrationForm = new RegistrationForm();
// Center the RegistrationForm
setHorizontalComponentAlignment(Alignment.CENTER, registrationForm);
add(registrationForm);
RegistrationFormBinder registrationFormBinder = new RegistrationFormBinder(registrationForm);
registrationFormBinder.addBindingAndValidation();
}
}
Run the application
To run the project from the command line, type mvnw spring-boot:run
(Windows), or ./mvnw spring-boot:run
(Mac & Linux).
Then in your browser, open http://localhost:8080.
Summary
Congratulations! You have created a responsive registration form with data binding and validation. And you did it in pure Java without the need to expose REST services or think about HTTP requests, responses, and filter chains.
You can explore the full source code on GitHub.
See Also
- Docs: Binding Beans to Forms
- Docs: Binding Data to Forms (discusses ways to do the binding even if your data is not stored in Beans)
- Vaadin Flow In-Depth Tutorial
- Configure and download a customized starting point for your application from start.vaadin.com