Validating and Converting User Input

Binder supports checking the validity of the user’s input and converting the values between the type used in business objects and the bound UI components. These two concepts go hand in hand since validation can be based on a converted value, and being able to convert a value is a kind of validation.

Validation

An application typically has some restrictions on exactly what kinds of values the user is allowed to enter into different fields. Binder lets us define validators for each field that we are binding. The validator is by default run whenever the user changes the value of a field, and the validation status is also checked again when saving.

Validators for a field are defined between the forField and bind steps when a binding is created. A validator can be defined using Validator instance or inline using a 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,
    "Full 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);
Note
Binder.forField works like a builder where forField starts the process, is followed by various configuration calls for the field and bind acts as the finalizing method which applies the configuration.

The validation state of each field is updated whenever the user modifies the value of that field.

To customize the way a binder displays error messages is to configure each binding to use its own Label that is used to show the status for each field.

Note
The status label is not only used for validation errors but also for showing confirmation and helper messages.
Label emailStatus = new Label();

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,
    "Full name must contain at least three characters")
  // Define how the validation status is displayed
  .withValidationStatusHandler(status -> {
      nameStatus.setText(status.getMessage().orElse(""));
      setVisible(nameStatus, status.isError());
    })
  // Finalize the binding
  .bind(Person::getName, Person::setName);

It is possible to add multiple validators for the same binding. The following example will first validate that the entered text looks like an email address, and only for seemingly valid email addresses it will continue checking that the email address is 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);

In some cases, the validation of one field depends on the value of some other field. We can save the binding to a local variable and trigger a revalidation when another field fires a value change event.

Binder<Trip> binder = new Binder<>();
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.BindingBuilder<Trip, LocalDate> returnBindingBuilder = binder
        .forField(returning).withValidator(
                returnDate -> !returnDate
                .isBefore(departing.getValue()),
                "Cannot return before departing");
Binder.Binding<Trip, LocalDate> returnBinder = returnBindingBuilder
        .bind(Trip::getReturnDate, Trip::setReturnDate);

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

Conversion

You can also bind application data to a UI field component even though the types do not match. In some cases, there might be types specific for the application, such as custom type that encapsulates a postal code that the user enters through a TextField. Another quite typical case is for entering integer numbers using a TextField or enumeration values (Checkbox is used as an example). Similarly to validators, we can define a converter using Converter instance or inline using lambda expressions. We can optionally specify also an error message.

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

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

// Checkbox for gender
Checkbox genderField = new Checkbox("Gender");

binder.forField(genderField)
.withConverter(gender -> gender ? Gender.FEMALE : Gender.MALE,
        gender -> Gender.FEMALE.equals(gender))
        .bind(Person::getGender, Person::setGender);

Multiple validators and converters can be used for building one binding. Each validator or converter is used in the order they were defined for a value provided by the user. The value is passed along until a final converted value is stored in the business object, or until the first validation error or impossible conversion is encountered. When updating the UI components, values from the business object are passed through each converter in the reverse order without doing any validation.

Note
A converter can be used as a validator but for code clarity and to avoid boilerplate code, you should use a validator when checking the contents and a converter when modifying the value.
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);

You can define your own conversion either by using callbacks, typically lambda expressions or method references, or by implementing the Converter interface.

When using callbacks, there is one for converting in each direction. If the callback used for converting the user-provided value throws an unchecked exception, then the field will be marked as invalid and the message of the exception will be used as the validation error message. Messages in Java runtime exceptions are typically written with developers in mind and might not be suitable to show to end users. We can provide a custom error message that is used whenever the conversion throws an unchecked exception.

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);

There are two separate methods to implement in the Converter interface. convertToModel receives a value that originates from the user. The method should return a Result that either contains a converted value or a conversion error message. convertToPresentation receives a value that originates from the business object. Since it is assumed that the business object only contains valid values, this method directly returns the converted value.

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("Please 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 for finding Locale to be used for the conversion.