Validating & 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 used in business objects to types used in bound UI components — and vice versa.
These concepts go together because validation can be based on a converted value, and because the ability to convert a value is also somewhat a validation method.
Vaadin Flow includes several validators and converters that can be implemented.
Validating User Input
It’s common for applications to restrict the kind of value the user is allowed to enter into certain fields. This section explains how to define validators, as well as how to configure them and how your application will respond to validation results.
Define 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.
This example defines 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 is displayed, and, if the user leaves the field empty, an error message is displayed.
Custom Validation Error Messages
You can customize the way error messages are displayed by defining a ValidationStatusHandler
. Or you can do so by configuring the Label
for each binding.
The label is used to show the status of the field. It can be used for validation errors, as well as confirmation and helper messages.
The example below shows how to configure 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. It’s also more flexible than using the status label approach.
Multiple Validators
You can add multiple validators for the same binding. For example, below shows how to define 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 do this by saving the binding to a local variable and triggering revalidation when the other field fires a value-change event.
The example stores 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());
Temporarily By-Passing Validation
Validators can be by-passed temporarily on both binder-level and binding-level. In practice, this allows bean writing to succeed even when one or more validators would not pass.
// Disables all validators, both binder-level and binding-level
binder.setValidatorsDisabled(true);
// Disables validators for a single binding
binding.setValidatorsDisabled(true);
Convert User Input
You can bind application data to a UI field component, even if the types don’t match. This might be useful when an application-specific type is used for a postal code that the user enters in a TextField
. It might also help in requesting that the user enter only integers in a TextField
, or to select enumeration values in a Checkbox
field.
This section explains how to define converters, add multiple converters and how to respond to user input to include custom error messages.
Define 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.
The example below 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);
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.
In the example here, you can see a validator and a 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, the best practice is to use a validator to check the content 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; they may not be suitable for end-users. The example here defines 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. First, 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.
Second, 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.
Implementing a String to Integer converter is shown in the example here:
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.
Bind Validation on Changes
It’s possible to bind automatic validation upon changes in component validation status. You should consider 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. Since 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 or fixed — regardless of whether the field is required.
There might be workarounds for such cases, but the preferred solution is the one that keeps Binder Validation Status as the single source of truth regarding 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 benefit 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 revalidates 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));
}
}
}
-
The validator instance returned by
getDefaultValidator()
gets called every time the binding instance validates or revalidates as part of the validator chain of the binding.
For a complete implementation example, see the DatePicker
source code.
Disabling Default Validators
The default validators of all fields are executed automatically. This applies to fields that implement the HasValidator
interface and override its getDefaultValidator()
method to provide a validator.
Certain cases may require preventing execution of the default validator of one or more field components. This might be needed so that the developer can instead provide custom validation. It can be done for individual bindings, as well as all bindings of a binder at once. It can also be changed after the bindings have been built:
// Disable all field-level default validators
binder.setDefaultValidatorsEnabled(false);
// Disable default validator of a binding's field when building the binding
binder.forField(yearOfBirthField)
.withDefaultValidator(false)
.bind(Person::getYearOfBirth,
Person::setYearOfBirth);
// Disable default validator of a binding's field after the binding has been built
binding.setDefaultValidatorEnabled(false)
The binding-level setting overrides the binder-level setting. The binding-level setting can also be set to null
so as to revert to adhering to the binder-level setting.
E3EBE8A9-74B7-4D31-A071-F65EB28119A5