Skip to content

Commit

Permalink
Adds skipDefaultValidator API (#18549)
Browse files Browse the repository at this point in the history
* Adds skipDefaultValidator API

* Simplify API

* Add tests

* Rename method

* Change API to allow toggling skip default validators for Binder and Binding

* Cleanup

* API naming changes; Add tests for dynamic disable/enable

* Fix javadoc error

* Change plural validators to validator for binding

* Formatting
  • Loading branch information
tepi authored and rodolforfq committed Feb 7, 2024
1 parent b75b481 commit 3bbefea
Show file tree
Hide file tree
Showing 2 changed files with 220 additions and 7 deletions.
110 changes: 105 additions & 5 deletions flow-data/src/main/java/com/vaadin/flow/data/binder/Binder.java
Expand Up @@ -255,6 +255,30 @@ default BindingValidationStatus<TARGET> validate() {
*/
public boolean isValidatorsDisabled();

/**
* Sets up this binding to either disable or enable the default field
* validator (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 defaultValidatorDisabled
* {@literal true} to disable default validator for this
* binding, {@literal false} to enable it, {@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 setDefaultValidatorDisabled(boolean defaultValidatorDisabled);

/**
* Returns if default validator of bound field is disabled.
*
* @return {@literal true} if default validator is disabled for this
* binding, {@literal false} if it is enabled
*/
boolean isDefaultValidatorDisabled();

/**
* 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 +848,24 @@ 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
* validator (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 defaultValidatorDisabled
* {@literal true} to disable default validator for this
* binding, {@literal false} to enable it, {@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> setDefaultValidatorDisabled(
boolean defaultValidatorDisabled);

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

private boolean asRequiredSet;

private Boolean defaultValidatorDisabled;

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

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

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

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

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

private Registration onValidationStatusChange;

private Boolean defaultValidatorDisabled;

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

defaultValidatorDisabled = builder.defaultValidatorDisabled;

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

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

@Override
public void setDefaultValidatorDisabled(
boolean defaultValidatorDisabled) {
this.defaultValidatorDisabled = defaultValidatorDisabled;
}

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

@Override
public void setConvertBackToPresentation(
boolean convertBackToPresentation) {
Expand Down Expand Up @@ -1724,6 +1798,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 +1953,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 +2781,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)
.setDefaultValidatorDisabled(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.setDefaultValidatorDisabled(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)
.setDefaultValidatorDisabled(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.setDefaultValidatorDisabled(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

0 comments on commit 3bbefea

Please sign in to comment.