From 0e492f0998fe3df96ee95bd2a888206bb4b1cecf Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Tue, 4 Nov 2025 18:19:45 +0200 Subject: [PATCH] fix: preserve value on unparsable input --- .../flow/component/datepicker/DatePicker.java | 10 ++- .../validation/BinderValidationTest.java | 64 +++++++++++++++++++ .../flow/component/timepicker/TimePicker.java | 10 ++- .../validation/BinderValidationTest.java | 63 ++++++++++++++++++ 4 files changed, 145 insertions(+), 2 deletions(-) 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 4fbb3794e6a..036434ec96e 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 @@ -809,13 +809,21 @@ protected void setModelValue(LocalDate newModelValue, boolean fromClient) { isFallbackParserRunning = false; } + // Case: User enters unparsable input in a field with valid input + if (fromClient && newModelValue == null && oldModelValue != null + && isInputUnparsable()) { + validate(); + fireValidationStatusChangeEvent(); + return; + } + boolean isModelValueRemainedEmpty = newModelValue == null && oldModelValue == null; // Cases: // - User modifies input but it remains unparsable // - User enters unparsable input in empty field - // - User clears unparsable input + // - User clears unparsable input (that was already empty) if (fromClient && isModelValueRemainedEmpty) { validate(); fireValidationStatusChangeEvent(); diff --git a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/validation/BinderValidationTest.java b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/validation/BinderValidationTest.java index 0175404edee..8276929195c 100644 --- a/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/validation/BinderValidationTest.java +++ b/vaadin-date-picker-flow-parent/vaadin-date-picker-flow/src/test/java/com/vaadin/flow/component/datepicker/validation/BinderValidationTest.java @@ -26,10 +26,14 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import com.vaadin.flow.component.Component; import com.vaadin.flow.component.datepicker.DatePicker; import com.vaadin.flow.data.binder.Binder; import com.vaadin.flow.data.binder.BindingValidationStatus; import com.vaadin.flow.data.binder.BindingValidationStatusHandler; +import com.vaadin.flow.dom.DomEvent; +import com.vaadin.flow.internal.JacksonUtils; +import com.vaadin.flow.internal.nodefeature.ElementListenerMap; public class BinderValidationTest { @@ -119,6 +123,52 @@ public void elementWithConstraints_validValue_validationPasses() { } + @Test + public void validValue_enterUnparsableInput_valueIsPreserved() { + var binder = attachBinderToField(); + var bean = new Bean(); + var validDate = LocalDate.of(2025, 1, 15); + bean.setDate(validDate); + binder.setBean(bean); + + // Simulate setting unparsable input + fakeClientPropertyChange(field, "_inputElementValue", "foobar"); + fakeClientPropertyChange(field, "value", null); + fakeClientDomEvent("change"); + + Assert.assertEquals( + "Field value should be preserved after entering unparsable input", + validDate, field.getValue()); + Assert.assertEquals( + "Binder value should be preserved after entering unparsable input", + validDate, bean.getDate()); + } + + @Test + public void validValue_enterUnparsableInput_clearInput_binderValueChangesToNull() { + var binder = attachBinderToField(); + var bean = new Bean(); + var validDate = LocalDate.of(2025, 1, 15); + bean.setDate(validDate); + binder.setBean(bean); + + // Simulate setting an invalid input + fakeClientPropertyChange(field, "_inputElementValue", "foobar"); + fakeClientPropertyChange(field, "value", null); + fakeClientDomEvent("change"); + + // Simulate setting clearing the invalid input + fakeClientPropertyChange(field, "_inputElementValue", ""); + fakeClientDomEvent("unparsable-change"); + + Assert.assertNull( + "Field value should be null after clearing unparsable input", + field.getValue()); + Assert.assertNull( + "Binder value should be null after clearing unparsable input", + bean.getDate()); + } + private Binder attachBinderToField() { return attachBinderToField(false); } @@ -140,4 +190,18 @@ private Binder attachBinderToField(boolean isRequired) { return binder; } + + private void fakeClientDomEvent(String eventName) { + var element = field.getElement(); + var event = new DomEvent(element, eventName, + JacksonUtils.createObjectNode()); + element.getNode().getFeature(ElementListenerMap.class).fireEvent(event); + } + + private void fakeClientPropertyChange(Component component, String property, + String value) { + var element = component.getElement(); + element.getStateProvider().setProperty(element.getNode(), property, + value, false); + } } diff --git a/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java b/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java index 9f87d261a5c..33388ac65c2 100644 --- a/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java +++ b/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/main/java/com/vaadin/flow/component/timepicker/TimePicker.java @@ -390,13 +390,21 @@ protected void setModelValue(LocalTime newModelValue, boolean fromClient) { unparsableValue = null; } + // Case: User enters unparsable input in a field with valid input + if (fromClient && newModelValue == null && oldModelValue != null + && isInputUnparsable()) { + validate(); + fireValidationStatusChangeEvent(); + return; + } + boolean isModelValueRemainedEmpty = newModelValue == null && oldModelValue == null; // Cases: // - User modifies input but it remains unparsable // - User enters unparsable input in empty field - // - User clears unparsable input + // - User clears unparsable input (that was already empty) if (fromClient && isModelValueRemainedEmpty) { validate(); fireValidationStatusChangeEvent(); diff --git a/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/test/java/com/vaadin/flow/component/timepicker/tests/validation/BinderValidationTest.java b/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/test/java/com/vaadin/flow/component/timepicker/tests/validation/BinderValidationTest.java index 3a43706eeaa..257c136ec26 100644 --- a/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/test/java/com/vaadin/flow/component/timepicker/tests/validation/BinderValidationTest.java +++ b/vaadin-time-picker-flow-parent/vaadin-time-picker-flow/src/test/java/com/vaadin/flow/component/timepicker/tests/validation/BinderValidationTest.java @@ -117,6 +117,52 @@ public void elementWithConstraints_validValue_validationPasses() { Assert.assertFalse(statusCaptor.getValue().isError()); } + @Test + public void validValue_enterUnparsableInput_valueIsPreserved() { + var binder = attachBinderToField(); + var bean = new Bean(); + var validTime = LocalTime.of(14, 30); + bean.setTime(validTime); + binder.setBean(bean); + + // Simulate setting unparsable input + fakeClientPropertyChange(field, "_inputElementValue", "foobar"); + fakeClientPropertyChange(field, "value", ""); + fakeClientDomEvent("change"); + + Assert.assertEquals( + "Field value should be preserved after entering unparsable input", + validTime, field.getValue()); + Assert.assertEquals( + "Binder value should be preserved after entering unparsable input", + validTime, bean.getTime()); + } + + @Test + public void validValue_enterUnparsableInput_clearInput_binderValueChangesToNull() { + var binder = attachBinderToField(); + var bean = new Bean(); + var validTime = LocalTime.of(14, 30); + bean.setTime(validTime); + binder.setBean(bean); + + // Simulate setting an invalid input + fakeClientPropertyChange(field, "_inputElementValue", "foobar"); + fakeClientPropertyChange(field, "value", ""); + fakeClientDomEvent("change"); + + // Simulate clearing the invalid input + fakeClientPropertyChange(field, "_inputElementValue", ""); + fakeClientDomEvent("unparsable-change"); + + Assert.assertNull( + "Field value should be null after clearing unparsable input", + field.getValue()); + Assert.assertNull( + "Binder value should be null after clearing unparsable input", + bean.getTime()); + } + private Binder attachBinderToField() { return attachBinderToField(false); } @@ -138,4 +184,21 @@ private Binder attachBinderToField(boolean isRequired) { return binder; } + + private void fakeClientDomEvent(String eventName) { + var element = field.getElement(); + var event = new com.vaadin.flow.dom.DomEvent(element, eventName, + com.vaadin.flow.internal.JacksonUtils.createObjectNode()); + element.getNode().getFeature( + com.vaadin.flow.internal.nodefeature.ElementListenerMap.class) + .fireEvent(event); + } + + private void fakeClientPropertyChange( + com.vaadin.flow.component.Component component, String property, + String value) { + var element = component.getElement(); + element.getStateProvider().setProperty(element.getNode(), property, + value, false); + } }