diff --git a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datepicker/validation/BasicValidationPage.java b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datepicker/validation/BasicValidationPage.java index 1db6f4ad5f9..02467a91c70 100644 --- a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datepicker/validation/BasicValidationPage.java +++ b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datepicker/validation/BasicValidationPage.java @@ -36,6 +36,12 @@ public BasicValidationPage() { } protected DatePicker createTestField() { - return new DatePicker(); + return new DatePicker() { + @Override + protected void validate() { + super.validate(); + incrementServerValidationCounter(); + } + }; } } diff --git a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datepicker/validation/BinderValidationPage.java b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datepicker/validation/BinderValidationPage.java index f11e95190ff..6c2d2429b99 100644 --- a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datepicker/validation/BinderValidationPage.java +++ b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/main/java/com/vaadin/flow/component/datepicker/validation/BinderValidationPage.java @@ -13,6 +13,7 @@ public class BinderValidationPage extends AbstractValidationPage { public static final String MAX_INPUT = "max-input"; public static final String CLEAR_VALUE_BUTTON = "clear-value-button"; public static final String EXPECTED_VALUE_INPUT = "expected-value-input"; + public static final String RESET_BEAN_BUTTON = "reset-bean-button"; public static final String REQUIRED_ERROR_MESSAGE = "The field is required"; public static final String UNEXPECTED_VALUE_ERROR_MESSAGE = "The field doesn't match the expected value"; @@ -41,6 +42,9 @@ public BinderValidationPage() { .withValidator(value -> value.equals(expectedValue), UNEXPECTED_VALUE_ERROR_MESSAGE) .bind("property"); + binder.addStatusChangeListener(event -> { + incrementServerValidationCounter(); + }); add(createInput(EXPECTED_VALUE_INPUT, "Set expected date", event -> { LocalDate value = LocalDate.parse(event.getValue()); @@ -60,6 +64,10 @@ public BinderValidationPage() { add(createButton(CLEAR_VALUE_BUTTON, "Clear value", event -> { testField.clear(); })); + + add(createButton(RESET_BEAN_BUTTON, "Reset bean", event -> { + binder.setBean(new Bean()); + })); } protected DatePicker createTestField() { diff --git a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datepicker/validation/BasicValidationIT.java b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datepicker/validation/BasicValidationIT.java index 547b9069a5b..ef43191a9af 100644 --- a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datepicker/validation/BasicValidationIT.java +++ b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datepicker/validation/BasicValidationIT.java @@ -23,6 +23,7 @@ public void fieldIsInitiallyValid() { @Test public void triggerBlur_assertValidity() { testField.sendKeys(Keys.TAB); + assertValidationCount(0); assertServerValid(); assertClientValid(); } @@ -32,8 +33,9 @@ public void required_triggerBlur_assertValidity() { $("button").id(REQUIRED_BUTTON).click(); testField.sendKeys(Keys.TAB); - assertServerInvalid(); - assertClientInvalid(); + assertValidationCount(0); + assertServerValid(); + assertClientValid(); } @Test @@ -41,10 +43,14 @@ public void required_changeValue_assertValidity() { $("button").id(REQUIRED_BUTTON).click(); testField.setInputValue("1/1/2022"); + assertValidationCount(1); assertServerValid(); assertClientValid(); + resetValidationCount(); + testField.setInputValue(""); + assertValidationCount(1); assertServerInvalid(); assertClientInvalid(); } @@ -54,14 +60,28 @@ public void min_changeValue_assertValidity() { $("input").id(MIN_INPUT).sendKeys("2022-03-01", Keys.ENTER); testField.setInputValue("2/1/2022"); + assertValidationCount(1); assertClientInvalid(); assertServerInvalid(); + resetValidationCount(); + testField.setInputValue("3/1/2022"); + assertValidationCount(1); assertClientValid(); assertServerValid(); + resetValidationCount(); + testField.setInputValue("4/1/2022"); + assertValidationCount(1); + assertClientValid(); + assertServerValid(); + + resetValidationCount(); + + testField.setInputValue(""); + assertValidationCount(1); assertClientValid(); assertServerValid(); } @@ -71,14 +91,28 @@ public void max_changeValue_assertValidity() { $("input").id(MAX_INPUT).sendKeys("2022-03-01", Keys.ENTER); testField.setInputValue("4/1/2022"); + assertValidationCount(1); assertClientInvalid(); assertServerInvalid(); + resetValidationCount(); + testField.setInputValue("3/1/2022"); + assertValidationCount(1); assertClientValid(); assertServerValid(); + resetValidationCount(); + testField.setInputValue("2/1/2022"); + assertValidationCount(1); + assertClientValid(); + assertServerValid(); + + resetValidationCount(); + + testField.setInputValue(""); + assertValidationCount(1); assertClientValid(); assertServerValid(); } @@ -86,16 +120,30 @@ public void max_changeValue_assertValidity() { @Test public void badInput_changeValue_assertValidity() { testField.setInputValue("INVALID"); + assertValidationCount(1); assertServerInvalid(); assertClientInvalid(); + resetValidationCount(); + testField.setInputValue("1/1/2022"); + assertValidationCount(1); assertServerValid(); assertClientValid(); + resetValidationCount(); + testField.setInputValue("INVALID"); + assertValidationCount(1); assertServerInvalid(); assertClientInvalid(); + + resetValidationCount(); + + testField.setInputValue(""); + assertValidationCount(1); + assertClientValid(); + assertServerValid(); } @Test @@ -112,10 +160,14 @@ public void setValue_clearValue_assertValidity() { @Test public void badInput_setValue_clearValue_assertValidity() { testField.setInputValue("INVALID"); + assertValidationCount(1); assertServerInvalid(); assertClientInvalid(); + resetValidationCount(); + $("button").id(CLEAR_VALUE_BUTTON).click(); + assertValidationCount(1); assertServerValid(); assertClientValid(); } @@ -124,7 +176,8 @@ public void badInput_setValue_clearValue_assertValidity() { public void detach_attach_preservesInvalidState() { // Make field invalid $("button").id(REQUIRED_BUTTON).click(); - testField.sendKeys(Keys.TAB); + testField.setInputValue("1/1/2022"); + testField.setInputValue(""); detachAndReattachField(); @@ -145,7 +198,8 @@ public void webComponentCanNotModifyInvalidState() { public void clientSideInvalidStateIsNotPropagatedToServer() { // Make the field invalid $("button").id(REQUIRED_BUTTON).click(); - testField.sendKeys(Keys.TAB); + testField.setInputValue("1/1/2022"); + testField.setInputValue(""); executeScript("arguments[0].invalid = false", testField); diff --git a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datepicker/validation/BinderValidationIT.java b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datepicker/validation/BinderValidationIT.java index 6186fd6896e..dbb5d386843 100644 --- a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datepicker/validation/BinderValidationIT.java +++ b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow-integration-tests/src/test/java/com/vaadin/flow/component/datepicker/validation/BinderValidationIT.java @@ -13,6 +13,7 @@ import static com.vaadin.flow.component.datepicker.validation.BinderValidationPage.REQUIRED_ERROR_MESSAGE; import static com.vaadin.flow.component.datepicker.validation.BinderValidationPage.UNEXPECTED_VALUE_ERROR_MESSAGE; import static com.vaadin.flow.component.datepicker.validation.BinderValidationPage.CLEAR_VALUE_BUTTON; +import static com.vaadin.flow.component.datepicker.validation.BinderValidationPage.RESET_BEAN_BUTTON; @TestPath("vaadin-date-picker/validation/binder") public class BinderValidationIT @@ -27,9 +28,9 @@ public void fieldIsInitiallyValid() { @Test public void required_triggerBlur_assertValidity() { testField.sendKeys(Keys.TAB); - assertServerInvalid(); - assertClientInvalid(); - assertErrorMessage(REQUIRED_ERROR_MESSAGE); + assertValidationCount(0); + assertServerValid(); + assertClientValid(); } @Test @@ -37,15 +38,32 @@ public void required_changeValue_assertValidity() { $("input").id(EXPECTED_VALUE_INPUT).sendKeys("2022-01-01", Keys.ENTER); testField.setInputValue("1/1/2022"); + assertValidationCount(1); assertServerValid(); assertClientValid(); + resetValidationCount(); + testField.setInputValue(""); + assertValidationCount(1); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(REQUIRED_ERROR_MESSAGE); } + @Test + public void required_setValue_resetBean_assertValidity() { + $("input").id(EXPECTED_VALUE_INPUT).sendKeys("2022-01-01", Keys.ENTER); + + testField.setInputValue("1/1/2022"); + assertServerValid(); + assertClientValid(); + + $("button").id(RESET_BEAN_BUTTON).click(); + assertServerValid(); + assertClientValid(); + } + @Test public void min_changeValue_assertValidity() { $("input").id(MIN_INPUT).sendKeys("2022-03-01", Keys.ENTER); @@ -53,20 +71,36 @@ public void min_changeValue_assertValidity() { // Constraint validation fails: testField.setInputValue("2/1/2022"); + assertValidationCount(1); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(""); + resetValidationCount(); + // Binder validation fails: testField.setInputValue("3/1/2022"); + assertValidationCount(1); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(UNEXPECTED_VALUE_ERROR_MESSAGE); + resetValidationCount(); + // Both validations pass: testField.setInputValue("4/1/2022"); + assertValidationCount(1); assertClientValid(); assertServerValid(); + + resetValidationCount(); + + // Binder validation fails: + testField.setInputValue(""); + assertValidationCount(1); + assertClientInvalid(); + assertServerInvalid(); + assertErrorMessage(REQUIRED_ERROR_MESSAGE); } @Test @@ -76,20 +110,36 @@ public void max_changeValue_assertValidity() { // Constraint validation fails: testField.setInputValue("4/1/2022"); + assertValidationCount(1); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(""); + resetValidationCount(); + // Binder validation fails: testField.setInputValue("3/1/2022"); + assertValidationCount(1); assertClientInvalid(); assertServerInvalid(); assertErrorMessage(UNEXPECTED_VALUE_ERROR_MESSAGE); + resetValidationCount(); + // Both validations pass: testField.setInputValue("2/1/2022"); + assertValidationCount(1); assertClientValid(); assertServerValid(); + + resetValidationCount(); + + // Binder validation fails: + testField.setInputValue(""); + assertValidationCount(1); + assertClientInvalid(); + assertServerInvalid(); + assertErrorMessage(REQUIRED_ERROR_MESSAGE); } @Test @@ -97,18 +147,33 @@ public void badInput_changeValue_assertValidity() { $("input").id(EXPECTED_VALUE_INPUT).sendKeys("2022-01-01", Keys.ENTER); testField.setInputValue("INVALID"); + assertValidationCount(1); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(""); + resetValidationCount(); + testField.setInputValue("1/1/2022"); + assertValidationCount(1); assertServerValid(); assertClientValid(); + resetValidationCount(); + testField.setInputValue("INVALID"); + assertValidationCount(1); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(""); + + resetValidationCount(); + + testField.setInputValue(""); + assertValidationCount(1); + assertClientInvalid(); + assertServerInvalid(); + assertErrorMessage(REQUIRED_ERROR_MESSAGE); } @Test @@ -128,11 +193,15 @@ public void setValue_clearValue_assertValidity() { @Test public void badInput_setValue_clearValue_assertValidity() { testField.setInputValue("INVALID"); + assertValidationCount(1); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(""); + resetValidationCount(); + $("button").id(CLEAR_VALUE_BUTTON).click(); + assertValidationCount(1); assertServerInvalid(); assertClientInvalid(); assertErrorMessage(REQUIRED_ERROR_MESSAGE); diff --git a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java index 5e7b2ed96a9..b1bcf120c73 100644 --- a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java +++ b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/main/java/com/vaadin/flow/component/datepicker/DatePicker.java @@ -23,6 +23,7 @@ import java.util.Locale; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -115,6 +116,8 @@ public class DatePicker private boolean manualValidationEnabled = false; + private final CopyOnWriteArrayList> validationStatusChangeListeners = new CopyOnWriteArrayList<>(); + /** * Default constructor. */ @@ -166,7 +169,10 @@ private DatePicker(LocalDate initialDate, boolean isInitialValueOptional) { addValueChangeListener(e -> validate()); - addClientValidatedEventListener(e -> validate()); + getElement().addEventListener("unparsable-change", event -> { + validate(); + fireValidationStatusChangeEvent(); + }); getElement().addPropertyChangeListener("opened", event -> fireEvent( new OpenedChangeEvent(this, event.isUserOriginated()))); @@ -518,10 +524,21 @@ public Validator getDefaultValidator() { @Override public Registration addValidationStatusChangeListener( ValidationStatusChangeListener listener) { - return addClientValidatedEventListener( - event -> listener.validationStatusChanged( - new ValidationStatusChangeEvent(this, - !isInvalid()))); + return Registration.addAndRemove(validationStatusChangeListeners, + listener); + } + + /** + * Notifies Binder that it needs to revalidate the component since the + * component's validity state may have changed. Note, there is no need to + * notify Binder separately in the case of a ValueChangeEvent, as Binder + * already listens to this event and revalidates automatically. + */ + private void fireValidationStatusChangeEvent() { + ValidationStatusChangeEvent event = new ValidationStatusChangeEvent<>( + this, !isInvalid()); + validationStatusChangeListeners + .forEach(listener -> listener.validationStatusChanged(event)); } private ValidationResult checkValidity(LocalDate value) { @@ -596,7 +613,8 @@ public void setValue(LocalDate value) { // `executeJs` can end up invoked after a non-empty value is set. getElement() .executeJs("if (!this.value) this._inputElementValue = ''"); - fireEvent(new ClientValidatedEvent(this, false)); + validate(); + fireValidationStatusChangeEvent(); } } diff --git a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/validation/BasicValidationTest.java b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/validation/BasicValidationTest.java index 37b6d327b9c..7c0fba7c487 100644 --- a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/validation/BasicValidationTest.java +++ b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/validation/BasicValidationTest.java @@ -17,11 +17,25 @@ import java.time.LocalDate; +import org.junit.Test; + import com.vaadin.flow.component.datepicker.DatePicker; import com.vaadin.tests.validation.AbstractBasicValidationTest; public class BasicValidationTest extends AbstractBasicValidationTest { + @Test + public void addValidationStatusChangeListener_addAnotherListenerOnInvocation_noExceptions() { + testField.addValidationStatusChangeListener(event1 -> { + testField.addValidationStatusChangeListener(event2 -> { + }); + }); + + // Trigger ValidationStatusChangeEvent + testField.getElement().setProperty("_hasInputValue", true); + testField.clear(); + } + protected DatePicker createTestField() { return new DatePicker(); }