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

fix: fix setBean single binding write and add tests #18891

Merged
merged 4 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
60 changes: 17 additions & 43 deletions flow-data/src/main/java/com/vaadin/flow/data/binder/Binder.java
Original file line number Diff line number Diff line change
Expand Up @@ -1916,10 +1916,15 @@ public static <BEAN> Binder<BEAN> withPropertySet(
}

/**
* Informs the Binder that a value in Binding was changed. This method will
* trigger validating and writing of the whole bean if using
* {@link #setBean(Object)}. If using {@link #readBean(Object)} only the
* field validation for the given Binding is run.
* Informs the Binder that a value in Binding was changed.
*
* If {@link #readBean(Object)} was used, this method will only validate the
* changed binding and ignore state of other bindings.
*
* If {@link #setBean(Object)} was used, all pending changed bindings will
* be validated and non-changed ones will be ignored. The changed value will
* be written to the bean immediately, assuming that Binder-level validators
* also pass.
*
* @param binding
* the binding whose value has been changed
Expand All @@ -1930,20 +1935,7 @@ protected void handleFieldValueChange(Binding<BEAN, ?> binding) {
if (getBean() == null) {
binding.validate();
} else {
BinderValidationStatus<BEAN> status = validateBindingsAndBean();
if (status.isOk()) {
doWriteIfValid(getBean(), changedBindings);
} else {
// Fire status change for changed bindings only
getValidationStatusHandler()
.statusChange(new BinderValidationStatus<>(this,
status.getFieldValidationStatuses().stream()
.filter(s -> changedBindings
.contains(s.getBinding()))
.collect(Collectors.toList()),
status.getBeanValidationErrors()));
fireStatusChangeEvent(status.hasErrors());
}
doWriteIfValid(getBean(), changedBindings);
}
}

Expand Down Expand Up @@ -2229,9 +2221,13 @@ public <FIELDVALUE> Binding<BEAN, FIELDVALUE> bindReadOnly(
* back to their corresponding property values of the bean as long as the
* bean is bound.
* <p>
* Any change made in one of the fields also runs validation for all the
* fields {@link Binding} and bean level validation for this binder (bean
* level validators are added using {@link Binder#withValidator(Validator)}.
* Note: Any change made in one of the bound fields runs validation for only
* the changed {@link Binding}, and additionally any bean level validation
* for this binder (bean level validators are added using
* {@link Binder#withValidator(Validator)}. As a result, the bean set via
* this method is not guaranteed to always be in a valid state. If bean
* validity is required at all times, {@link #readBean(Object)} and
* {@link #writeBean(Object)} should be used instead.
tepi marked this conversation as resolved.
Show resolved Hide resolved
* <p>
* After updating each field, the value is read back from the field and the
* bean's property value is updated if it has been changed from the original
Expand Down Expand Up @@ -2801,28 +2797,6 @@ protected BinderValidationStatus<BEAN> validate(boolean fireEvent) {
return validationStatus;
}

/**
* Runs validation for all bindings to determine binder's validity state. If
* a bean has been set and all bindings pass validation, bean-level
* validations are run as well.
*
* @return BinderValidationStatus for the validation run
*/
private BinderValidationStatus<BEAN> validateBindingsAndBean() {
List<BindingValidationStatus<?>> bindingStatuses = validateBindings();
boolean bindingsInError = bindingStatuses.stream()
.anyMatch(BindingValidationStatus::isError);

List<ValidationResult> beanStatuses = new ArrayList<>();
// Only execute bean-level validation when binding validators pass
if (!bindingsInError) {
beanStatuses.addAll(validateBean(getBean()));
}

return new BinderValidationStatus<>(this, bindingStatuses,
beanStatuses);
}

/**
* Runs all currently configured field level validators, as well as all bean
* level validators if a bean is currently set with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
* Note: if there are any field level validation errors, the bean level
* validation is not run.
* <p>
* Note: if the status change is triggered via automatic validation due to a
* changed field value, the field validation statuses will only cover the
* changed fields.
* <p>
* Use {@link Binder#setValidationStatusHandler(BinderValidationStatusHandler)}
* to handle form level validation status changes.
*
Expand Down
tepi marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -2230,6 +2230,62 @@ public void setBean_readOnlyBinding_accessorsBiding_valueIsNotUpdated() {
Assert.assertSame(val, bean.getVals());
}

@Test
public void setBean_oneBindingValidationFails_otherBindingValueShouldBeSet() {
binder.forField(nameField).withValidator(
(value, context) -> ValidationResult.error("Always fails"))
.bind(Person::getFirstName, Person::setFirstName);
binder.forField(ageField)
.withConverter(new StringToIntegerConverter(""))
.bind(Person::getAge, Person::setAge);

binder.setBean(item);

ageField.setValue("15");

assertEquals(
"Age should have been set regardless of invalid name field.",
15, item.getAge());
}

@Test
public void setBean_oneBindingRequiredButEmpty_otherBindingValueShouldBeSet() {
item.setFirstName(null);

binder.forField(nameField).asRequired("Name is required")
.bind(Person::getFirstName, Person::setFirstName);
binder.forField(ageField)
.withConverter(new StringToIntegerConverter(""))
.bind(Person::getAge, Person::setAge);

binder.setBean(item);

ageField.setValue("15");

assertEquals(
"Age should have been set regardless of required but empty name field.",
15, item.getAge());
}

@Test
public void setBean_binderValidationFails_noValueShouldBeSet() {
binder.forField(nameField).bind(Person::getFirstName,
Person::setFirstName);
binder.forField(ageField)
.withConverter(new StringToIntegerConverter(""))
.bind(Person::getAge, Person::setAge);

binder.withValidator(
(value, context) -> ValidationResult.error("Always fails"));
binder.setBean(item);

ageField.setValue("15");

assertEquals(
"Age should not have been set since binder validation fails.",
32, item.getAge());
}

@Test
public void invalidUsage_modifyFieldsInsideValidator_binderDoesNotThrow() {
TestTextField field = new TestTextField();
Expand Down
Loading