Binder conditional validation

Hi,

I’m trying to implement a conditional validation using the unbuffered mode of binder. I have a TextField (Citzen Card) that is evaluated based on a specific value selected from Combobox (country). The binder seems not validate if there is an ValidationError in a component binding to the binder.
In my example bellow I have the value PT that must be greater that 5 and the ES that must be greater than 6. If I provoke the error for PT and change to ES the validator remain using the PT value. The binder seems not write the value when has errors. I need to write/validate even if the binder has errors. How can I avoid this scenario?

@Route("")
@PWA(name = "Project Base for Vaadin Flow", shortName = "Project Base")
public class MainView extends VerticalLayout {

 public class Person {
  private String name;
  private String surname;
  private String country;
  private String citzenCard;


  public String getName() {
   return name;
  }

  public void setName(String name) {
   this.name = name;
  }

  public String getSurname() {
   return surname;
  }

  public void setSurname(String surname) {
   this.surname = surname;
  }

  public String getCountry() {
   return country;
  }

  public void setCountry(String country) {
   this.country = country;
  }

  public String getCitzenCard() {
   return citzenCard;
  }

  public void setCitzenCard(String citzenCard) {
   this.citzenCard = citzenCard;
  }

 }

 private TextField name;
 private TextField surname;
 private TextField citzenCard;
 private ComboBox < String > country;

 private Binder < Person > binder;

 public MainView() {
  FormLayout formLayout = new FormLayout();
  formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("300px", 1, FormLayout.ResponsiveStep.LabelsPosition.ASIDE));
  add(formLayout);

  name = new TextField();
  formLayout.addFormItem(name, createRequiredIndicator("Name:"));

  surname = new TextField();
  formLayout.addFormItem(surname, createRequiredIndicator("Surname:"));

  country = new ComboBox < > ();
  country.setItems("PT", "ES");
  formLayout.addFormItem(country, createRequiredIndicator("Country:"));

  citzenCard = new TextField();
  formLayout.addFormItem(citzenCard, "Citzen Card Number:");

  binder = new Binder < > ();
  binder.forField(name).asRequired().bind(Person::getName, Person::setName);
  binder.forField(surname).asRequired().bind(Person::getSurname, Person::setSurname);
  binder.forField(country).asRequired().bind(Person::getCountry, Person::setCountry);
  Binder.Binding < Person, String > citzenCardBinder = binder.forField(citzenCard).withValidator((s, valueContext) -> {
   if ("PT".equals(binder.getBean().country) && s.length() < 6) {
    return ValidationResult.error("Citzen Card must be greater than 5 characters!");
   } else if ("ES".equals(binder.getBean().country) && s.length() < 7) {
    return ValidationResult.error("Citzen Card must be greater than 6 characters!");
   }

   return ValidationResult.ok();
  }).bind(Person::getCitzenCard, Person::setCitzenCard);

  country.addValueChangeListener(e -> citzenCardBinder.validate());

  binder.setBean(new Person());
 }

 private Div createRequiredIndicator(String text) {
  return new Div(new Label(Optional.ofNullable(text).orElse("")), createRequiredIndicatorIcon());
 }

 private static Icon createRequiredIndicatorIcon() {
  Icon requiredIndicator = VaadinIcon.ASTERISK.create();
  requiredIndicator.getStyle().set("height", "10px").set("color", "red");
  return requiredIndicator;
 }
}

Hi,

You can force validation of your citizenfield when country is changed (and validate citizen with country field value):

 binder = new Binder<>();
 binder.forField(name).asRequired().bind(Person::getName,Person::setName);
 binder.forField(surname).asRequired().bind(Person::getSurname,Person::setSurname);
 binder.forField(country).asRequired().bind(Person::getCountry,Person::setCountry);
 Binding<Person, String> citzenCardBinder = binder.forField(citzenCard).withValidator((s, valueContext) -> {
     if("PT".equals(country.getValue()) && s.length()<6){
         return ValidationResult.error("Citzen Card must be greater than 5 characters!");
     }else if ("ES".equals(country.getValue()) && s.length()<7){
         return ValidationResult.error("Citzen Card must be greater than 6 characters!");
     }
 
     return ValidationResult.ok();
 }).bind(Person::getCitzenCard,Person::setCitzenCard);
 country.addValueChangeListener(e -> citzenCardBinder.validate());
  binder.setBean(new Person());

Jean-Christophe Gueriaud:
Hi,

You can force validation of your citizenfield when country is changed (and validate citizen with country field value):

 binder = new Binder<>();
 binder.forField(name).asRequired().bind(Person::getName,Person::setName);
 binder.forField(surname).asRequired().bind(Person::getSurname,Person::setSurname);
 binder.forField(country).asRequired().bind(Person::getCountry,Person::setCountry);
 Binding<Person, String> citzenCardBinder = binder.forField(citzenCard).withValidator((s, valueContext) -> {
     if("PT".equals(country.getValue()) && s.length()<6){
         return ValidationResult.error("Citzen Card must be greater than 5 characters!");
     }else if ("ES".equals(country.getValue()) && s.length()<7){
         return ValidationResult.error("Citzen Card must be greater than 6 characters!");
     }
 
     return ValidationResult.ok();
 }).bind(Person::getCitzenCard,Person::setCitzenCard);
 country.addValueChangeListener(e -> citzenCardBinder.validate());
  binder.setBean(new Person());

These solves the validation problem but not the problem with the write bean. If you provoke a ValidationError in another field the binder doesn’t write the value to the Person instance. After that you could actually change the value of combobox for PT/ES because the bean state remains and the validation show same error over and over.

If one field is invalid then the bean is not updated.

So to validate the citizenField you can’t use bean.getCountry(). You can use country.getValue() in the citizenField validator or you can use a bean validator.

Here the code:

 
@Route("view2")
public class MainView2 extends VerticalLayout {

    public class Person {
        private String name;
        private String surname;
        private String country;
        private String citzenCard;


        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getSurname() {
            return surname;
        }

        public void setSurname(String surname) {
            this.surname = surname;
        }

        public String getCountry() {
            return country;
        }

        public void setCountry(String country) {
            this.country = country;
        }

        public String getCitzenCard() {
            return citzenCard;
        }

        public void setCitzenCard(String citzenCard) {
            this.citzenCard = citzenCard;
        }

    }

    private TextField name;
    private TextField surname;
    private TextField citzenCard;
    private ComboBox< String > country;

    private Binder< Person > binder;

    public MainView2() {
        FormLayout formLayout = new FormLayout();
        formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("300px", 1, FormLayout.ResponsiveStep.LabelsPosition.ASIDE));
        add(formLayout);

        name = new TextField();
        formLayout.addFormItem(name, createRequiredIndicator("Name:"));

        surname = new TextField();
        formLayout.addFormItem(surname, createRequiredIndicator("Surname:"));

        country = new ComboBox < > ();
        country.setItems("PT", "ES");
        formLayout.addFormItem(country, createRequiredIndicator("Country:"));

        citzenCard = new TextField();
        formLayout.addFormItem(citzenCard, "Citzen Card Number:");

        binder = new Binder < > ();
        binder.forField(name).asRequired().bind(Person::getName, Person::setName);
        binder.forField(surname).asRequired().bind(Person::getSurname, Person::setSurname);
        binder.forField(country).asRequired().bind(Person::getCountry, Person::setCountry);
        Binder.Binding < Person, String > citzenCardBinder = binder.forField(citzenCard).withValidator((s, valueContext) -> {
            if ("PT".equals(country.getValue()) && s.length() < 6) {
                return ValidationResult.error("Citzen Card must be greater than 5 characters!");
            } else if ("ES".equals(country.getValue()) && s.length() < 7) {
                return ValidationResult.error("Citzen Card must be greater than 6 characters!");
            }

            return ValidationResult.ok();
        }).bind(Person::getCitzenCard, Person::setCitzenCard);

        /* or use this instead ciizen validator
        binder.withValidator((tempPerson, valueContext) -> {
            detailDiv.setText(tempPerson.toString());
            if("PT".equals(tempPerson.getCountry()) && tempPerson.getCitzenCard().length()<6){
                return ValidationResult.error("Citzen Card must be greater than 5 characters!");
            }else if ("ES".equals(tempPerson.getCountry()) && tempPerson.getCitzenCard().length()<7){
                return ValidationResult.error("Citzen Card must be greater than 6 characters!");
            }

         */

        country.addValueChangeListener(e -> citzenCardBinder.validate());

        binder.setBean(new Person());
    }

    private Div createRequiredIndicator(String text) {
        return new Div(new Label(Optional.ofNullable(text).orElse("")), createRequiredIndicatorIcon());
    }

    private static Icon createRequiredIndicatorIcon() {
        Icon requiredIndicator = VaadinIcon.ASTERISK.create();
        requiredIndicator.getStyle().set("height", "10px").set("color", "red");
        return requiredIndicator;
    }
}

Perhaps you need to write invalid data inside the bean for other purposes.

FormLayout sucks
and also Vertical and Horizontal Layout

does not expand content, label always at top, never aside.