Many years ago (June 2016 to be exact), Binder
was designed as one of the main new features for Vaadin 8.0. One of the central ideas then was to do all the tricky things related to conversion and validation only once in Binder so that individual field component implementations would only have to be concerned with providing a value through HasValue
and nothing else.
Since then, we have introduced field components with more sophisticated client-side restrictions that also need to be verified through server-side logic for security reasons. This means that part of the validation is configured through the component while Binder
is still aware of the validation status through getDefaultValidator()
.
It seems inevitable that some aspects of validation have to be specific to the type of the field component which currently means that you are forced to split up your validation logic into two separate places: some in the field configuration and some in the binder configuration. The only practical way of avoiding this duality seems to be to move validation out of the binder and into the fields so that all validation could again be defined in the same place - the field. This RFC investigates all the things that may be affected by this.
The basic idea is thus that all validation rules would be defined for the field rather than for a Binder
binding.
IntegerField participants = new IntegerField();
participants.setMin(1, "At least one participant is required");
participants.addValidator(value -> value != 13, "Can't have bad luck");
participants.setRequired("Must define the number of participants");
This structure means that each field implementation would have to be concerned with validation even if the field doesn’t have any inherent validation needs. This is probably not a big problem in practice since there’s already so many other details needed for a proper HasValue implementation that you anyways have to use e.g. AbstractField
or CustomField
.
This seemingly simple change does, however, have some knock-on effects that lead to trade-offs that need to be considered before we can choose a definite strategy.
Validating converted values
One central idea behind the current Binder
design is that validators and converters can be mixed into an arbitrary chain of processing logic. This allows expressing validations rules in terms of domain-specific data types rather than only validating the basic value from the field itself. It doesn’t seem feasible to introduce conversion as a part of a field’s state since you would then have to make a distinction between the basic TextField<String>
and a converted TextField<MyType>
.
Viable options include:
- Keep the current structure where a
Binder
binding is a chain of validators and converters while recommending users to define validation on the field whenever possible. - Remove (or deprecate) validator support from
Binder
bindings and instead only support a single converter that might in turn be a chain of simpler converters and validators. We could then also add a shorthand API for creating a binding with a single converter:binder.bind(someField, someConverter.withValidator(convertedValueValidator).chain(anotherConverter), getter, setter)
. - Remove (or deprecate) both validator and converter support from
Binder
and instead introduce a concept for creating a convertedHasValue
from a field component.ConvertedField<MyType> converted = myField.converted(someConverter)
. This field could then have additional validators added and be used directly withBinder
:binder.bind(converted, getter, setter);
Bean validation
Bean validation (JSR 380) is fundamentally working on a bean level which means that Binder would still on some level also deal with validation. The current implementation applies property-level validators for the appropriate binding and it could in the future either keep doing the same or configure the field by adding a validator to the field. This would also give the opportunity that different field implementations could be aware of common validation annotations so that e.g. a @Min(1)
annotation on a bean property would not only apply the corresponding server-side validation logic but also automatically apply field.setMin(1)
so that the field’s UI would reflect the constraint. It might seem problematic to update the state of a field instance as a side effect from creating a binding but that’s pattern is already used e.g. when binding.asRequired()
delegates to field.setRequiredIndicatorVisible(true)
.
Cross-field validation
The concept for cross-field validation would not change but it might become easier for users to understand: put the validator on the field on which the error message should be shown and then add a change listener to the other field to trigger the first field’s validators to run again even though the field’s own value has not changed. No such method exists today but would have to be added.
I18n objects
Form fields are currently (as of Vaadin 24.5) using i18n objects to configure error messages for the field’s own validation logic. This causes a redundancy in combination with the possibility of defining an error message as e.g. field.setMin(1, "Error message")
. The i18n object might be shared between multiple field instances of the same type which means that a method like setMin
would have to keep track of the error message separately rather than writing it to the current i18n object (if any).
In the short term, we could just let a message configured through a validation method override the corresponding message from the i18n object. This effectively means that the i18n object only defines default values.
In the long run, we should probably move away from i18n objects but instead make the components configure themselves based on getTranslation
. This would also require a way of overriding the translation key that a specific field instance should use for a specific error condition.
Anything else?
Are these all the aspects that would be impacted by making validation a feature of field components instead of a feature of form binding? Would the approaches considered here work or are there additional things that need to be taken into account?