diff options
author | Teemu Suo-Anttila <teemusa@vaadin.com> | 2016-11-17 17:05:28 +0200 |
---|---|---|
committer | Vaadin Code Review <review@vaadin.com> | 2016-11-25 13:13:00 +0000 |
commit | d37b2d430eca6b0eeacb48626c0bbfb33d1502de (patch) | |
tree | a73527e2b0eef2e902572908c1c6a22d44a2ab27 /server | |
parent | d63c1f9014e0a49e5250edd41bd5c7542901c267 (diff) | |
download | vaadin-framework-d37b2d430eca6b0eeacb48626c0bbfb33d1502de.tar.gz vaadin-framework-d37b2d430eca6b0eeacb48626c0bbfb33d1502de.zip |
Reintroduce Grid Editor using Binder
This patch restores the bean type to BinderValidationStatusHandler
Change-Id: I9ace77a492c4823c15591fb1426e9bd216895fb0
Diffstat (limited to 'server')
5 files changed, 595 insertions, 11 deletions
diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index 771726ea3c..df6d72d79c 100644 --- a/server/src/main/java/com/vaadin/data/Binder.java +++ b/server/src/main/java/com/vaadin/data/Binder.java @@ -884,7 +884,7 @@ public class Binder<BEAN> implements Serializable { private Label statusLabel; - private BinderValidationStatusHandler statusHandler; + private BinderValidationStatusHandler<BEAN> statusHandler; private boolean hasChanges = false; @@ -1356,7 +1356,7 @@ public class Binder<BEAN> implements Serializable { * @see Binding#withValidationStatusHandler(ValidationStatusHandler) */ public void setValidationStatusHandler( - BinderValidationStatusHandler statusHandler) { + BinderValidationStatusHandler<BEAN> statusHandler) { Objects.requireNonNull(statusHandler, "Cannot set a null " + BinderValidationStatusHandler.class.getSimpleName()); if (statusLabel != null) { @@ -1377,7 +1377,7 @@ public class Binder<BEAN> implements Serializable { * @return the status handler used, never <code>null</code> * @see #setValidationStatusHandler(BinderStatusHandler) */ - public BinderValidationStatusHandler getValidationStatusHandler() { + public BinderValidationStatusHandler<BEAN> getValidationStatusHandler() { return Optional.ofNullable(statusHandler) .orElse(this::handleBinderValidationStatus); } @@ -1509,7 +1509,7 @@ public class Binder<BEAN> implements Serializable { * validators */ protected void handleBinderValidationStatus( - BinderValidationStatus<?> binderStatus) { + BinderValidationStatus<BEAN> binderStatus) { // let field events go to binding status handlers binderStatus.getFieldValidationStatuses() .forEach(status -> ((BindingImpl<?, ?, ?>) status.getBinding()) diff --git a/server/src/main/java/com/vaadin/data/BinderValidationStatusHandler.java b/server/src/main/java/com/vaadin/data/BinderValidationStatusHandler.java index 691b7bd6c0..1f3a95688b 100644 --- a/server/src/main/java/com/vaadin/data/BinderValidationStatusHandler.java +++ b/server/src/main/java/com/vaadin/data/BinderValidationStatusHandler.java @@ -23,8 +23,8 @@ import com.vaadin.ui.AbstractComponent; /** * Handler for {@link BinderValidationStatus} changes. * <p> - * {{@link Binder#setValidationStatusHandler(BinderStatusHandler) Register} an instance of - * this class to be able to customize validation status handling. + * {{@link Binder#setValidationStatusHandler(BinderStatusHandler) Register} an + * instance of this class to be able to customize validation status handling. * <p> * The default handler will show * {@link AbstractComponent#setComponentError(com.vaadin.server.ErrorMessage) an @@ -39,9 +39,12 @@ import com.vaadin.ui.AbstractComponent; * @see Binder#validate() * @see ValidationStatus * + * @param <BEAN> + * the bean type of binder + * * @since 8.0 */ -public interface BinderValidationStatusHandler - extends Consumer<BinderValidationStatus<?>>, Serializable { +public interface BinderValidationStatusHandler<BEAN> + extends Consumer<BinderValidationStatus<BEAN>>, Serializable { } diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java index ee1547bfce..53e22a53d4 100644 --- a/server/src/main/java/com/vaadin/ui/Grid.java +++ b/server/src/main/java/com/vaadin/ui/Grid.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.stream.Collectors; @@ -39,6 +40,7 @@ import java.util.stream.Stream; import org.jsoup.nodes.Element; import com.vaadin.data.Binder; +import com.vaadin.data.BinderValidationStatus; import com.vaadin.data.SelectionModel; import com.vaadin.event.ConnectorEvent; import com.vaadin.event.ContextClickEvent; @@ -48,6 +50,7 @@ import com.vaadin.server.Extension; import com.vaadin.server.JsonCodec; import com.vaadin.server.SerializableComparator; import com.vaadin.server.SerializableFunction; +import com.vaadin.server.data.DataCommunicator; import com.vaadin.server.data.SortOrder; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.Registration; @@ -63,6 +66,7 @@ import com.vaadin.shared.ui.grid.HeightMode; import com.vaadin.shared.ui.grid.SectionState; import com.vaadin.shared.util.SharedUtil; import com.vaadin.ui.Grid.FooterRow; +import com.vaadin.ui.components.grid.EditorImpl; import com.vaadin.ui.components.grid.Footer; import com.vaadin.ui.components.grid.Header; import com.vaadin.ui.components.grid.Header.Row; @@ -587,6 +591,11 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { .getSortOrder(order.getDirection())) .forEach(s -> s.forEach(sortProperties::add)); getDataCommunicator().setBackEndSorting(sortProperties); + + // Close grid editor if it's open. + if (getEditor().isOpen()) { + getEditor().cancel(); + } } @Override @@ -795,6 +804,8 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { private StyleGenerator<T> styleGenerator = item -> null; private DescriptionGenerator<T> descriptionGenerator; + private SerializableFunction<T, Component> componentGenerator; + /** * Constructs a new Column configuration with given header caption, * renderer and value provider. @@ -1498,8 +1509,85 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { } /** + * Sets whether this Column has a component displayed in Editor or not. + * + * @param editable + * {@code true} if column is editable; {@code false} if not + * @return this column + * + * @see #setEditorComponent(Component) + * @see #setEditorComponentGenerator(SerializableFunction) + */ + public Column<T, V> setEditable(boolean editable) { + Objects.requireNonNull(componentGenerator, + "Column has no editor component defined"); + getState().editable = editable; + return this; + } + + /** + * Gets whether this Column has a component displayed in Editor or not. + * + * @return {@code true} if the column displays an editor component; + * {@code false} if not + */ + public boolean isEditable() { + return getState(false).editable; + } + + /** + * Sets a static editor component for this column. + * <p> + * <strong>Note:</strong> The same component cannot be used for multiple + * columns. + * + * @param component + * the editor component + * @return this column + * + * @see Editor#getBinder() + * @see Editor#setBinder(Binder) + * @see #setEditorComponentGenerator(SerializableFunction) + */ + public Column<T, V> setEditorComponent(Component component) { + Objects.requireNonNull(component, + "null is not a valid editor field"); + return setEditorComponentGenerator(t -> component); + } + + /** + * Sets a component generator to provide editor component for this + * Column. This method can be used to generate any dynamic component to + * be displayed in the editor row. + * <p> + * <strong>Note:</strong> The same component cannot be used for multiple + * columns. + * + * @param componentGenerator + * the editor component generator + * @return this column + * + * @see #setEditorComponent(Component) + */ + public Column<T, V> setEditorComponentGenerator( + SerializableFunction<T, Component> componentGenerator) { + Objects.requireNonNull(componentGenerator); + this.componentGenerator = componentGenerator; + return setEditable(true); + } + + /** + * Gets the editor component generator for this Column. + * + * @return editor component generator + */ + public SerializableFunction<T, Component> getEditorComponentGenerator() { + return componentGenerator; + } + + /** * Checks if column is attached and throws an - * {@link IllegalStateException} if it is not + * {@link IllegalStateException} if it is not. * * @throws IllegalStateException * if the column is no longer attached to any grid @@ -1557,7 +1645,7 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { * The cells to merge. Must be from the same row. * @return The remaining visible cell after the merge */ - HeaderCell join(HeaderCell ... cellsToMerge); + HeaderCell join(HeaderCell... cellsToMerge); } @@ -1716,6 +1804,167 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { public GridStaticCellType getCellType(); } + /** + * Generator for creating editor validation and conversion error messages. + * + * @param <T> + * the bean type + */ + public interface EditorErrorGenerator<T> extends Serializable, + BiFunction<Map<Component, Column<T, ?>>, BinderValidationStatus<T>, String> { + + /** + * Generates an error message from given validation status object. + * + * @param fieldToColumn + * the map of failed fields and corresponding columns + * @param status + * the binder status object with all failures + * + * @return error message string + */ + @Override + public String apply(Map<Component, Column<T, ?>> fieldToColumn, + BinderValidationStatus<T> status); + } + + /** + * An editor in a Grid. + * + * @param <T> + */ + public interface Editor<T> extends Serializable { + + /** + * Sets the underlying Binder to this Editor. + * + * @param binder + * the binder for updating editor fields; not {@code null} + * @return this editor + */ + public Editor<T> setBinder(Binder<T> binder); + + /** + * Returns the underlying Binder from Editor. + * + * @return the binder; not {@code null} + */ + public Binder<T> getBinder(); + + /** + * Sets the Editor buffered mode. When the editor is in buffered mode, + * edits are only committed when the user clicks the save button. In + * unbuffered mode valid changes are automatically committed. + * + * @param buffered + * {@code true} if editor should be buffered; {@code false} + * if not + * @return this editor + */ + public Editor<T> setBuffered(boolean buffered); + + /** + * Enables or disabled the Editor. A disabled editor cannot be opened. + * + * @param enabled + * {@code true} if editor should be enabled; {@code false} if + * not + * @return this editor + */ + public Editor<T> setEnabled(boolean enabled); + + /** + * Returns whether Editor is buffered or not. + * + * @see #setBuffered(boolean) + * + * @return {@code true} if editor is buffered; {@code false} if not + */ + public boolean isBuffered(); + + /** + * Returns whether Editor is enabled or not. + * + * @return {@code true} if editor is enabled; {@code false} if not + */ + public boolean isEnabled(); + + /** + * Returns whether Editor is open or not. + * + * @return {@code true} if editor is open; {@code false} if not + */ + public boolean isOpen(); + + /** + * Saves any changes from the Editor fields to the edited bean. + * + * @return {@code true} if save succeeded; {@code false} if not + */ + public boolean save(); + + /** + * Close the editor discarding any unsaved changes. + */ + public void cancel(); + + /** + * Sets the caption of the save button in buffered mode. + * + * @param saveCaption + * the save button caption + * @return this editor + */ + public Editor<T> setSaveCaption(String saveCaption); + + /** + * Sets the caption of the cancel button in buffered mode. + * + * @param cancelCaption + * the cancel button caption + * @return this editor + */ + public Editor<T> setCancelCaption(String cancelCaption); + + /** + * Gets the caption of the save button in buffered mode. + * + * @return the save button caption + */ + public String getSaveCaption(); + + /** + * Gets the caption of the cancel button in buffered mode. + * + * @return the cancel button caption + */ + public String getCancelCaption(); + + /** + * Sets the error message generator for this editor. + * <p> + * The default message is a concatenation of column field validation + * failures and bean validation failures. + * + * @param errorGenerator + * the function to generate error messages; not {@code null} + * @return this editor + * + * @see EditorErrorGenerator + */ + public Editor<T> setErrorGenerator( + EditorErrorGenerator<T> errorGenerator); + + /** + * Gets the error message generator of this editor. + * + * @return the function that generates error messages; not {@code null} + * + * @see EditorErrorGenerator + */ + public EditorErrorGenerator<T> getErrorGenerator(); + } + private class HeaderImpl extends Header { @Override @@ -1768,6 +2017,8 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { private GridSelectionModel<T> selectionModel; + private Editor<T> editor; + /** * Constructor for the {@link Grid} component. */ @@ -1782,6 +2033,11 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { addExtension(detailsManager); addDataGenerator(detailsManager); + editor = createEditor(); + if (editor instanceof Extension) { + addExtension((Extension) editor); + } + addDataGenerator((item, json) -> { String styleName = styleGenerator.apply(item); if (styleName != null && !styleName.isEmpty()) { @@ -2651,6 +2907,10 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { return ((SingleSelectionModel<T>) model).asSingleSelect(); } + public Editor<T> getEditor() { + return editor; + } + /** * Sets the selection model for this listing. * <p> @@ -2675,6 +2935,17 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { return (GridState) super.getState(markAsDirty); } + /** + * Creates a new Editor instance. Can be overridden to create a custom + * Editor. If the Editor is a {@link AbstractGridExtension}, it will be + * automatically added to {@link DataCommunicator}. + * + * @return editor + */ + protected Editor<T> createEditor() { + return new EditorImpl<>(); + } + private void addExtensionComponent(Component c) { if (extensionComponents.add(c)) { c.setParent(this); diff --git a/server/src/main/java/com/vaadin/ui/components/grid/EditorImpl.java b/server/src/main/java/com/vaadin/ui/components/grid/EditorImpl.java new file mode 100644 index 0000000000..bc83547488 --- /dev/null +++ b/server/src/main/java/com/vaadin/ui/components/grid/EditorImpl.java @@ -0,0 +1,310 @@ +/* + * 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.ui.components.grid; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.vaadin.data.Binder; +import com.vaadin.data.BinderValidationStatus; +import com.vaadin.data.BinderValidationStatusHandler; +import com.vaadin.shared.ui.grid.editor.EditorClientRpc; +import com.vaadin.shared.ui.grid.editor.EditorServerRpc; +import com.vaadin.shared.ui.grid.editor.EditorState; +import com.vaadin.ui.Component; +import com.vaadin.ui.Grid.AbstractGridExtension; +import com.vaadin.ui.Grid.Column; +import com.vaadin.ui.Grid.Editor; +import com.vaadin.ui.Grid.EditorErrorGenerator; + +import elemental.json.JsonObject; + +/** + * Implementation of {@code Editor} interface. + * + * @param <T> + * the grid bean type + */ +public class EditorImpl<T> extends AbstractGridExtension<T> + implements Editor<T> { + + private class EditorStatusHandler + implements BinderValidationStatusHandler<T> { + + @Override + public void accept(BinderValidationStatus<T> status) { + boolean ok = status.isOk(); + if (saving) { + rpc.confirmSave(ok); + saving = false; + } + + if (ok) { + binder.getBean().ifPresent(t -> refresh(t)); + rpc.setErrorMessage(null, Collections.emptyList()); + } else { + List<Component> fields = status.getFieldValidationErrors() + .stream().map(error -> error.getField()) + .filter(columnFields.values()::contains) + .map(field -> (Component) field) + .collect(Collectors.toList()); + + Map<Component, Column<T, ?>> fieldToColumn = new HashMap<>(); + columnFields.entrySet().stream() + .filter(entry -> fields.contains(entry.getValue())) + .forEach(entry -> fieldToColumn.put(entry.getValue(), + entry.getKey())); + + String message = errorGenerator.apply(fieldToColumn, status); + + List<String> columnIds = fieldToColumn.values().stream() + .map(Column::getId).collect(Collectors.toList()); + + rpc.setErrorMessage(message, columnIds); + } + } + + } + + private Binder<T> binder; + private Map<Column<T, ?>, Component> columnFields = new HashMap<>(); + private T edited; + private boolean saving = false; + private EditorClientRpc rpc; + private EditorErrorGenerator<T> errorGenerator = (fieldToColumn, + status) -> { + String message = status.getFieldValidationErrors().stream() + .filter(e -> e.getMessage().isPresent() + && fieldToColumn.containsKey(e.getField())) + .map(e -> fieldToColumn.get(e.getField()).getCaption() + ": " + + e.getMessage().get()) + .collect(Collectors.joining("; ")); + + String beanMessage = status.getBeanValidationErrors().stream() + .map(e -> e.getErrorMessage()) + .collect(Collectors.joining("; ")); + + message = Stream.of(message, beanMessage).filter(s -> !s.isEmpty()) + .collect(Collectors.joining("; ")); + + return message; + }; + + /** + * Constructor for internal implementation of the Editor. + */ + public EditorImpl() { + rpc = getRpcProxy(EditorClientRpc.class); + registerRpc(new EditorServerRpc() { + + @Override + public void save() { + saving = true; + EditorImpl.this.save(); + } + + @Override + public void cancel() { + doClose(); + } + + @Override + public void bind(String key) { + // When in buffered mode, the editor is not allowed to move. + // Binder with failed validation returns true for hasChanges. + if (isOpen() && (isBuffered() || getBinder().hasChanges())) { + rpc.confirmBind(false); + return; + } + doClose(); + doEdit(getData(key)); + rpc.confirmBind(true); + } + }); + + setBinder(new Binder<>()); + } + + @Override + public void generateData(T item, JsonObject jsonObject) { + } + + @Override + public Editor<T> setBinder(Binder<T> binder) { + this.binder = binder; + + binder.setValidationStatusHandler(new EditorStatusHandler()); + return this; + } + + @Override + public Binder<T> getBinder() { + return binder; + } + + @Override + public Editor<T> setBuffered(boolean buffered) { + if (isOpen()) { + throw new IllegalStateException( + "Cannot modify Editor when it is open."); + } + getState().buffered = buffered; + + return this; + } + + @Override + public Editor<T> setEnabled(boolean enabled) { + if (isOpen()) { + throw new IllegalStateException( + "Cannot modify Editor when it is open."); + } + getState().enabled = enabled; + + return this; + } + + @Override + public boolean isBuffered() { + return getState(false).buffered; + } + + @Override + public boolean isEnabled() { + return getState(false).enabled; + } + + /** + * Handles editor component generation and adding them to the hierarchy of + * the Grid. + * + * @param bean + * the edited item; can't be {@code null} + */ + protected void doEdit(T bean) { + Objects.requireNonNull(bean, "Editor can't edit null"); + if (!isEnabled()) { + throw new IllegalStateException( + "Editing is not allowed when Editor is disabled."); + } + + if (!isBuffered()) { + binder.setBean(bean); + } else { + binder.readBean(bean); + } + edited = bean; + + getParent().getColumns().stream().filter(Column::isEditable) + .forEach(c -> { + Component component = c.getEditorComponentGenerator() + .apply(edited); + addComponentToGrid(component); + columnFields.put(c, component); + getState().columnFields.put(c.getId(), + component.getConnectorId()); + }); + } + + @Override + public boolean save() { + if (isOpen() && isBuffered()) { + binder.validate(); + if (binder.writeBeanIfValid(edited)) { + refresh(edited); + return true; + } + } + return false; + } + + @Override + public boolean isOpen() { + return edited != null; + } + + @Override + public void cancel() { + doClose(); + rpc.cancel(); + } + + /** + * Handles clean up for closing the Editor. + */ + protected void doClose() { + edited = null; + + for (Component c : columnFields.values()) { + removeComponentFromGrid(c); + } + columnFields.clear(); + getState().columnFields.clear(); + } + + @Override + public Editor<T> setSaveCaption(String saveCaption) { + Objects.requireNonNull(saveCaption); + getState().saveCaption = saveCaption; + + return this; + } + + @Override + public Editor<T> setCancelCaption(String cancelCaption) { + Objects.requireNonNull(cancelCaption); + getState().cancelCaption = cancelCaption; + + return this; + } + + @Override + public String getSaveCaption() { + return getState(false).saveCaption; + } + + @Override + public String getCancelCaption() { + return getState(false).cancelCaption; + } + + @Override + protected EditorState getState() { + return getState(true); + } + + @Override + protected EditorState getState(boolean markAsDirty) { + return (EditorState) super.getState(markAsDirty); + } + + @Override + public Editor<T> setErrorGenerator(EditorErrorGenerator<T> errorGenerator) { + Objects.requireNonNull(errorGenerator, "Error generator can't be null"); + this.errorGenerator = errorGenerator; + return this; + } + + @Override + public EditorErrorGenerator<T> getErrorGenerator() { + return errorGenerator; + } +}
\ No newline at end of file diff --git a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java index a621549b83..14a6be0307 100644 --- a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java +++ b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java @@ -637,7 +637,7 @@ public class BinderBookOfVaadinTest { public void withBinderStatusHandlerExample() { Label formStatusLabel = new Label(); - BinderValidationStatusHandler defaultHandler = binder + BinderValidationStatusHandler<BookPerson> defaultHandler = binder .getValidationStatusHandler(); binder.setValidationStatusHandler(status -> { |