From 7a1adf6b3b9878600759eaed26d679c69b603375 Mon Sep 17 00:00:00 2001 From: Denis Anisimov Date: Tue, 16 Aug 2016 12:47:55 +0300 Subject: Binding.withStatusChangeHandler and Binding.withStatusLabel (#30). Change-Id: Iecd8bd88d94b98829dfaec43b8635b1e93df330f --- server/src/main/java/com/vaadin/data/Binder.java | 158 ++++++++++++++++++--- .../java/com/vaadin/data/StatusChangeHandler.java | 40 ++++++ .../java/com/vaadin/data/ValidationStatus.java | 35 +++++ .../vaadin/data/ValidationStatusChangeEvent.java | 93 ++++++++++++ 4 files changed, 310 insertions(+), 16 deletions(-) create mode 100644 server/src/main/java/com/vaadin/data/StatusChangeHandler.java create mode 100644 server/src/main/java/com/vaadin/data/ValidationStatus.java create mode 100644 server/src/main/java/com/vaadin/data/ValidationStatusChangeEvent.java (limited to 'server/src/main') diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index 5ce80d2c0b..daac947c07 100644 --- a/server/src/main/java/com/vaadin/data/Binder.java +++ b/server/src/main/java/com/vaadin/data/Binder.java @@ -33,6 +33,7 @@ import com.vaadin.event.Registration; import com.vaadin.server.ErrorMessage; import com.vaadin.server.UserError; import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.Label; /** * Connects one or more {@code Field} components to properties of a backing data @@ -262,6 +263,80 @@ public class Binder implements Serializable { */ public HasValue getField(); + /** + * Sets the given {@code label} to show an error message if validation + * fails. + *

+ * The validation state of each field is updated whenever the user + * modifies the value of that field. The validation state is by default + * shown using {@link AbstractComponent#setComponentError} which is used + * by the layout that the field is shown in. Most built-in layouts will + * show this as a red exclamation mark icon next to the component, so + * that hovering or tapping the icon shows a tooltip with the message + * text. + *

+ * This method allows to customize the way a binder displays error + * messages to get more flexibility than what + * {@link AbstractComponent#setComponentError} provides (it replaces the + * default behavior). + *

+ * This is just a shorthand for + * {@link #withStatusChangeHandler(StatusChangeHandler)} method where + * the handler instance hides the {@code label} if there is no error and + * shows it with validation error message if validation fails. It means + * that it cannot be called after + * {@link #withStatusChangeHandler(StatusChangeHandler)} method call or + * {@link #withStatusChangeHandler(StatusChangeHandler)} after this + * method call. + * + * @see #withStatusChangeHandler(StatusChangeHandler) + * @see AbstractComponent#setComponentError(ErrorMessage) + * @param label + * label to show validation status for the field + * @return this binding, for chaining + */ + public default Binding withStatusLabel( + Label label) { + return withStatusChangeHandler(event -> { + label.setValue(event.getMessage().orElse("")); + // Only show the label when validation has failed + label.setVisible( + ValidationStatus.ERROR.equals(event.getStatus())); + }); + } + + /** + * Sets a {@link StatusChangeHandler} to track validation status + * changes. + *

+ * The validation state of each field is updated whenever the user + * modifies the value of that field. The validation state is by default + * shown using {@link AbstractComponent#setComponentError} which is used + * by the layout that the field is shown in. Most built-in layouts will + * show this as a red exclamation mark icon next to the component, so + * that hovering or tapping the icon shows a tooltip with the message + * text. + *

+ * This method allows to customize the way a binder displays error + * messages to get more flexibility than what + * {@link AbstractComponent#setComponentError} provides (it replaces the + * default behavior). + *

+ * The method may be called only once. It means there is no chain unlike + * {@link #withValidator(Validator)} or + * {@link #withConverter(Converter)}. Also it means that the shorthand + * method {@link #withStatusLabel(Label)} also may not be called after + * this method. + * + * @see #withStatusLabel(Label) + * @see AbstractComponent#setComponentError(ErrorMessage) + * @param handler + * status change handler + * @return this binding, for chaining + */ + public Binding withStatusChangeHandler( + StatusChangeHandler handler); + /** * Validates the field value and returns a {@code Result} instance * representing the outcome of the validation. @@ -293,6 +368,8 @@ public class Binder implements Serializable { private final HasValue field; private Registration onValueChange; + private StatusChangeHandler statusChangeHandler; + private boolean isStatusHandlerChanged; private Function getter; private BiConsumer setter; @@ -310,11 +387,15 @@ public class Binder implements Serializable { * the binder this instance is connected to * @param field * the field to bind + * @param statusChangeHandler + * handler to track validation status */ @SuppressWarnings("unchecked") - protected BindingImpl(Binder binder, HasValue field) { + protected BindingImpl(Binder binder, HasValue field, + StatusChangeHandler statusChangeHandler) { this(binder, field, - (Converter) Converter.identity()); + (Converter) Converter.identity(), + statusChangeHandler); } /** @@ -327,12 +408,16 @@ public class Binder implements Serializable { * the field to bind * @param converterValidatorChain * the converter/validator chain to use + * @param statusChangeHandler + * handler to track validation status */ protected BindingImpl(Binder binder, HasValue field, - Converter converterValidatorChain) { + Converter converterValidatorChain, + StatusChangeHandler statusChangeHandler) { this.field = field; this.binder = binder; this.converterValidatorChain = converterValidatorChain; + this.statusChangeHandler = statusChangeHandler; } @Override @@ -372,9 +457,21 @@ public class Binder implements Serializable { checkUnbound(); Objects.requireNonNull(converter, "converter cannot be null"); - BindingImpl newBinding = new BindingImpl<>( - binder, field, converterValidatorChain.chain(converter)); - return newBinding; + return createNewBinding(converter); + } + + @Override + public Binding withStatusChangeHandler( + StatusChangeHandler handler) { + Objects.requireNonNull(handler, "Handler may not be null"); + if (isStatusHandlerChanged) { + throw new IllegalStateException( + "A StatusChangeHandler has already been set"); + } + isStatusHandlerChanged = true; + checkUnbound(); + statusChangeHandler = handler; + return this; } private void bind(BEAN bean) { @@ -448,6 +545,23 @@ public class Binder implements Serializable { public HasValue getField() { return field; } + + private BindingImpl createNewBinding( + Converter converter) { + BindingImpl newBinding = new BindingImpl<>( + binder, field, converterValidatorChain.chain(converter), + statusChangeHandler); + return newBinding; + } + + private void fireStatusChangeEvent(Result result) { + ValidationStatusChangeEvent event = new ValidationStatusChangeEvent( + getField(), + result.isError() ? ValidationStatus.ERROR + : ValidationStatus.OK, + result.getMessage().orElse(null)); + statusChangeHandler.accept(event); + } } /** @@ -601,13 +715,10 @@ public class Binder implements Serializable { public List> validate() { List> resultErrors = new ArrayList<>(); for (BindingImpl binding : bindings) { - clearError(binding.field); - - binding.validate().ifError(errorMessage -> { - resultErrors.add( - new ValidationError<>(binding.field, errorMessage)); - handleError(binding.field, errorMessage); - }); + Result result = binding.validate(); + binding.fireStatusChangeEvent(result); + result.ifError(errorMessage -> resultErrors + .add(new ValidationError<>(binding.field, errorMessage))); } return resultErrors; } @@ -635,7 +746,6 @@ public class Binder implements Serializable { public void load(BEAN bean) { Objects.requireNonNull(bean, "bean cannot be null"); bindings.forEach(binding -> binding.setFieldValue(bean)); - } /** @@ -662,11 +772,11 @@ public class Binder implements Serializable { * the field to bind * @return the new incomplete binding */ - protected BindingImpl createBinding( + protected Binding createBinding( HasValue field) { Objects.requireNonNull(field, "field cannot be null"); BindingImpl b = new BindingImpl( - this, field); + this, field, this::handleValidationStatusChange); return b; } @@ -700,6 +810,22 @@ public class Binder implements Serializable { if (field instanceof AbstractComponent) { ((AbstractComponent) field).setComponentError(new UserError(error)); } + + } + + /** + * Default {@link StatusChangeHandler} functional method implementation. + * + * @param event + * the validation event + */ + private void handleValidationStatusChange( + ValidationStatusChangeEvent event) { + HasValue source = event.getSource(); + clearError(source); + if (Objects.equals(ValidationStatus.ERROR, event.getStatus())) { + handleError(source, event.getMessage().get()); + } } } diff --git a/server/src/main/java/com/vaadin/data/StatusChangeHandler.java b/server/src/main/java/com/vaadin/data/StatusChangeHandler.java new file mode 100644 index 0000000000..0fbfcf3d46 --- /dev/null +++ b/server/src/main/java/com/vaadin/data/StatusChangeHandler.java @@ -0,0 +1,40 @@ +/* + * Copyright 2000-2014 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.function.Consumer; + +import com.vaadin.data.Binder.Binding; + +/** + * Validation status change handler. + *

+ * Register an instance of this class using + * {@link Binding#withStatusChangeHandler(StatusChangeHandler) to be able to + * listen to validation status updates. + * + * @see Binding#withStatusChangeHandler(StatusChangeHandler) + * @see ValidationStatusChangeEvent + * + * @author Vaadin Ltd + * @since 8.0 + * + */ +public interface StatusChangeHandler + extends Consumer, Serializable { + +} diff --git a/server/src/main/java/com/vaadin/data/ValidationStatus.java b/server/src/main/java/com/vaadin/data/ValidationStatus.java new file mode 100644 index 0000000000..9582ad8153 --- /dev/null +++ b/server/src/main/java/com/vaadin/data/ValidationStatus.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2014 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 com.vaadin.data.Binder.Binding; + +/** + * Validation status. + *

+ * The status is the part of {@link ValidationStatusChangeEvent} which indicate + * whether the validation failed or not. + * + * @see ValidationStatusChangeEvent + * @see Binding#withStatusChangeHandler(StatusChangeHandler) + * @see StatusChangeHandler + * + * @author Vaadin Ltd + * @since 8.0 + */ +public enum ValidationStatus { + OK, ERROR; +} diff --git a/server/src/main/java/com/vaadin/data/ValidationStatusChangeEvent.java b/server/src/main/java/com/vaadin/data/ValidationStatusChangeEvent.java new file mode 100644 index 0000000000..fba7e5c510 --- /dev/null +++ b/server/src/main/java/com/vaadin/data/ValidationStatusChangeEvent.java @@ -0,0 +1,93 @@ +/* + * Copyright 2000-2014 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.EventObject; +import java.util.Objects; +import java.util.Optional; + +import com.vaadin.data.Binder.Binding; + +/** + * Validation status change event which is fired each time when validation is + * done. Use {@link Binding#withStatusChangeHandler(StatusChangeHandler)} method + * to add your validation handler to listen to the event. + * + * @see Binding#withStatusChangeHandler(StatusChangeHandler) + * @see StatusChangeHandler + * + * @author Vaadin Ltd + * @since 8.0 + * + */ +public class ValidationStatusChangeEvent extends EventObject { + + private final ValidationStatus status; + private final String message; + + /** + * Creates a new status change event. + *

+ * The {@code message} must be null if the {@code status} is + * {@link ValidationStatus#OK}. + * + * @param source + * field whose status has changed, not {@code null} + * @param status + * updated status value, not {@code null} + * @param message + * error message if status is ValidationStatus.ERROR, may be + * {@code null} + */ + public ValidationStatusChangeEvent(HasValue source, + ValidationStatus status, String message) { + super(source); + Objects.requireNonNull(source, "Event source may not be null"); + Objects.requireNonNull(status, "Status may not be null"); + if (Objects.equals(status, ValidationStatus.OK) && message != null) { + throw new IllegalStateException( + "Message must be null if status is not an error"); + } + this.status = status; + this.message = message; + } + + /** + * Returns validation status of the event. + * + * @return validation status + */ + public ValidationStatus getStatus() { + return status; + } + + /** + * Returns error validation message if status is + * {@link ValidationStatus#ERROR}. + * + * @return an optional validation error status or an empty optional if + * status is not an error + */ + public Optional getMessage() { + return Optional.ofNullable(message); + } + + @Override + public HasValue getSource() { + return (HasValue) super.getSource(); + } + +} -- cgit v1.2.3