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: theforField
call starts the process, it is followed by various configuration calls for the field, andbind
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 theLocale
to be used for the conversion.
32256556-8BAD-4559-B343-ED82E8D5142D