Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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<Bean> attachBinderToField() {
return attachBinderToField(false);
}
Expand All @@ -140,4 +190,18 @@ private Binder<Bean> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bean> attachBinderToField() {
return attachBinderToField(false);
}
Expand All @@ -138,4 +184,21 @@ private Binder<Bean> 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);
}
}