Blog

How to create a user registration form in pure Java

By  
Tarek Oraby
Tarek Oraby
·
On Sep 5, 2024 11:00:00 AM
·

Learn how to create a responsive signup form with error handling and cross-field validation written in pure Java. Last updated September 2024.

Registration Form Tutorial Featured ImageIn 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

Signup form in Java

What you need

  • About 15 minutes
  • JDK 17 or later 

Import a starter project

  1. Download a starter project by clicking the following button:DOWNLOAD A STARTER
  2. 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
   @Email
   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

Tarek Oraby
Tarek Oraby
Tarek is a Product Manager at Vaadin. His daily work is a blend of talking to users, planning, and overseeing the evolution of Vaadin Flow and Hilla, ensuring they consistently delight our developer community.
Other posts by Tarek Oraby