How to create a custom field based on simple POJO's

Hi,

I’m trying to implement custom fields for pojo’s with the following features:

  • define Validators and Converters only once in the custom field
  • Styling should look like standard Lumo fields
  • Api should be the same (or at least expose important methods) as standard Vaadin fields

So basically. I don’t want to write this code multiple times:

TextField field = new TextField("POJO");
field.setWidth("250px");

binder.forField(field).withConverter(new POJOConverter()).withValidator(new POJOValidator())
                .bind(POJO::getPOJO, POJO::setPOJO);
ComboBox<POJOEnum> comboBox = new ComboBox<>("POJOs");
comboBox.setWidth("5em");
comboBox.setItems(POJOEnum.values());
comboBox.setItemLabelGenerator(POJOEnum::getName);
comboBox.setValue(POJOEnum.FU);

binder.forField(comboBox).withValidator(new POJOEnumValidator()).bind(POJO::getPOJOEnum,POJO::setPOJOEnum);

Instead, I would rather want to do something like this:

POJOField pojoField = new POJOField();
binder.forField(pojoField).bind(POJO::getPOJO, POJO::setPOJO);

I looked at the Docs for [Creating Componets based of AbstractField, AbstractCompositeFiel and AbstractSinglePropertyField]
(https://vaadin.com/docs/v11/flow/binding-data/tutorial-flow-field.html) and also [Creating a Component Using Existing Components]
(https://vaadin.com/docs/v11/flow/creating-components/tutorial-component-composite.html), but only got solutions which fulfill only some of the listed features.

Example POJOs:

  • phone
  • money
  • security number
  • gender
  • title
  • email
  • address
  • etc…

Thanks in advance!

Johannes

anyone?

In vaadin 8 you could use Customfield, but in Vaadin 10 there is no substitute for Customfield. ([Vaadin 8 → Vaadin 10]
(https://vaadin.com/docs/v11/flow/migration/5-components.html))

anyone?

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:

public class FieldBinder {
    public static <T> TextField textField(String label, Binder<T> binder, ValueProvider<T, String> getter, Setter<T, String> setter){
        TextField textField = new TextField(label);
        textField.setWidth("100%");
        binder.forField(textField)
                .withNullRepresentation("")
                .bind(getter, setter);
        return textField;
    }

    public static <T> TextField textFieldDouble(String label, Binder<T> binder, ValueProvider<T, Double> getter, Setter<T, Double> setter){
        TextField textField = new TextField(label);
        textField.setWidth("100%");
        binder.forField(textField)
                .withNullRepresentation("-")
                .withConverter(new StringToDoubleConverter("-"))
                .bind(getter, setter);
        return textField;
    }
	public static <T> Checkbox checkBox(String label, Binder<T> binder, ValueProvider<T, Boolean> getter, Setter<T, Boolean> setter){
        Checkbox checkBox = new Checkbox(label);
        checkBox.setWidth("100%");
        binder.forField(checkBox)
                .bind(getter, setter);
        return checkBox;
    }
}

This makes it possible for easy creation of input fields:

layout.add(FieldBinder.textFieldDouble("Price", binder, POJO::getPrice, POJO::setPrice));

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.

Thanks Kaspar,

I’ll use that for now!

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.

Hi Leif,

i can’t get the most basic implementation to work. Maybe you can tell me what i’m missing.

I wanted to try out your solution and create a E-mail field based on AbstractCompositeField. I have read the section about AbstractCompositeField from the vaadin [docs]
(https://vaadin.com/docs/v12/flow/binding-data/tutorial-flow-field.html)

I have the following model class:

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.

regards

Johannes

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