I do not know if such a super-generic Field is possible, and if so I’d love to hear about it too.
I don’t like to write so much duplicate code either, so I created a helper class that is able to create TextFields with appropriate converters, ComboBoxes, CheckBoxes… Here are example codes for some of them:
This solotion is not on the same level of genericness as you wanted it to be in your question, but it’s still one step better than rewriting the code over and over again.
The substitue for CustomField is one of AbstractField, AbstractCompositeField or AbstractSinglePropertyField depending on the exact details. For some reason, it’s missing from the migration documentation. I’ve now created a PR for adding it there.
For your case, AbstractCompositeField is probably the most appropriate type. You can set the composition root of the field as either a single internal field or a layout to which you can add several inner field instances.
You can implement conversion as part of the field component by reading the value(s) from the internal field component(s) and then doing the conversion before passing a converted value to setModelValue. Conversion in the other direction can be done in setPresentationValue by converting before passing the original value to the internal field component(s). You can also implement validation internal to the field by updating the field’s internal representation (e.g. doing innerField.setErrorMessage(someMessage)) instead of calling setModelValue.
In some cases, it might still be be preferable to do accept some code duplication so that validation and conversion happens through the form’s main Binder instance. The benefit of this approach is that error messages and the overall form status can be handled in the same way regardless of whether the problem originates from a regular standalone field or one of your custom field implementations. This aspect is exactly the same as it has been in Vaadin 8. It does, however, differ from the situation in Vaadin 7 and older where generic validation and conversion was built-in for each field implementation and FieldGroup only queried the status and converted value from each field.
public class Email {
private String emailaddress;
public Email() {
}
public Email(String emailaddress) {
this.emailaddress = emailaddress;
}
public String getEmailaddress() {
return emailaddress;
}
public void setEmailaddress(String emailaddress) {
this.emailaddress = emailaddress;
}
}
My AbstractCompositeField implementation looks like this:
public class EmailField extends AbstractCompositeField<TextField, EmailField, Email> {
private static final long serialVersionUID = -9136522340661184652L;
private static final String LABEL = "E-Mail";
public EmailField(Email defaultValue) {
super(defaultValue);
//This has no effect on the actual label?
getContent().setLabel(LABEL);
addValueChangeListener(event -> setModelValue(event.getValue(), true));
/**
* Adding the ValuechangeListener directly to the TextField does not
* work either. getContent().addValueChangeListener(event ->
* setModelValue(new Email(event.getValue()), true));
*/
}
public EmailField() {
super(null);
}
@Override
protected void setPresentationValue(Email newPresentationValue) {
if (newPresentationValue == null)
getContent().clear();
else
getContent().setValue(newPresentationValue.getEmailaddress());
}
}
Using the field:
Binder<Patient> binder = new Binder<>();
EmailField emailField = new EmailField();
binder.forField(emailField).withValidator(new EmailValidator())
.bind(Patient::getEmail, Patient::setEmail);
The TextField is shown on the UI, but there is no Label visible and no ValueChangeEvents get triggered when entering values into the field. I’m using: vaadin 12.0.3 / Java 1.8 / jetty
I would appreciate if the documentation were supplemented with an example on creating a field component that is directly based on another field, while converting / validating the value from that field.
You should use the second variant with getContent().addValueChangeListener(...) from the comment in the constructor so that the field’s value is set to a new email address instance when the text in the wrapped field is changed. The reason the label isn’t set and no changes are propagated is because that constructor is not called. If you change the zero-args constructor to call this(null) instead of super(null), then things will start working again.
The relevant parts of a working solution are thus:
public EmailField(Email defaultValue) {
super(defaultValue);
getContent().setLabel(LABEL);
getContent().addValueChangeListener(
event -> setModelValue(new Email(event.getValue()), true));
}
public EmailField() {
this(null);
}