Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds skipDefaultValidator API #18549

Merged
merged 13 commits into from Feb 2, 2024
114 changes: 109 additions & 5 deletions flow-data/src/main/java/com/vaadin/flow/data/binder/Binder.java
Expand Up @@ -255,6 +255,31 @@ default BindingValidationStatus<TARGET> validate() {
*/
public boolean isValidatorsDisabled();

/**
* Sets up this binding to either disable or enable the default field
* validators (e.g. min/max validators in DatePicker). This
* binding-level setting will override the Binder-level setting for this
* property.
*
* Defaults to {@literal false}.
*
* @param defaultValidatorsDisabled
* {@literal true} to disable default validators for this
* binding, {@literal false} to enable them, {@literal null}
* to reset (fallback to Binder-level setting)
* @see Binder#setDefaultValidatorsDisabled(boolean) for faster way to
* disable default validators for all bound fields.
*/
void setDefaultValidatorsDisabled(boolean defaultValidatorsDisabled);

/**
* Returns if default validators of bound field are disabled..
*
* @return {@literal true} if default validators are disabled for this
* binding, {@literal false} if they are enabled
*/
boolean isDefaultValidatorsDisabled();

/**
* Define whether the value should be converted back to the presentation
* in the field when a converter is used in binding.
Expand Down Expand Up @@ -824,6 +849,25 @@ default BindingBuilder<BEAN, TARGET> withStatusLabel(HasText label) {
BindingBuilder<BEAN, TARGET> withValidationStatusHandler(
BindingValidationStatusHandler handler);

/**
* Sets up this binding to either disable or enable the default field
* validators (e.g. min/max validators in DatePicker). This
* binding-level setting will override the Binder-level setting for this
* property.
*
* Defaults to {@literal false}.
*
* @param defaultValidatorsDisabled
* {@literal true} to disable default validators for this
* binding, {@literal false} to enable them, {@literal null}
* to reset (fallback to Binder-level setting)
* @return this binding, for chaining
* @see Binder#setDefaultValidatorsDisabled(boolean) for faster way to
* disable default validators for all bound fields.
*/
BindingBuilder<BEAN, TARGET> setDefaultValidatorsDisabled(
boolean defaultValidatorsDisabled);

/**
* Sets the field to be required. This means two things:
* <ol>
Expand Down Expand Up @@ -938,6 +982,8 @@ protected static class BindingBuilderImpl<BEAN, FIELDVALUE, TARGET>

private boolean asRequiredSet;

private Boolean defaultValidatorsDisabled;

/**
* Creates a new binding builder associated with the given field.
* Initializes the builder with the given converter chain and status
Expand All @@ -960,6 +1006,14 @@ protected BindingBuilderImpl(Binder<BEAN> binder,
this.binder = binder;
this.converterValidatorChain = converterValidatorChain;
this.statusHandler = statusHandler;

if (field instanceof HasValidator hasValidator) {
withValidator(
(val, ctx) -> binding.isDefaultValidatorsDisabled()
? ValidationResult.ok()
: hasValidator.getDefaultValidator().apply(val,
ctx));
}
}

Converter<FIELDVALUE, ?> getConverterValidatorChain() {
Expand Down Expand Up @@ -1103,6 +1157,14 @@ public BindingBuilder<BEAN, TARGET> withValidationStatusHandler(
return this;
}

@Override
public BindingBuilder<BEAN, TARGET> setDefaultValidatorsDisabled(
boolean defaultValidatorsDisabled) {
checkUnbound();
this.defaultValidatorsDisabled = defaultValidatorsDisabled;
return this;
}

@Override
public BindingBuilder<BEAN, TARGET> asRequired(
ErrorMessageProvider errorMessageProvider) {
Expand Down Expand Up @@ -1238,6 +1300,8 @@ protected static class BindingImpl<BEAN, FIELDVALUE, TARGET>

private Registration onValidationStatusChange;

private Boolean defaultValidatorsDisabled;

public BindingImpl(BindingBuilderImpl<BEAN, FIELDVALUE, TARGET> builder,
ValueProvider<BEAN, TARGET> getter,
Setter<BEAN, TARGET> setter) {
Expand All @@ -1247,6 +1311,8 @@ public BindingImpl(BindingBuilderImpl<BEAN, FIELDVALUE, TARGET> builder,
this.asRequiredSet = builder.asRequiredSet;
converterValidatorChain = ((Converter<FIELDVALUE, TARGET>) builder.converterValidatorChain);

defaultValidatorsDisabled = builder.defaultValidatorsDisabled;

onValueChange = getField().addValueChangeListener(
event -> handleFieldValueChange(event));

Expand Down Expand Up @@ -1566,6 +1632,18 @@ public boolean isValidatorsDisabled() {
return validatorsDisabled;
}

@Override
public void setDefaultValidatorsDisabled(
boolean defaultValidatorsDisabled) {
this.defaultValidatorsDisabled = defaultValidatorsDisabled;
}

@Override
public boolean isDefaultValidatorsDisabled() {
return Optional.ofNullable(defaultValidatorsDisabled)
.orElse(getBinder().isDefaultValidatorsDisabled());
}

@Override
public void setConvertBackToPresentation(
boolean convertBackToPresentation) {
Expand Down Expand Up @@ -1724,6 +1802,8 @@ void setIdentity() {

private boolean fieldsValidationStatusChangeListenerEnabled = true;

private boolean defaultValidatorsDisabled;

/**
* Creates a binder using a custom {@link PropertySet} implementation for
* finding and resolving property names for
Expand Down Expand Up @@ -1877,12 +1957,8 @@ public <FIELDVALUE> BindingBuilder<BEAN, FIELDVALUE> forField(
// clear previous errors for this field and any bean level validation
clearError(field);
getStatusLabel().ifPresent(label -> label.setText(""));

return createBinding(field, createNullRepresentationAdapter(field),
this::handleValidationStatus)
.withValidator(field instanceof HasValidator
? ((HasValidator) field).getDefaultValidator()
: Validator.alwaysPass());
this::handleValidationStatus);
}

/**
Expand Down Expand Up @@ -2709,6 +2785,34 @@ private List<ValidationResult> validateBean(BEAN bean) {
Collections::unmodifiableList));
}

/**
* Sets up the Binder to either disable or enable the default field
* validators (e.g. min/max validators in DatePicker) of all bound fields.
* This Binder-level setting can be overridden for each binding via either
* the binding object itself, or the binding builder.
* <p>
* Defaults to {@literal false}.
*
* @param defaultValidatorsDisabled
* {@literal true} to disable default validators of bound fields,
* {@literal false} to enable them
*/
public void setDefaultValidatorsDisabled(
boolean defaultValidatorsDisabled) {
this.defaultValidatorsDisabled = defaultValidatorsDisabled;
}

/**
* Returns the Binder-level setting for disabling default validators of
* bound fields.
*
* @return {@literal true} if default validators of bound fields are
* disabled, {@literal false} if they are enabled
*/
public boolean isDefaultValidatorsDisabled() {
return defaultValidatorsDisabled;
}

/**
* Sets the label to show the binder level validation errors not related to
* any specific field.
Expand Down
117 changes: 115 additions & 2 deletions flow-data/src/test/java/com/vaadin/flow/data/binder/BinderTest.java
Expand Up @@ -51,7 +51,6 @@
import com.vaadin.flow.data.validator.IntegerRangeValidator;
import com.vaadin.flow.data.validator.NotEmptyValidator;
import com.vaadin.flow.data.validator.StringLengthValidator;
import com.vaadin.flow.function.ValueProvider;
import com.vaadin.flow.internal.CurrentInstance;
import com.vaadin.flow.tests.data.bean.Person;
import com.vaadin.flow.tests.data.bean.Sex;
Expand Down Expand Up @@ -1542,6 +1541,120 @@ public void two_asRequired_fields_without_initial_values_readBean() {
isEmptyString());
}

@Test
public void disableDefaultValidators_binderLevel_enabledAndDisabled() {
TestTextFieldDefaultValidator field1 = new TestTextFieldDefaultValidator(
(val, ctx) -> ValidationResult.error("fail_1"));
TestTextFieldDefaultValidator field2 = new TestTextFieldDefaultValidator(
(val, ctx) -> ValidationResult.error("fail_2"));

binder.forField(field1).bind(Person::getFirstName,
Person::setFirstName);
binder.forField(field2).bind(Person::getLastName, Person::setLastName);

// Default behavior -> validators enabled
BinderValidationStatus<Person> status = binder.validate();
assertEquals(
"Validation should have two errors. "
+ "Default validators should be run.",
2, status.getValidationErrors().size());

// Toggle default validators to disabled
binder.setDefaultValidatorsDisabled(true);
status = binder.validate();
Assert.assertTrue(
"Validation should not have errors. "
+ "Default validator should be skipped.",
status.getValidationErrors().isEmpty());
}

@Test
public void disableDefaultValidator_testSingleBindingLevelDisabled_whenBinderLevelEnabled() {
TestTextFieldDefaultValidator field1 = new TestTextFieldDefaultValidator(
(val, ctx) -> ValidationResult.error("fail_1"));
TestTextFieldDefaultValidator field2 = new TestTextFieldDefaultValidator(
(val, ctx) -> ValidationResult.error("fail_2"));

binder.forField(field1).bind(Person::getFirstName,
Person::setFirstName);
Binding<Person, String> binding = binder.forField(field2)
.setDefaultValidatorsDisabled(true)
.bind(Person::getLastName, Person::setLastName);

// One binding has default validators disabled
BinderValidationStatus<Person> status = binder.validate();
assertEquals(
"Validation should have one error. "
+ "Only one default validators should be skipped.",
1, status.getValidationErrors().size());

// Re-enable default validators of binding
binding.setDefaultValidatorsDisabled(false);
status = binder.validate();
assertEquals(
"Validation should have two errors. "
+ "Default validators should be run.",
2, status.getValidationErrors().size());
}

@Test
public void skipDefaultValidator_crossToggleSingleBindingAndBinderState() {
AtomicBoolean nonSkippedDidRun = new AtomicBoolean(false);

TestTextFieldDefaultValidator field1 = new TestTextFieldDefaultValidator(
(val, ctx) -> ValidationResult.error("fail_1"));
TestTextFieldDefaultValidator field2 = new TestTextFieldDefaultValidator(
(val, ctx) -> {
nonSkippedDidRun.getAndSet(true);
return ValidationResult.error("fail_2");
});

binder.forField(field1).bind(Person::getFirstName,
Person::setFirstName);
Binding<Person, String> binding = binder.forField(field2)
.setDefaultValidatorsDisabled(false)
.bind(Person::getLastName, Person::setLastName);

// Initial case: binder disabled & binding overrides to enable
binder.setDefaultValidatorsDisabled(true);
BinderValidationStatus<Person> status = binder.validate();
assertEquals(
"Validation should have one error. "
+ "Only one default validators should be skipped.",
1, status.getValidationErrors().size());
assertTrue("Non-skipped validator should have been run.",
nonSkippedDidRun.get());

nonSkippedDidRun.getAndSet(false);

// Cross-toggle false <-> true
binder.setDefaultValidatorsDisabled(false);
binding.setDefaultValidatorsDisabled(true);
status = binder.validate();
assertEquals(
"Validation should have one error. "
+ "Only one default validators should be skipped.",
1, status.getValidationErrors().size());
assertFalse("Now skipped validator should not have been run.",
nonSkippedDidRun.get());
}

public class TestTextFieldDefaultValidator extends TestTextField
implements HasValidator<String> {

private final Validator<String> defaultValidator;

public TestTextFieldDefaultValidator(
Validator<String> defaultValidator) {
this.defaultValidator = defaultValidator;
}

@Override
public Validator<String> getDefaultValidator() {
return defaultValidator;
}
}

@Test
public void refreshValueFromBean() {
Binding<Person, String> binding = binder.bind(nameField,
Expand Down Expand Up @@ -1932,7 +2045,7 @@ public void validationShouldNotRunTwice() {
// Without fix for #12356 count will be 5
assertEquals(3, count.get());

assertEquals(new Double(2000), item.getSalaryDouble());
assertEquals(Double.valueOf(2000), item.getSalaryDouble());
}

@Test
Expand Down