Validating and Converting User Input

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 is 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 is followed by various configuration calls for the field, and bind is the final method of the configuration.

  • asRequired is used for mandatory fields:

    • 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 do not match.

Examples where this is useful include an application-specific type for a postal code that the user enters in a TextField, or requesting the user enter only integers in a TextField, or 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 will be run with the String value
    // of the field
    .withValidator(text -> text.length() == 4,
            "Doesn't look like a year")
    // Converter will only be run for strings
    // with 4 characters
    .withConverter(new StringToIntegerConverter(
            "Must enter a number"))
    // Validator will be 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 is 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 used for converting 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
        "Please enter a number")
    .bind(Person::getYearOfBirth,
            Person::setYearOfBirth);

Implementing the Converter Interface

There are two methods to implement in the Converter interface:

  • convertToModel receives a value that originates from the user.

    • The method returns a Result that either contains 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 is assumed that the business object only contains 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.