]> source.dussan.org Git - vaadin-framework.git/commitdiff
Add Form level status handler and status label
authorPekka Hyvönen <pekka@vaadin.com>
Thu, 25 Aug 2016 21:29:50 +0000 (00:29 +0300)
committerVaadin Code Review <review@vaadin.com>
Thu, 8 Sep 2016 12:15:24 +0000 (12:15 +0000)
This feature doesn't make a whole lot of sense until
form level status changes are available.

Change-Id: Ie634c4a6b3511b7cbf9e367192034934b0e0d4b0

documentation/datamodel/datamodel-forms.asciidoc
server/src/main/java/com/vaadin/data/Binder.java
server/src/main/java/com/vaadin/data/BinderResult.java [new file with mode: 0644]
server/src/main/java/com/vaadin/data/BinderStatusHandler.java [new file with mode: 0644]
server/src/main/java/com/vaadin/data/Result.java
server/src/main/java/com/vaadin/data/SimpleResult.java
server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java
server/src/test/java/com/vaadin/data/BinderTest.java

index b86cf8f6722af2778e6a7bb02bbea7329a1dab59..62fd9df38cca255913e5007debe5789882a5b8d7 100644 (file)
@@ -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<BinderResult> 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);
 });
 ----
 
index cf7f788677e9101ac07770d03a4c453cdd537017..9150c046f67417b85dfc36128db8a5d8e43b1dd0 100644 (file)
@@ -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<BEAN> implements Serializable {
 
         @Override
         public Result<TARGET> validate() {
-            Result<TARGET> dataValue = getTargetValue();
-            fireStatusChangeEvent(dataValue);
-            return dataValue;
+            BinderResult<FIELDVALUE, TARGET> 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<TARGET> getTargetValue() {
+        private BinderResult<FIELDVALUE, TARGET> getTargetValue() {
             FIELDVALUE fieldValue = field.getValue();
             Result<TARGET> 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<BEAN> implements Serializable {
                 boolean runBeanLevelValidation) {
             assert bean != null;
             if (setter != null) {
-                getTargetValue().ifOk(value -> setBeanValue(bean, value));
+                BinderResult<FIELDVALUE, TARGET> 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<ValidationError<?>> errors = binder.validateItem(bean);
-                // TODO: Pass errors to Binder statusChangeHandler once that is
-                // available
+                binder.validateItem(bean);
             }
         }
 
@@ -576,7 +581,7 @@ public class Binder<BEAN> implements Serializable {
             setter.accept(bean, value);
         }
 
-        private void fireStatusChangeEvent(Result<TARGET> result) {
+        private void fireStatusChangeEvent(Result<?> result) {
             ValidationStatusChangeEvent event = new ValidationStatusChangeEvent(
                     getField(),
                     result.isError() ? ValidationStatus.ERROR
@@ -632,6 +637,10 @@ public class Binder<BEAN> implements Serializable {
 
     private final List<Validator<? super BEAN>> 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<BEAN> implements Serializable {
      * If all validators pass, the resulting list is empty.
      * <p>
      * Does not run bean validators.
+     * <p>
+     * All results are passed to the {@link #getStatusHandler() status change
+     * handler.}
      *
      * @see #validateItem(Object)
      *
@@ -916,13 +928,17 @@ public class Binder<BEAN> implements Serializable {
      *         succeeded
      */
     private List<ValidationError<?>> validateBindings() {
-        List<ValidationError<?>> resultErrors = new ArrayList<>();
+        List<BinderResult<?, ?>> 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<BEAN> implements Serializable {
      */
     private List<ValidationError<?>> validateItem(BEAN bean) {
         Objects.requireNonNull(bean, "bean cannot be null");
-        return validators.stream().map(validator -> validator.apply(bean))
+        List<BinderResult<?, ?>> 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.
+     * <p>
+     * Only the one validation error message is shown in this label at a time.
+     * <p>
+     * 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<Label> getStatusLabel() {
+        return Optional.ofNullable(statusLabel);
+    }
+
+    /**
+     * Sets the status handler to track form status changes.
+     * <p>
+     * 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)}.
+     * <p>
+     * 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 <code>null</code>
+     * @throws NullPointerException
+     *             for <code>null</code> 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.
+     * <p>
+     * If none has been set with {@link #setStatusHandler(BinderStatusHandler)},
+     * the default implementation is returned.
+     *
+     * @return the status handler used, never <code>null</code>
+     * @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<BEAN> implements Serializable {
         }
     }
 
+    /**
+     * The default binder level status handler.
+     * <p>
+     * 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<BinderResult<?, ?>> results) {
+        // let field events go to binding status handlers
+        results.stream().filter(br -> br.getField().isPresent())
+                .forEach(br -> ((BindingImpl<BEAN, ?, ?>) 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 (file)
index 0000000..52375b8
--- /dev/null
@@ -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 <FIELDVALUE>
+ *            the value type of the field
+ * @param <VALUE>
+ *            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<FIELDVALUE, VALUE> extends SimpleResult<VALUE> {
+
+    private final Binding<?, FIELDVALUE, VALUE> 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 <code>null</code>
+     * @param message
+     *            the error message of the result, may be {@code null}
+     */
+    public BinderResult(Binding<?, FIELDVALUE, VALUE> 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<Binding<?, FIELDVALUE, VALUE>> getBinding() {
+        return Optional.ofNullable(binding);
+    }
+
+    /**
+     * Return the field this result originated from, or an empty optional if
+     * none.
+     *
+     * @return the optional field
+     */
+    public Optional<HasValue<FIELDVALUE>> 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 (file)
index 0000000..4c516a5
--- /dev/null
@@ -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.
+ * <p>
+ * Register a handler using {@link Binder#setStatusHandler(BinderStatusHandler)}
+ * to be able to customize the status change handling such as displaying
+ * validation messages.
+ * <p>
+ * 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<List<BinderResult<?, ?>>>, Serializable {
+
+}
index b82b3ff4e7165c1f33b5aaab50bb6d51246ad286..5803eace11eef1b73f54194d3056b7b3ce019342 100644 (file)
@@ -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<R> extends Serializable {
      *            the mapping function
      * @return the mapped result
      */
-    public default <S> Result<S> map(Function<R, S> mapper) {
+    default <S> Result<S> map(Function<R, S> mapper) {
         return flatMap(value -> ok(mapper.apply(value)));
     }
 
@@ -119,7 +120,20 @@ public interface Result<R> extends Serializable {
      *            the mapping function
      * @return the mapped result
      */
-    public <S> Result<S> flatMap(Function<R, Result<S>> mapper);
+    <S> Result<S> flatMap(Function<R, Result<S>> 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 <S>
+     *            the type of the mapped value
+     * @param mapper
+     *            the mapping function
+     * @return the mapped result
+     */
+    <S> S biMap(BiFunction<R, String, S> mapper);
 
     /**
      * Invokes either the first callback or the second one, depending on whether
@@ -130,7 +144,7 @@ public interface Result<R> extends Serializable {
      * @param ifError
      *            the function to call if failure
      */
-    public void handle(Consumer<R> ifOk, Consumer<String> ifError);
+    void handle(Consumer<R> ifOk, Consumer<String> ifError);
 
     /**
      * Applies the {@code consumer} if result is not an error.
@@ -138,7 +152,7 @@ public interface Result<R> extends Serializable {
      * @param consumer
      *            consumer to apply in case it's not an error
      */
-    public default void ifOk(Consumer<R> consumer) {
+    default void ifOk(Consumer<R> consumer) {
         handle(consumer, error -> {
         });
     }
@@ -149,7 +163,7 @@ public interface Result<R> extends Serializable {
      * @param consumer
      *            consumer to apply in case it's an error
      */
-    public default void ifError(Consumer<String> consumer) {
+    default void ifError(Consumer<String> consumer) {
         handle(value -> {
         }, consumer);
     }
@@ -160,14 +174,14 @@ public interface Result<R> extends Serializable {
      * @return <code>true</code> if the result denotes an error,
      *         <code>false</code> 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<String> getMessage();
+    Optional<String> getMessage();
 
     /**
      * Return the value, if the result denotes success, otherwise throw an
@@ -182,6 +196,6 @@ public interface Result<R> extends Serializable {
      * @throws X
      *             if this result denotes an error
      */
-    public <X extends Throwable> R getOrThrow(
+    <X extends Throwable> R getOrThrow(
             Function<String, ? extends X> exceptionProvider) throws X;
 }
index 935fb545e3b832bede0b2a01f7e264f7ea2ab643..ceedcb5ae3f8b95bd349b1c947a6de33ac9e2352 100644 (file)
@@ -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<R> implements Result<R> {
         }
     }
 
+    @Override
+    public <S> S biMap(BiFunction<R, String, S> mapper) {
+        return mapper.apply(value, message);
+    }
+
     @Override
     public void handle(Consumer<R> ifOk, Consumer<String> ifError) {
         Objects.requireNonNull(ifOk, "ifOk cannot be null");
index c51d41d6a727d01f6455bd21821bb303c7100a05..6861903132b017dc6f663aeb7f7812cab510b48c 100644 (file)
@@ -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<ValidationStatusChangeEvent> 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<BookPerson> 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<ValidationError<?>> 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<ValidationError<?>> 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());
+
+    }
 }
index 4c59773b2fb6256e8086a7378edb54b293791cd9..e6b78eb6b373cf797f4e48ba6d21816f9ea35b76 100644 (file)
@@ -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<Person, String, String> 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<Person, String, String> 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<Person, String, String> 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<Person, String, String> 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<Person, String, String> 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<Person, String, String> 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<Person, String, String> 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<Person, String, String> 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<List<BinderResult<?, ?>>> 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<BinderResult<?, ?>> 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<Person, String, String> 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<Person, String, String> 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<Person, String, String> 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<Person, String, String> 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<Person, String, String> 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<Person, String, String> 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<List<BinderResult<?, ?>>> capture = new AtomicReference<>();
+
+        Binding<Person, String, String> 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<BinderResult<?, ?>> results = capture.get();
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+    }
+
 }