From 222908a9372885cc05bc3cb04374aea5aba66139 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Pekka=20Hyv=C3=B6nen?= Date: Fri, 26 Aug 2016 00:29:50 +0300 Subject: [PATCH] Add Form level status handler and status label This feature doesn't make a whole lot of sense until form level status changes are available. Change-Id: Ie634c4a6b3511b7cbf9e367192034934b0e0d4b0 --- .../datamodel/datamodel-forms.asciidoc | 6 +- .../src/main/java/com/vaadin/data/Binder.java | 166 ++++++++++++-- .../java/com/vaadin/data/BinderResult.java | 72 ++++++ .../com/vaadin/data/BinderStatusHandler.java | 46 ++++ .../src/main/java/com/vaadin/data/Result.java | 30 ++- .../java/com/vaadin/data/SimpleResult.java | 6 + .../vaadin/data/BinderBookOfVaadinTest.java | 128 ++++++++++- .../test/java/com/vaadin/data/BinderTest.java | 212 +++++++++++++++++- 8 files changed, 628 insertions(+), 38 deletions(-) create mode 100644 server/src/main/java/com/vaadin/data/BinderResult.java create mode 100644 server/src/main/java/com/vaadin/data/BinderStatusHandler.java diff --git a/documentation/datamodel/datamodel-forms.asciidoc b/documentation/datamodel/datamodel-forms.asciidoc index b86cf8f672..62fd9df38c 100644 --- a/documentation/datamodel/datamodel-forms.asciidoc +++ b/documentation/datamodel/datamodel-forms.asciidoc @@ -566,21 +566,21 @@ We can also define our own status handler to provide a custom way of handling st ---- BinderStatusHandler defaultHandler = binder.getStatusHandler(); -binder.setStatusHandler((List results) -> { +binder.setStatusHandler(results -> { String errorMessage = results.stream() // Ignore helper and confirmation messages .filter(BinderResult::isError) // Ignore messages that belong to a specific field .filter(error -> !error.getField().isPresent()) // Create a string out of the remaining messages - .map(BinderResult::getMessage) + .map(Result::getMessage).map(o -> o.get()) .collect(Collectors.joining("\n")); formStatusLabel.setValue(errorMessage); formStatusLabel.setVisible(!errorMessage.isEmpty()); // Let the default handler show messages for each field - defaultHandler.handleStatus(results); + defaultHandler.accept(event); }); ---- diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index cf7f788677..9150c046f6 100644 --- a/server/src/main/java/com/vaadin/data/Binder.java +++ b/server/src/main/java/com/vaadin/data/Binder.java @@ -17,6 +17,7 @@ package com.vaadin.data; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; @@ -505,22 +506,25 @@ public class Binder implements Serializable { @Override public Result validate() { - Result dataValue = getTargetValue(); - fireStatusChangeEvent(dataValue); - return dataValue; + BinderResult bindingResult = getTargetValue(); + getBinder().getStatusHandler().accept(Arrays.asList(bindingResult)); + return bindingResult; } /** - * Returns the field value run through all converters and validators. + * Returns the field value run through all converters and validators, + * but doesn't fire a {@link ValidationStatusChangeEvent status change + * event}. * * @return a result containing the validated and converted value or * describing an error */ - private Result getTargetValue() { + private BinderResult getTargetValue() { FIELDVALUE fieldValue = field.getValue(); Result dataValue = converterValidatorChain.convertToModel( fieldValue, ((AbstractComponent) field).getLocale()); - return dataValue; + return dataValue.biMap((value, message) -> new BinderResult<>(this, + value, message)); } private void unbind() { @@ -561,14 +565,15 @@ public class Binder implements Serializable { boolean runBeanLevelValidation) { assert bean != null; if (setter != null) { - getTargetValue().ifOk(value -> setBeanValue(bean, value)); + BinderResult validationResult = getTargetValue(); + getBinder().getStatusHandler() + .accept(Arrays.asList(validationResult)); + validationResult.ifOk(value -> setter.accept(bean, value)); } if (runBeanLevelValidation && !getBinder().bindings.stream() .map(BindingImpl::getTargetValue) .anyMatch(Result::isError)) { - List> errors = binder.validateItem(bean); - // TODO: Pass errors to Binder statusChangeHandler once that is - // available + binder.validateItem(bean); } } @@ -576,7 +581,7 @@ public class Binder implements Serializable { setter.accept(bean, value); } - private void fireStatusChangeEvent(Result result) { + private void fireStatusChangeEvent(Result result) { ValidationStatusChangeEvent event = new ValidationStatusChangeEvent( getField(), result.isError() ? ValidationStatus.ERROR @@ -632,6 +637,10 @@ public class Binder implements Serializable { private final List> validators = new ArrayList<>(); + private Label statusLabel; + + private BinderStatusHandler statusHandler; + /** * Returns an {@code Optional} of the bean that has been bound with * {@link #bind}, or an empty optional if a bean is not currently bound. @@ -909,6 +918,9 @@ public class Binder implements Serializable { * If all validators pass, the resulting list is empty. *

* Does not run bean validators. + *

+ * All results are passed to the {@link #getStatusHandler() status change + * handler.} * * @see #validateItem(Object) * @@ -916,13 +928,17 @@ public class Binder implements Serializable { * succeeded */ private List> validateBindings() { - List> resultErrors = new ArrayList<>(); + List> results = new ArrayList<>(); for (BindingImpl binding : bindings) { - binding.validate().ifError(errorMessage -> resultErrors - .add(new ValidationError<>(binding, - binding.getField().getValue(), errorMessage))); + results.add(binding.getTargetValue()); } - return resultErrors; + + getStatusHandler().accept(Collections.unmodifiableList(results)); + + return results.stream().filter(r -> r.isError()) + .map(r -> new ValidationError<>(r.getBinding().get(), + r.getField().get().getValue(), r.getMessage().get())) + .collect(Collectors.toList()); } /** @@ -941,12 +957,99 @@ public class Binder implements Serializable { */ private List> validateItem(BEAN bean) { Objects.requireNonNull(bean, "bean cannot be null"); - return validators.stream().map(validator -> validator.apply(bean)) + List> results = Collections.unmodifiableList( + validators.stream().map(validator -> validator.apply(bean)) + .map(dataValue -> dataValue.biMap( + (value, message) -> new BinderResult<>(null, + value, message))) + .collect(Collectors.toList())); + getStatusHandler().accept(results); + + return results.stream() .filter(Result::isError).map(res -> new ValidationError<>(this, bean, res.getMessage().get())) .collect(Collectors.toList()); } + /** + * Sets the label to show the binder level validation errors not related to + * any specific field. + *

+ * Only the one validation error message is shown in this label at a time. + *

+ * This is a convenience method for + * {@link #setStatusHandler(BinderStatusHandler)}, which means that this + * method cannot be used after the handler has been set. Also the handler + * cannot be set after this label has been set. + * + * @param statusLabel + * the status label to set + * @see #setStatusHandler(BinderStatusHandler) + * @see Binding#withStatusLabel(Label) + */ + public void setStatusLabel(Label statusLabel) { + if (statusHandler != null) { + throw new IllegalStateException("Cannot set status label if a " + + BinderStatusHandler.class.getSimpleName() + + " has already been set."); + } + this.statusLabel = statusLabel; + } + + /** + * Gets the status label or an empty optional if none has been set. + * + * @return the optional status label + * @see #setStatusLabel(Label) + */ + public Optional

+ * Setting this handler will override the default behavior, which is to let + * fields show their validation status messages and show binder level + * validation errors or OK status in the label set with + * {@link #setStatusLabel(Label)}. + *

+ * This handler cannot be set after the status label has been set with + * {@link #setStatusLabel(Label)}, or {@link #setStatusLabel(Label)} cannot + * be used after this handler has been set. + * + * @param statusHandler + * the status handler to set, not null + * @throws NullPointerException + * for null status handler + * @see #setStatusLabel(Label) + * @see Binding#withStatusChangeHandler(StatusChangeHandler) + */ + public void setStatusHandler(BinderStatusHandler statusHandler) { + Objects.requireNonNull(statusHandler, "Cannot set a null " + + BinderStatusHandler.class.getSimpleName()); + if (statusLabel != null) { + throw new IllegalStateException( + "Cannot set " + BinderStatusHandler.class.getSimpleName() + + " if a status label has already been set."); + } + this.statusHandler = statusHandler; + } + + /** + * Gets the status handler of this form. + *

+ * If none has been set with {@link #setStatusHandler(BinderStatusHandler)}, + * the default implementation is returned. + * + * @return the status handler used, never null + * @see #setStatusHandler(BinderStatusHandler) + */ + public BinderStatusHandler getStatusHandler() { + return Optional.ofNullable(statusHandler) + .orElse(this::defaultHandleBinderStatusChange); + } + /** * Creates a new binding with the given field. * @@ -1017,4 +1120,33 @@ public class Binder implements Serializable { } } + /** + * The default binder level status handler. + *

+ * Passes all field related results to the Binding status handlers. All + * other status changes are displayed in the status label, if one has been + * set with {@link #setStatusLabel(Label)}. + * + * @param results + * a list of validation results from binding and/or item level + * validators + */ + @SuppressWarnings("unchecked") + protected void defaultHandleBinderStatusChange( + List> results) { + // let field events go to binding status handlers + results.stream().filter(br -> br.getField().isPresent()) + .forEach(br -> ((BindingImpl) br.getBinding().get()) + .fireStatusChangeEvent(br)); + + // show first possible error or OK status in the label if set + if (getStatusLabel().isPresent()) { + String statusMessage = results.stream() + .filter(r -> !r.getField().isPresent()) + .map(Result::getMessage).map(m -> m.orElse("")).findFirst() + .orElse(""); + getStatusLabel().get().setValue(statusMessage); + } + } + } diff --git a/server/src/main/java/com/vaadin/data/BinderResult.java b/server/src/main/java/com/vaadin/data/BinderResult.java new file mode 100644 index 0000000000..52375b88ff --- /dev/null +++ b/server/src/main/java/com/vaadin/data/BinderResult.java @@ -0,0 +1,72 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data; + +import java.util.Optional; + +import com.vaadin.data.Binder.Binding; + +/** + * A result that keeps track of the possible binding (field) it belongs to. + * + * @param + * the value type of the field + * @param + * the result value type and the data type of the binding, matches + * the field type if a converter has not been set + */ +public class BinderResult extends SimpleResult { + + private final Binding binding; + + /** + * Creates a new binder result. + * + * @param binding + * the binding where the result originated, may be {@code null} + * @param value + * the resut value, can be null + * @param message + * the error message of the result, may be {@code null} + */ + public BinderResult(Binding binding, VALUE value, + String message) { + super(value, message); + this.binding = binding; + } + + /** + * Return the binding this result originated from, or an empty optional if + * none. + * + * @return the optional binding + */ + public Optional> getBinding() { + return Optional.ofNullable(binding); + } + + /** + * Return the field this result originated from, or an empty optional if + * none. + * + * @return the optional field + */ + public Optional> getField() { + return binding == null ? Optional.empty() + : Optional.ofNullable(binding.getField()); + } + +} \ No newline at end of file diff --git a/server/src/main/java/com/vaadin/data/BinderStatusHandler.java b/server/src/main/java/com/vaadin/data/BinderStatusHandler.java new file mode 100644 index 0000000000..4c516a5ed6 --- /dev/null +++ b/server/src/main/java/com/vaadin/data/BinderStatusHandler.java @@ -0,0 +1,46 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data; + +import java.io.Serializable; +import java.util.List; +import java.util.function.Consumer; + +import com.vaadin.data.Binder.Binding; + +/** + * Status change handler for forms. + *

+ * Register a handler using {@link Binder#setStatusHandler(BinderStatusHandler)} + * to be able to customize the status change handling such as displaying + * validation messages. + *

+ * The list will contain results for either binding level or binder level, but + * never both mixed. This is because binder level validation is not run if + * binding level validation fails. + * + * @see Binder#setStatusHandler(BinderStatusHandler) + * @see Binder#setStatusLabel(com.vaadin.ui.Label) + * @see Binding#withStatusChangeHandler(StatusChangeHandler) + * + * @author Vaadin Ltd + * @since 8.0 + * + */ +public interface BinderStatusHandler + extends Consumer>>, Serializable { + +} diff --git a/server/src/main/java/com/vaadin/data/Result.java b/server/src/main/java/com/vaadin/data/Result.java index b82b3ff4e7..5803eace11 100644 --- a/server/src/main/java/com/vaadin/data/Result.java +++ b/server/src/main/java/com/vaadin/data/Result.java @@ -19,6 +19,7 @@ package com.vaadin.data; import java.io.Serializable; import java.util.Objects; import java.util.Optional; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -103,7 +104,7 @@ public interface Result extends Serializable { * the mapping function * @return the mapped result */ - public default Result map(Function mapper) { + default Result map(Function mapper) { return flatMap(value -> ok(mapper.apply(value))); } @@ -119,7 +120,20 @@ public interface Result extends Serializable { * the mapping function * @return the mapped result */ - public Result flatMap(Function> mapper); + Result flatMap(Function> mapper); + + /** + * Applies the given function to this result, regardless if this is an error + * or not. Passes the value and the message to the given function as + * parameters. + * + * @param + * the type of the mapped value + * @param mapper + * the mapping function + * @return the mapped result + */ + S biMap(BiFunction mapper); /** * Invokes either the first callback or the second one, depending on whether @@ -130,7 +144,7 @@ public interface Result extends Serializable { * @param ifError * the function to call if failure */ - public void handle(Consumer ifOk, Consumer ifError); + void handle(Consumer ifOk, Consumer ifError); /** * Applies the {@code consumer} if result is not an error. @@ -138,7 +152,7 @@ public interface Result extends Serializable { * @param consumer * consumer to apply in case it's not an error */ - public default void ifOk(Consumer consumer) { + default void ifOk(Consumer consumer) { handle(consumer, error -> { }); } @@ -149,7 +163,7 @@ public interface Result extends Serializable { * @param consumer * consumer to apply in case it's an error */ - public default void ifError(Consumer consumer) { + default void ifError(Consumer consumer) { handle(value -> { }, consumer); } @@ -160,14 +174,14 @@ public interface Result extends Serializable { * @return true if the result denotes an error, * false otherwise */ - public boolean isError(); + boolean isError(); /** * Returns an Optional of the result message, or an empty Optional if none. * * @return the optional message */ - public Optional getMessage(); + Optional getMessage(); /** * Return the value, if the result denotes success, otherwise throw an @@ -182,6 +196,6 @@ public interface Result extends Serializable { * @throws X * if this result denotes an error */ - public R getOrThrow( + R getOrThrow( Function exceptionProvider) throws X; } diff --git a/server/src/main/java/com/vaadin/data/SimpleResult.java b/server/src/main/java/com/vaadin/data/SimpleResult.java index 935fb545e3..ceedcb5ae3 100644 --- a/server/src/main/java/com/vaadin/data/SimpleResult.java +++ b/server/src/main/java/com/vaadin/data/SimpleResult.java @@ -17,6 +17,7 @@ package com.vaadin.data; import java.util.Objects; import java.util.Optional; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; @@ -64,6 +65,11 @@ class SimpleResult implements Result { } } + @Override + public S biMap(BiFunction mapper) { + return mapper.apply(value, message); + } + @Override public void handle(Consumer ifOk, Consumer ifError) { Objects.requireNonNull(ifOk, "ifOk cannot be null"); diff --git a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java index c51d41d6a7..6861903132 100644 --- a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java +++ b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java @@ -20,6 +20,7 @@ import java.util.Date; import java.util.List; import java.util.Locale; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; import org.junit.Assert; import org.junit.Before; @@ -399,7 +400,7 @@ public class BinderBookOfVaadinTest { } @Test - public void withStatusChangeHandlerExample() { + public void withBindingStatusChangeHandlerExample() { Label nameStatus = new Label(); AtomicReference event = new AtomicReference<>(); @@ -555,4 +556,129 @@ public class BinderBookOfVaadinTest { binder.load(p); Assert.assertEquals("12500", yearOfBirthField.getValue()); } + + @Test + public void withBinderStatusLabelExample() { + Label formStatusLabel = new Label(); + + BeanBinder binder = new BeanBinder<>(BookPerson.class); + + binder.setStatusLabel(formStatusLabel); + + final String message = "Too young, son"; + final String message2 = "Y2K error"; + TextField yearOfBirth = new TextField(); + BookPerson p = new BookPerson(1500, 12); + binder.forField(yearOfBirth) + .withConverter(new StringToIntegerConverter("err")) + .bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth); + binder.withValidator(bean -> bean.yearOfBirth < 2000 ? Result.ok(bean) + : Result.error(message)) + .withValidator(bean -> bean.yearOfBirth == 2000 + ? Result.error(message2) : Result.ok(bean)); + + binder.bind(p); + + // first bean validator fails and passes error message to status label + yearOfBirth.setValue("2001"); + + List> errors = binder.validate(); + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(errors.get(0).getMessage(), message); + + Assert.assertEquals(message, formStatusLabel.getValue()); + + // value is correct, status label is cleared + yearOfBirth.setValue("1999"); + + errors = binder.validate(); + Assert.assertEquals(0, errors.size()); + + Assert.assertEquals("", formStatusLabel.getValue()); + + // both bean validators fail, should be two error messages chained + yearOfBirth.setValue("2000"); + + errors = binder.validate(); + Assert.assertEquals(2, errors.size()); + + // only first error is shown + Assert.assertEquals(message, formStatusLabel.getValue()); + } + + @Test + public void withBinderStatusChangeHandlerExample() { + Label formStatusLabel = new Label(); + + BinderStatusHandler defaultHandler = binder.getStatusHandler(); + + binder.setStatusHandler(results -> { + String errorMessage = results.stream() + // Ignore confirmation messages + .filter(BinderResult::isError) + // Ignore messages that belong to a specific field + .filter(error -> !error.getField().isPresent()) + // Create a string out of the remaining messages + .map(Result::getMessage).map(o -> o.get()) + .collect(Collectors.joining("\n")); + + formStatusLabel.setValue(errorMessage); + formStatusLabel.setVisible(!errorMessage.isEmpty()); + + // Let the default handler show messages for each field + defaultHandler.accept(results); + }); + + final String bindingMessage = "uneven"; + final String message = "Too young, son"; + final String message2 = "Y2K error"; + TextField yearOfBirth = new TextField(); + BookPerson p = new BookPerson(1500, 12); + binder.forField(yearOfBirth) + .withConverter(new StringToIntegerConverter("err")) + .withValidator(value -> value % 2 == 0 ? Result.ok(value) + : Result.error(bindingMessage)) + .bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth); + binder.withValidator(bean -> bean.yearOfBirth < 2000 ? Result.ok(bean) + : Result.error(message)) + .withValidator(bean -> bean.yearOfBirth == 2000 + ? Result.error(message2) : Result.ok(bean)); + + binder.bind(p); + + // first binding validation fails, no bean level validation is done + yearOfBirth.setValue("2001"); + List> errors = binder.validate(); + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(errors.get(0).getMessage(), bindingMessage); + + Assert.assertEquals("", formStatusLabel.getValue()); + + // first bean validator fails and passes error message to status label + yearOfBirth.setValue("2002"); + + errors = binder.validate(); + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(errors.get(0).getMessage(), message); + + Assert.assertEquals(message, formStatusLabel.getValue()); + + // value is correct, status label is cleared + yearOfBirth.setValue("1998"); + + errors = binder.validate(); + Assert.assertEquals(0, errors.size()); + + Assert.assertEquals("", formStatusLabel.getValue()); + + // both bean validators fail, should be two error messages chained + yearOfBirth.setValue("2000"); + + errors = binder.validate(); + Assert.assertEquals(2, errors.size()); + + Assert.assertEquals(message + "\n" + message2, + formStatusLabel.getValue()); + + } } diff --git a/server/src/test/java/com/vaadin/data/BinderTest.java b/server/src/test/java/com/vaadin/data/BinderTest.java index 4c59773b2f..e6b78eb6b3 100644 --- a/server/src/test/java/com/vaadin/data/BinderTest.java +++ b/server/src/test/java/com/vaadin/data/BinderTest.java @@ -577,7 +577,7 @@ public class BinderTest { Assert.assertNull(event.get()); event.set(evt); }); - binding.bind(Person::getFirstName, Person::setLastName); + binding.bind(Person::getFirstName, Person::setFirstName); nameField.setValue(""); @@ -610,7 +610,7 @@ public class BinderTest { Binding binding = binder.forField(nameField) .withValidator(notEmpty).withStatusChangeHandler(evt -> { }); - binding.bind(Person::getFirstName, Person::setLastName); + binding.bind(Person::getFirstName, Person::setFirstName); Assert.assertNull(nameField.getComponentError()); @@ -630,7 +630,7 @@ public class BinderTest { Binding binding = binder.forField(nameField) .withValidator(notEmpty).withStatusLabel(label); - binding.bind(Person::getFirstName, Person::setLastName); + binding.bind(Person::getFirstName, Person::setFirstName); nameField.setValue(""); @@ -657,7 +657,7 @@ public class BinderTest { Binding binding = binder.forField(nameField) .withValidator(notEmpty).withStatusLabel(label); - binding.bind(Person::getFirstName, Person::setLastName); + binding.bind(Person::getFirstName, Person::setFirstName); Assert.assertNull(nameField.getComponentError()); @@ -675,7 +675,7 @@ public class BinderTest { public void bindingWithStatusChangeHandler_addAfterBound() { Binding binding = binder.forField(nameField) .withValidator(notEmpty); - binding.bind(Person::getFirstName, Person::setLastName); + binding.bind(Person::getFirstName, Person::setFirstName); binding.withStatusChangeHandler(evt -> Assert.fail()); } @@ -686,7 +686,7 @@ public class BinderTest { Binding binding = binder.forField(nameField) .withValidator(notEmpty); - binding.bind(Person::getFirstName, Person::setLastName); + binding.bind(Person::getFirstName, Person::setFirstName); binding.withStatusLabel(label); } @@ -696,7 +696,6 @@ public class BinderTest { Label label = new Label(); Binding binding = binder.forField(nameField); - binding.bind(Person::getFirstName, Person::setLastName); binding.withStatusChangeHandler(event -> { }); @@ -709,7 +708,6 @@ public class BinderTest { Label label = new Label(); Binding binding = binder.forField(nameField); - binding.bind(Person::getFirstName, Person::setLastName); binding.withStatusLabel(label); @@ -721,7 +719,6 @@ public class BinderTest { public void bingingWithStatusChangeHandler_setAfterOtherHandler() { Binding binding = binder.forField(nameField); - binding.bind(Person::getFirstName, Person::setLastName); binding.withStatusChangeHandler(event -> { }); @@ -865,4 +862,201 @@ public class BinderTest { Assert.assertTrue(beanLevelValidationRun.get()); } + @Test + public void binderWithStatusChangeHandler_handlerGetsEvents() { + AtomicReference>> resultsCapture = new AtomicReference<>(); + binder.forField(nameField).withValidator(notEmpty) + .withStatusChangeHandler(evt -> { + Assert.fail( + "Using a custom status change handler so no change should end up here"); + }).bind(Person::getFirstName, Person::setFirstName); + binder.forField(ageField).withConverter(stringToInteger) + .withValidator(notNegative).withStatusChangeHandler(evt -> { + Assert.fail( + "Using a custom status change handler so no change should end up here"); + }).bind(Person::getAge, Person::setAge); + binder.withValidator( + bean -> !bean.getFirstName().isEmpty() && bean.getAge() > 0 + ? Result.ok(bean) + : Result.error("Need first name and age")); + + binder.setStatusHandler(r -> { + resultsCapture.set(r); + }); + binder.bind(p); + Assert.assertNull(nameField.getComponentError()); + + nameField.setValue(""); + ageField.setValue("5"); + + // First binding validation fails => should be result with ERROR status + // and message + binder.validate(); + + Assert.assertNull(nameField.getComponentError()); + + List> results = resultsCapture.get(); + Assert.assertNotNull(results); + Assert.assertEquals(2, results.size()); + + BinderResult r = results.get(0); + Assert.assertTrue(r.isError()); + Assert.assertEquals("Value cannot be empty", r.getMessage().get()); + Assert.assertEquals(nameField, r.getField().get()); + + r = results.get(1); + Assert.assertFalse(r.isError()); + Assert.assertFalse(r.getMessage().isPresent()); + Assert.assertEquals(ageField, r.getField().get()); + + nameField.setValue("foo"); + ageField.setValue(""); + + resultsCapture.set(null); + // Second validation succeeds => should be result with OK status and + // no message, and error result for age + binder.validate(); + + results = resultsCapture.get(); + Assert.assertNotNull(results); + Assert.assertEquals(2, results.size()); + + r = results.get(0); + Assert.assertFalse(r.isError()); + Assert.assertFalse(r.getMessage().isPresent()); + Assert.assertEquals(nameField, r.getField().get()); + + r = results.get(1); + Assert.assertTrue(r.isError()); + Assert.assertEquals("Value must be a number", r.getMessage().get()); + Assert.assertEquals(ageField, r.getField().get()); + + resultsCapture.set(null); + // binding validations pass, binder validation fails + ageField.setValue("0"); + binder.validate(); + + results = resultsCapture.get(); + Assert.assertNotNull(results); + Assert.assertEquals(1, results.size()); + + r = results.get(0); + Assert.assertTrue(r.isError()); + Assert.assertTrue(r.getMessage().isPresent()); + Assert.assertFalse(r.getField().isPresent()); + } + + @Test + public void binderWithStatusChangeHandler_defaultStatusChangeHandlerIsReplaced() { + Binding binding = binder.forField(nameField) + .withValidator(notEmpty).withStatusChangeHandler(evt -> { + }); + binding.bind(Person::getFirstName, Person::setFirstName); + + Assert.assertNull(nameField.getComponentError()); + + nameField.setValue(""); + + // First validation fails => should be event with ERROR status and + // message + binding.validate(); + + // no component error since default handler is replaced + Assert.assertNull(nameField.getComponentError()); + } + + @Test + public void binderWithStatusLabel_defaultStatusChangeHandlerIsReplaced() { + Label label = new Label(); + + Binding binding = binder.forField(nameField) + .withValidator(notEmpty).withStatusLabel(label); + binding.bind(Person::getFirstName, Person::setFirstName); + + Assert.assertNull(nameField.getComponentError()); + + nameField.setValue(""); + + // First validation fails => should be event with ERROR status and + // message + binding.validate(); + + // default behavior should update component error for the nameField + Assert.assertNull(nameField.getComponentError()); + } + + @Test(expected = IllegalStateException.class) + public void binderWithStatusChangeHandler_addAfterBound() { + Binding binding = binder.forField(nameField) + .withValidator(notEmpty); + binding.bind(Person::getFirstName, Person::setFirstName); + + binding.withStatusChangeHandler(evt -> Assert.fail()); + } + + @Test(expected = IllegalStateException.class) + public void binderWithStatusLabel_addAfterBound() { + Label label = new Label(); + + Binding binding = binder.forField(nameField) + .withValidator(notEmpty); + binding.bind(Person::getFirstName, Person::setFirstName); + + binding.withStatusLabel(label); + } + + @Test(expected = IllegalStateException.class) + public void binderWithStatusLabel_setAfterHandler() { + Label label = new Label(); + + Binding binding = binder.forField(nameField); + binding.bind(Person::getFirstName, Person::setFirstName); + + binder.setStatusHandler(event -> { + }); + + binder.setStatusLabel(label); + } + + @Test(expected = IllegalStateException.class) + public void binderWithStatusChangeHandler_setAfterLabel() { + Label label = new Label(); + + Binding binding = binder.forField(nameField); + binding.bind(Person::getFirstName, Person::setFirstName); + + binder.setStatusLabel(label); + + binder.setStatusHandler(event -> { + }); + } + + @Test(expected = NullPointerException.class) + public void binderWithNullStatusChangeHandler_throws() { + binder.setStatusHandler(null); + } + + @Test + public void binderWithStatusChangeHandler_replaceHandler() { + AtomicReference>> capture = new AtomicReference<>(); + + Binding binding = binder.forField(nameField); + binding.bind(Person::getFirstName, Person::setFirstName); + + binder.setStatusHandler(results -> { + Assert.fail(); + }); + + binder.setStatusHandler(results -> { + capture.set(results); + }); + + nameField.setValue("foo"); + binder.validate(); + + List> results = capture.get(); + Assert.assertNotNull(results); + Assert.assertEquals(1, results.size()); + } + } -- 2.39.5