Documentation

Documentation versions (currently viewingVaadin 23)

You are viewing documentation for Vaadin 23. View latest documentation

Validating and Converting User Input

Like individual fields, forms with multiple fields can be validated before saving the input to the bound business objects.

Binder supports:

  • validating user input, and

  • converting value types from types used in business objects to types used in bound UI components, and vice versa.

These concepts go hand in hand, because validation can be based on a converted value, and the ability to convert a value is a kind of validation in itself.

Vaadin includes several validators and converters that you can implement.

Validating User Input

It’s typical for applications to restrict the kind of value the user is allowed to enter into certain fields.

Defining Validators

Binder allows you to define validators for each bound field. By default, validators run whenever the user changes the field value. The validation status is also checked when writing to the bean.

You should define the field validator between the forField() and bind() code lines when creating the binding.

Example: Defining a validator using a Validator instance or an inline lambda expression.

binder.forField(emailField)
    // Explicit validator instance
    .withValidator(new EmailValidator(
        "This doesn't look like a valid email address"))
    .bind(Person::getEmail, Person::setEmail);

binder.forField(nameField)
    // Validator defined based on a lambda
    // and an error message
    .withValidator(
        name -> name.length() >= 3,
        "Name must contain at least three characters")
    .bind(Person::getName, Person::setName);

binder.forField(titleField)
    // Shorthand for requiring the field to be non-empty
    .asRequired("Every employee must have a title")
    .bind(Person::getTitle, Person::setTitle);
  • Binder.forField() works like a builder: The forField() call starts the process; it’s followed by various configuration calls for the field; and bind() is the final method of the configuration.

  • asRequired() is used for mandatory fields. With this:

    • a visual "required" indicator displays;

    • if the user leaves the field empty, an error message displays.

Customizing Validation Error Messages

You can customize the way error messages display by defining a ValidationStatusHandler or configuring the Label for each binding. The label is used to show the status of the field. The label can be used for validation errors, as well as confirmation and helper messages.

Example: Configuring validation messages for email and minimum-length validation.

Label emailStatus = new Label();
emailStatus.getStyle().set("color", "Red");
binder.forField(emailField)
    .withValidator(new EmailValidator(
        "This doesn't look like a valid email address"))
    // Shorthand that updates the label based on the
    // status
    .withStatusLabel(emailStatus)
    .bind(Person::getEmail, Person::setEmail);

Label nameStatus = new Label();

binder.forField(nameField)
    // Define the validator
    .withValidator(
        name -> name.length() >= 3,
        "Name must contain at least three characters")
    // Define how the validation status is displayed
    .withValidationStatusHandler(status -> {
        nameStatus.setText(status
                .getMessage().orElse(""));
        nameStatus.setVisible(status.isError());
    })
    // Finalize the binding
    .bind(Person::getName, Person::setName);
  • The withStatusLabel(Label label) method sets the given label to show an error message if the validation fails.

As an alternative to using labels, you can set a custom validation status handler, using the withValidationStatusHandler() method. This allows you to customize how the binder displays error messages and is more flexible than using the status label approach.

Adding Multiple Validators

You can add multiple validators for the same binding.

Example: Defining two validators: first, for the email input, and second, for the expected domain.

binder.forField(emailField)
    .withValidator(new EmailValidator(
        "This doesn't look like a valid email address"))
    .withValidator(
        email -> email.endsWith("@acme.com"),
        "Only acme.com email addresses are allowed")
    .bind(Person::getEmail, Person::setEmail);

Triggering Revalidation

The validation of one field can depend on the value of another field. You can achieve this by saving the binding to a local variable and triggering revalidation when the other field fires a value-change event.

Example: Storing a binding for later revalidation.

Binder<Trip> binder = new Binder<>(Trip.class);
DatePicker departing = new DatePicker();
departing.setLabel("Departing");
DatePicker returning = new DatePicker();
returning.setLabel("Returning");

// Store return date binding so we can
// revalidate it later
Binder.Binding<Trip, LocalDate> returningBinding =
    binder
       .forField(returning).withValidator(
               returnDate -> !returnDate
                       .isBefore(departing.getValue()),
               "Cannot return before departing")
       .bind(Trip::getReturnDate, Trip::setReturnDate);

// Revalidate return date when departure date changes
departing.addValueChangeListener(
        event -> returningBinding.validate());

Converting User Input

You can bind application data to a UI field component, even if the types don’t match.

Examples where this is useful include:

  • an application-specific type for a postal code that the user enters in a TextField;

  • requesting that the user enter only integers in a TextField;

  • selecting enumeration values in a Checkbox field.

Defining Converters

Like validators, each binding can have one or more converters, with an optional error message.

You can define converters using callbacks (typically lambda expressions), method references, or by implementing the Converter interface.

Examples: Defining converters.

TextField yearOfBirthField =
    new TextField("Year of birth");

binder.forField(yearOfBirthField)
    .withConverter(
        new StringToIntegerConverter("Not a number"))
    .bind(Person::getYearOfBirth,
        Person::setYearOfBirth);

// Checkbox for marital status
Checkbox marriedField = new Checkbox("Married");

binder.forField(marriedField).withConverter(
  m -> m ? MaritalStatus.MARRIED : MaritalStatus.SINGLE,
  MaritalStatus.MARRIED::equals)
.bind(Person::getMaritalStatus,
    Person::setMaritalStatus);

Adding Multiple Converters

You can add multiple converters (and validators) for each binding.

Each validator or converter is used in the order defined in the class. The value is passed along until:

  • a final converted value is stored in the business object, or

  • the first validation error or impossible conversion is encountered.

Example: Validator and converter sequence.

binder.forField(yearOfBirthField)
    // Validator is run with the String value
    // of the field
    .withValidator(text -> text.length() == 4,
            "Doesn't look like a year")
    // Converter is only run for strings
    // with 4 characters
    .withConverter(new StringToIntegerConverter(
            "Must enter a number"))
    // Validator is run with the converted value
    .withValidator(year -> year >= 1900 && year < 2000,
            "Person must be born in the 20th century")
    .bind(Person::getYearOfBirth,
            Person::setYearOfBirth);

When updating UI components, values from the business object are passed through each converter in reverse order (without validation).

Note
Although it’s possible to use a converter as a validator, best practice is to use a validator to check the contents of a field, and a converter to modify the value. This improves code clarity and avoids excessive boilerplate code.

Conversion Error Messages

You can define a custom error message to be used if a conversion throws an unchecked exception.

When using callbacks, you should provide one converter in each direction. If the callback that’s used to convert the user-provided value throws an unchecked exception, the field is marked as invalid, and the exception message is used as the validation error message. Java runtime exception messages are typically written for developers, and may not be suitable for end users.

Example: Defining a custom conversion error message.

binder.forField(yearOfBirthField)
    .withConverter(
        Integer::valueOf,
        String::valueOf,
        // Text to use instead of the
        // NumberFormatException message
        "Enter a number")
    .bind(Person::getYearOfBirth,
            Person::setYearOfBirth);

Implementing the Converter Interface

You need to implement two methods from the Converter interface:

  • convertToModel() receives a value that originates from the user.

    • The method returns a Result that contains either a converted value or a conversion error message.

  • convertToPresentation() receives a value that originates from the business object.

    • This method returns the converted value directly. It’s assumed that the business object contains only valid values.

Example: Implementing a String to Integer converter.

class MyConverter
        implements Converter<String, Integer> {
    @Override
    public Result<Integer> convertToModel(
            String fieldValue, ValueContext context) {
        // Produces a converted value or an error
        try {
            // ok is a static helper method that
            // creates a Result
            return Result.ok(Integer.valueOf(
                    fieldValue));
        } catch (NumberFormatException e) {
            // error is a static helper method
            // that creates a Result
            return Result.error("Enter a number");
        }
    }

    @Override
    public String convertToPresentation(
            Integer integer, ValueContext context) {
        // Converting to the field type should
        // always succeed, so there is no support for
        // returning an error Result.
        return String.valueOf(integer);
    }
}

// Using the converter
binder.forField(yearOfBirthField)
  .withConverter(new MyConverter())
  .bind(Person::getYearOfBirth, Person::setYearOfBirth);
  • The provided ValueContext can be used to find the Locale to be used for the conversion.

Binding Automatic Validation Upon Changes in Component Validation Status

When a component such as Date Picker (or any other component that accepts a formatted text as input) is used as an optional field, what happens if the user provides some invalid value for it and tries to save the form? Since the provided value can’t be parsed correctly, a null is provided to the binder and, as the field is optional, the binder doesn’t complain and the validation status would be true. This behavior can create the illusion for the user that they were able to save an invalid value. Thus, there must be a way to prevent the form submission until that invalid value is either cleared out or fixed, whether or not the field is required.

There might be different workarounds for such cases, but the preferred solution is the one that keeps Binder Validation Status as the single source of truth when it comes to checking the sanity of the data in the form. This is why the addValidationStatusChangeListener() method exists in the HasValidator interface.

Components that implement the HasValidator interface and override the default implementation of addValidationStatusChangeListener() to fire the ValidationStatusChangeEvent always benefits automatically from an up-to-date validation status of their associated binding. This is because the associated binding instance (upon creation) registers itself for changes in the component’s validation status and revalidate itself accordingly.

The following code snippet shows how a component can enable the binding instance to subscribe itself to the ValidationStatusChangeEvent:

@Tag("date-picker-demo")
public class DatePickerDemo implements HasValidator<LocalDate> /*, HasValue<...>*/ {

    // Each web component has a way to communicate its validation status
    // to its server-side component instance. The following `clientSideValid`
    // state is introduced here only for the sake of simplicity of this code
    // snippet:
    private boolean clientSideValid = true;

     /**
      * Note how <code>clientSideValid</code> engaged in the definition
      * of this method. It's important to reflect this status either
      * in the returning validation result of this method or any other
      * validation that's associated with this component.
      */
     @Override
     public Validator getDefaultValidator() { 1
          return (value, valueContext) -> clientSideValid ? ValidationResult.ok()
                  : ValidationResult.error("Invalid date format");
     }

     private final Collection<ValidationStatusChangeListener<LocalDate>>
         validationStatusListeners = new ArrayList<>();

     /**
      * This enables the binding to subscribe for the validation status
      * change events that are fired by this component and revalidate
      * itself respectively.
      */
     @Override
     public Registration addValidationStatusChangeListener(
             ValidationStatusChangeListener<LocalDate> listener) {
         validationStatusListeners.add(listener);
         return () -> validationStatusListeners.remove(listener);
     }

     protected void fireValidationStatusChangeEvent(
             boolean newValidationStatus) {
         if (this.clientSideValid != newValidationStatus) {
             this.clientSideValid = newValidationStatus;
             var event = new ValidationStatusChangeEvent<>(this,
                     newValidationStatus);
             validationStatusListeners.forEach(
                     listener -> listener.validationStatusChanged(event));
         }
     }
 }
  1. The validator instance returned by getDefaultValidator() gets called every time the binding instance validates/revalidates (as a part of the validator chain of the binding).

For a complete implementation see, for example, the DatePicker source code.

E3EBE8A9-74B7-4D31-A071-F65EB28119A5