diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/AbstractColumn.java b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/AbstractColumn.java index c5342c570f5..0868af9a985 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/AbstractColumn.java +++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/AbstractColumn.java @@ -73,12 +73,14 @@ public Grid getGrid() { /** * {@inheritDoc} *

- * Note that column related data is sent to the client side even if the - * column is invisible. Use {@link Grid#removeColumn(Column)} to remove - * column (or don't add the column all) and avoid sending extra data. + * By default, every added column sends data to the client side regardless + * of its visibility state. To avoid sending extra data, either remove the + * column using {@link Grid#removeColumn(Column)} or use + * {@link Column#setGenerateDataWhenHidden(boolean)}. *

* * @see Grid#removeColumn(Column) + * @see Column#setGenerateDataWhenHidden(boolean) */ @Override public void setVisible(boolean visible) { diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/Grid.java b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/Grid.java index 51112cce321..3da9ee36ecb 100755 --- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/Grid.java +++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/Grid.java @@ -31,6 +31,7 @@ import java.util.Set; import java.util.function.BiFunction; import java.util.function.BinaryOperator; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -432,9 +433,10 @@ public static void setDefaultMultiSortPriority(MultiSortPriority priority) { * Server-side component for the {@code } element. * *

- * Every added column sends data to the client side regardless of its - * visibility state. Don't add a new column at all or use - * {@link Grid#removeColumn(Column)} to avoid sending extra data. + * By default, every added column sends data to the client side regardless + * of its visibility state. To avoid sending extra data, either remove the + * column using {@link Grid#removeColumn(Column)} or use + * {@link #setGenerateDataWhenHidden(boolean)}. *

* * @param @@ -448,6 +450,7 @@ public static class Column extends AbstractColumn> { private String columnKey; // defined and used by the user private boolean sortingEnabled; + private boolean generateDataWhenHidden = true; private Component editorComponent; private EditorRenderer editorRenderer; @@ -499,8 +502,48 @@ public Column(Grid grid, String columnId, Renderer renderer) { .getDataGenerator(); if (dataGenerator.isPresent()) { + var generator = dataGenerator.get(); + + // Use an anonymous class instead of Lambda to prevent potential + // deserialization issues when used with Grid + // see https://github.com/vaadin/flow-components/issues/6256 + var conditionalDataGenerator = new DataGenerator() { + @Override + public void generateData(T item, JsonObject jsonObject) { + if (isGenerateDataWhenHidden() + || Column.this.isVisible()) { + generator.generateData(item, jsonObject); + } + } + + @Override + public void destroyData(T item) { + generator.destroyData(item); + } + + @Override + public void destroyAllData() { + generator.destroyAllData(); + } + + @Override + public void refreshData(T item) { + generator.refreshData(item); + } + }; columnDataGeneratorRegistration = grid - .addDataGenerator(dataGenerator.get()); + .addDataGenerator(conditionalDataGenerator); + } + } + + @Override + public void setVisible(boolean visible) { + // Data has to be generated for previously hidden columns + boolean resetDataCommunicator = !isGenerateDataWhenHidden() + && visible && !isVisible(); + super.setVisible(visible); + if (resetDataCommunicator) { + getGrid().getDataCommunicator().reset(); } } @@ -769,7 +812,7 @@ public Column setComparator(Comparator comparator) { * the value provider used to extract the {@link Comparable} * sort key * @return this column - * @see Comparator#comparing(java.util.function.Function) + * @see Comparator#comparing(Function) */ public > Column setComparator( ValueProvider keyExtractor) { @@ -894,6 +937,49 @@ public boolean isSortable() { return sortingEnabled; } + /** + * Sets whether the data for this column should be generated and sent to + * the client even when the column is hidden. By default, data for + * hidden columns is generated and sent to the client. + *

+ * Setting this property to {@code false} will prevent the data for this + * column from being generated and sent to the client when the column is + * hidden. Alternatively, you can remove the column using + * {@link Grid#removeColumn(Column)} or avoid adding the column + * altogether. + *

+ * + * @param generateDataWhenHidden + * {@code true} to generate data even when the column is + * hidden, {@code false} otherwise + * @return this column + */ + public Column setGenerateDataWhenHidden( + boolean generateDataWhenHidden) { + if (this.generateDataWhenHidden == generateDataWhenHidden) { + return this; + } + // Data has to be generated for hidden columns. + if (!isVisible() && generateDataWhenHidden + && !isGenerateDataWhenHidden()) { + getGrid().getDataCommunicator().reset(); + } + this.generateDataWhenHidden = generateDataWhenHidden; + return this; + } + + /** + * Returns whether the data for this column is generated and sent to the + * client when the column is hidden. The default is {@code true}. + * + * @return {@code true} if data is generated even when the column is + * hidden, {@code false} otherwise + * @see #setGenerateDataWhenHidden(boolean) + */ + public boolean isGenerateDataWhenHidden() { + return generateDataWhenHidden; + } + /** * Sets a header text to the column. *

@@ -1859,9 +1945,10 @@ protected GridArrayUpdater createDefaultArrayUpdater( * see {@link #addColumn(Renderer)}. *

*

- * Every added column sends data to the client side regardless of its - * visibility state. Don't add a new column at all or use - * {@link Grid#removeColumn(Column)} to avoid sending extra data. + * By default, every added column sends data to the client side regardless + * of its visibility state. To avoid sending extra data, either remove the + * column using {@link #removeColumn(Column)} or use + * {@link Column#setGenerateDataWhenHidden(boolean)}. *

*

* NOTE: This method is a shorthand for @@ -1892,9 +1979,10 @@ public Column addColumn(ValueProvider valueProvider) { * see {@link #addColumn(Renderer)}. *

*

- * Every added column sends data to the client side regardless of its - * visibility state. Don't add a new column at all or use - * {@link Grid#removeColumn(Column)} to avoid sending extra data. + * By default, every added column sends data to the client side regardless + * of its visibility state. To avoid sending extra data, either remove the + * column using {@link #removeColumn(Column)} or use + * {@link Column#setGenerateDataWhenHidden(boolean)}. *

* * @param valueProvider @@ -1954,10 +2042,12 @@ private String formatValueToSendToTheClient(Object value) { * NOTE: Using {@link ComponentRenderer} is not as efficient as the * built in renderers or using {@link LitRenderer}. *

+ * *

- * Every added column sends data to the client side regardless of its - * visibility state. Don't add a new column at all or use - * {@link Grid#removeColumn(Column)} to avoid sending extra data. + * By default, every added column sends data to the client side regardless + * of its visibility state. To avoid sending extra data, either remove the + * column using {@link #removeColumn(Column)} or use + * {@link Column#setGenerateDataWhenHidden(boolean)}. *

* * @param componentProvider @@ -1983,9 +2073,10 @@ public Column addComponentColumn( * {@link ValueProvider}. * *

- * Every added column sends data to the client side regardless of its - * visibility state. Don't add a new column at all or use - * {@link Grid#removeColumn(Column)} to avoid sending extra data. + * By default, every added column sends data to the client side regardless + * of its visibility state. To avoid sending extra data, either remove the + * column using {@link #removeColumn(Column)} or use + * {@link Column#setGenerateDataWhenHidden(boolean)}. *

* * @see Column#setComparator(ValueProvider) @@ -2022,9 +2113,10 @@ public > Column addColumn( * or using {@link LitRenderer}. *

*

- * Every added column sends data to the client side regardless of its - * visibility state. Don't add a new column at all or use - * {@link Grid#removeColumn(Column)} to avoid sending extra data. + * By default, every added column sends data to the client side regardless + * of its visibility state. To avoid sending extra data, either remove the + * column using {@link #removeColumn(Column)} or use + * {@link Column#setGenerateDataWhenHidden(boolean)}. *

*

* NOTE: This method is a shorthand for @@ -2060,9 +2152,10 @@ public Column addColumn(Renderer renderer) { * or using {@link LitRenderer}. *

*

- * Every added column sends data to the client side regardless of its - * visibility state. Don't add a new column at all or use - * {@link Grid#removeColumn(Column)} to avoid sending extra data. + * By default, every added column sends data to the client side regardless + * of its visibility state. To avoid sending extra data, either remove the + * column using {@link #removeColumn(Column)} or use + * {@link Column#setGenerateDataWhenHidden(boolean)}. *

* * @param renderer @@ -2167,9 +2260,10 @@ protected BiFunction, String, Column> getDefaultColumnFactory() { * from a bean type with {@link #Grid(Class)}. * *

- * Every added column sends data to the client side regardless of its - * visibility state. Don't add a new column at all or use - * {@link Grid#removeColumn(Column)} to avoid sending extra data. + * By default, every added column sends data to the client side regardless + * of its visibility state. To avoid sending extra data, either remove the + * column using {@link #removeColumn(Column)} or use + * {@link Column#setGenerateDataWhenHidden(boolean)}. *

* *

@@ -2208,9 +2302,10 @@ public Column addColumn(String propertyName) { * from a bean type with {@link #Grid(Class)}. * *

- * Every added column sends data to the client side regardless of its - * visibility state. Don't add a new column at all or use - * {@link Grid#removeColumn(Column)} to avoid sending extra data. + * By default, every added column sends data to the client side regardless + * of its visibility state. To avoid sending extra data, either remove the + * column using {@link #removeColumn(Column)} or use + * {@link Column#setGenerateDataWhenHidden(boolean)}. *

* * @see #addColumn(String) @@ -2283,11 +2378,11 @@ private Object runPropertyValueGetter(PropertyDefinition property, *

* Note: This method can only be used for a Grid created * from a bean type with {@link #Grid(Class)}. - * *

- * Every added column sends data to the client side regardless of its - * visibility state. Don't add a new column at all or use - * {@link Grid#removeColumn(Column)} to avoid sending extra data. + * By default, every added column sends data to the client side regardless + * of its visibility state. To avoid sending extra data, either remove the + * column using {@link #removeColumn(Column)} or use + * {@link Column#setGenerateDataWhenHidden(boolean)}. *

* * @param propertyNames diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/grid/GridHiddenColumnRenderingTest.java b/vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/grid/GridHiddenColumnRenderingTest.java new file mode 100644 index 00000000000..df5117db76d --- /dev/null +++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/grid/GridHiddenColumnRenderingTest.java @@ -0,0 +1,411 @@ +/* + * Copyright 2000-2024 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.flow.component.grid; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.IntStream; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.data.provider.DataProvider; +import com.vaadin.flow.data.renderer.LitRenderer; +import com.vaadin.flow.data.renderer.Renderer; +import com.vaadin.flow.function.ValueProvider; +import com.vaadin.flow.server.VaadinSession; + +public class GridHiddenColumnRenderingTest { + + private static final int ITEM_COUNT = 10; + + private UI ui; + + private Grid grid; + + private AtomicInteger callCount; + + @Before + public void setup() { + ui = new UI(); + UI.setCurrent(ui); + var session = Mockito.mock(VaadinSession.class); + Mockito.when(session.hasLock()).thenReturn(true); + ui.getInternals().setSession(session); + grid = new Grid<>(); + grid.setItems(getItems()); + ui.add(grid); + fakeClientCommunication(); + callCount = new AtomicInteger(0); + } + + @After + public void tearDown() { + UI.setCurrent(null); + } + + @Test + public void generateDataWhenHiddenTrueByDefault() { + var column = grid.addColumn(s -> s); + Assert.assertTrue(column.isGenerateDataWhenHidden()); + } + + @Test + public void setGenerateDataWhenHidden_valueIsCorrectlySet() { + var column = grid.addColumn(s -> s).setGenerateDataWhenHidden(false); + Assert.assertFalse(column.isGenerateDataWhenHidden()); + column.setGenerateDataWhenHidden(true); + Assert.assertTrue(column.isGenerateDataWhenHidden()); + } + + @Test + public void dataGeneratorCalledOncePerItem() { + runTestCodeForMultipleColumns(column -> { + fakeClientCommunication(); + assertCalledOncePerItem(); + }); + } + + @Test + public void setGenerateDataWhenHiddenFalse_dataGeneratorCalledOncePerItem() { + runTestCodeForMultipleColumns(column -> { + column.setGenerateDataWhenHidden(false); + fakeClientCommunication(); + assertCalledOncePerItem(); + }); + } + + @Test + public void initiallyHiddenColumn_dataGeneratorCalledOncePerItem() { + runTestCodeForMultipleColumns(column -> { + column.setVisible(false); + fakeClientCommunication(); + assertCalledOncePerItem(); + }); + } + + @Test + public void initiallyHiddenColumn_setGenerateDataWhenHiddenFalse_dataGeneratorNotCalled() { + runTestCodeForMultipleColumns(column -> { + column.setVisible(false); + fakeClientCommunication(); + resetCallCount(); + column.setGenerateDataWhenHidden(false); + fakeClientCommunication(); + assertNotCalled(); + }); + } + + @Test + public void setHidden_dataGeneratorNotCalled() { + runTestCodeForMultipleColumns(column -> { + fakeClientCommunication(); + resetCallCount(); + column.setVisible(false); + fakeClientCommunication(); + assertNotCalled(); + }); + } + + @Test + public void setGenerateDataWhenHiddenFalse_setHidden_dataGeneratorNotCalled() { + runTestCodeForMultipleColumns(column -> { + column.setGenerateDataWhenHidden(false); + fakeClientCommunication(); + resetCallCount(); + column.setVisible(false); + fakeClientCommunication(); + assertNotCalled(); + }); + } + + @Test + public void initiallyHiddenColumn_setVisible_dataGeneratorNotCalled() { + runTestCodeForMultipleColumns(column -> { + column.setVisible(false); + fakeClientCommunication(); + resetCallCount(); + column.setVisible(true); + fakeClientCommunication(); + assertNotCalled(); + }); + } + + @Test + public void initiallyHiddenColumn_setGenerateDataWhenHiddenFalse_setVisible_dataGeneratorCalledOncePerItem() { + runTestCodeForMultipleColumns(column -> { + column.setGenerateDataWhenHidden(false); + column.setVisible(false); + fakeClientCommunication(); + column.setVisible(true); + fakeClientCommunication(); + assertCalledOncePerItem(); + }); + } + + @Test + public void toggleHiddenTwiceInRoundTrip_dataGeneratorNotCalled() { + runTestCodeForMultipleColumns(column -> { + fakeClientCommunication(); + column.setVisible(false); + column.setVisible(true); + resetCallCount(); + fakeClientCommunication(); + assertNotCalled(); + }); + } + + @Test + public void setGenerateDataWhenHiddenFalse_toggleHiddenTwiceInRoundTrip_dataGeneratorCalledOncePerItem() { + runTestCodeForMultipleColumns(column -> { + column.setGenerateDataWhenHidden(false); + fakeClientCommunication(); + column.setVisible(false); + column.setVisible(true); + resetCallCount(); + fakeClientCommunication(); + assertCalledOncePerItem(); + }); + } + + @Test + public void initiallyHiddenColumn_toggleHiddenTwiceInRoundTrip_dataGeneratorNotCalled() { + runTestCodeForMultipleColumns(column -> { + column.setVisible(false); + fakeClientCommunication(); + resetCallCount(); + column.setVisible(true); + column.setVisible(false); + fakeClientCommunication(); + assertNotCalled(); + }); + } + + @Test + public void initiallyHiddenColumn_setGenerateDataWhenHiddenFalse_toggleHiddenTwiceInRoundTrip_dataGeneratorNotCalled() { + runTestCodeForMultipleColumns(column -> { + column.setVisible(false); + fakeClientCommunication(); + resetCallCount(); + column.setGenerateDataWhenHidden(false); + column.setVisible(true); + column.setVisible(false); + fakeClientCommunication(); + assertNotCalled(); + }); + } + + @Test + public void detachAndReattachGrid_dataGeneratorCalledOncePerItem() { + runTestCodeForMultipleColumns(column -> { + ui.remove(grid); + fakeClientCommunication(); + resetCallCount(); + ui.add(grid); + fakeClientCommunication(); + assertCalledOncePerItem(); + }); + } + + @Test + public void setGenerateDataWhenHiddenFalse_detachAndReattachGrid_dataGeneratorCalledOncePerItem() { + runTestCodeForMultipleColumns(column -> { + column.setGenerateDataWhenHidden(false); + ui.remove(grid); + fakeClientCommunication(); + resetCallCount(); + ui.add(grid); + fakeClientCommunication(); + assertCalledOncePerItem(); + }); + } + + @Test + public void initiallyHiddenColumn_detachAndReattachGrid_dataGeneratorCalledOncePerItem() { + runTestCodeForMultipleColumns(column -> { + column.setVisible(false); + ui.remove(grid); + fakeClientCommunication(); + resetCallCount(); + ui.add(grid); + fakeClientCommunication(); + assertCalledOncePerItem(); + }); + } + + @Test + public void initiallyHiddenColumn_setGenerateDataWhenHiddenFalse_detachAndReattachGrid_dataGeneratorNotCalled() { + runTestCodeForMultipleColumns(column -> { + column.setGenerateDataWhenHidden(false); + column.setVisible(false); + ui.remove(grid); + fakeClientCommunication(); + ui.add(grid); + fakeClientCommunication(); + assertNotCalled(); + }); + } + + @Test + public void columnWithCustomRenderer_setAnotherRenderer_onlyNewRendererCalled() { + var column = addColumnWithCustomRenderer(); + fakeClientCommunication(); + resetCallCount(); + var newRendererCallCount = new AtomicInteger(0); + var newRenderer = LitRenderer + . of("${item.displayName}") + .withProperty("displayName", s -> { + newRendererCallCount.incrementAndGet(); + return s; + }); + column.setRenderer(newRenderer); + fakeClientCommunication(); + assertNotCalled(); + Assert.assertEquals(ITEM_COUNT, newRendererCallCount.get()); + } + + @Test + public void columnWithCustomRenderer_setGenerateDataWhenHiddenFalse_setAnotherRenderer_onlyNewRendererCalled() { + var column = addColumnWithCustomRenderer() + .setGenerateDataWhenHidden(false); + fakeClientCommunication(); + resetCallCount(); + var newRendererCallCount = new AtomicInteger(0); + var newRenderer = LitRenderer + . of("${item.displayName}") + .withProperty("displayName", s -> { + newRendererCallCount.incrementAndGet(); + return s; + }); + column.setRenderer(newRenderer); + fakeClientCommunication(); + assertNotCalled(); + Assert.assertEquals(ITEM_COUNT, newRendererCallCount.get()); + } + + @Test + public void addColumn_itemsSentOnlyOnce() { + var items = getItems(); + var fetchCount = new AtomicInteger(0); + var dataProvider = DataProvider. fromCallbacks(query -> { + fetchCount.incrementAndGet(); + return items.stream().skip(query.getOffset()) + .limit(query.getLimit()); + }, query -> items.size()); + grid.setDataProvider(dataProvider); + fakeClientCommunication(); + fetchCount.set(0); + addColumnWithValueProvider(); + fakeClientCommunication(); + Assert.assertEquals(1, fetchCount.get()); + } + + @Test + public void addColumn_setGenerateDataWhenHiddenFalse_itemsSentOnlyOnce() { + var items = getItems(); + var fetchCount = new AtomicInteger(0); + var dataProvider = DataProvider. fromCallbacks(query -> { + fetchCount.incrementAndGet(); + return items.stream().skip(query.getOffset()) + .limit(query.getLimit()); + }, query -> items.size()); + grid.setDataProvider(dataProvider); + fakeClientCommunication(); + fetchCount.set(0); + addColumnWithValueProvider().setGenerateDataWhenHidden(false); + fakeClientCommunication(); + Assert.assertEquals(1, fetchCount.get()); + } + + private void runTestCodeForMultipleColumns( + Consumer> testCode) { + getColumnSuppliers().forEach(columnSupplier -> { + resetCallCount(); + var column = columnSupplier.get(); + testCode.accept(column); + grid.removeColumn(column); + }); + } + + private List>> getColumnSuppliers() { + return List.of(this::addColumnWithValueProvider, + this::addComponentColumn, this::addColumnWithCustomRenderer); + } + + private Grid.Column addColumnWithValueProvider() { + return grid.addColumn(getValueProvider()); + } + + private Grid.Column addComponentColumn() { + return grid.addComponentColumn(this::getComponent); + } + + private Grid.Column addColumnWithCustomRenderer() { + return grid.addColumn(getCustomRenderer()); + } + + private ValueProvider getValueProvider() { + return s -> { + callCount.incrementAndGet(); + return s; + }; + } + + private Component getComponent(String string) { + callCount.incrementAndGet(); + return new NativeButton(string); + } + + private Renderer getCustomRenderer() { + return LitRenderer. of("
${item.displayName}
") + .withProperty("displayName", getValueProvider()); + } + + private List getItems() { + return IntStream.range(0, ITEM_COUNT).mapToObj(i -> "Item " + i) + .toList(); + } + + private void fakeClientCommunication() { + ui.getInternals().getStateTree().runExecutionsBeforeClientResponse(); + ui.getInternals().getStateTree().collectChanges(ignore -> { + }); + } + + private void assertNotCalled() { + Assert.assertEquals(0, getCallCount()); + } + + private void assertCalledOncePerItem() { + Assert.assertEquals(ITEM_COUNT, getCallCount()); + } + + private int getCallCount() { + return callCount.get(); + } + + private void resetCallCount() { + callCount.set(0); + } +} diff --git a/vaadin-grid-pro-flow-parent/vaadin-grid-pro-flow/src/main/java/com/vaadin/flow/component/gridpro/GridPro.java b/vaadin-grid-pro-flow-parent/vaadin-grid-pro-flow/src/main/java/com/vaadin/flow/component/gridpro/GridPro.java index 0d2b6d7cdd5..d75ccf7be7d 100644 --- a/vaadin-grid-pro-flow-parent/vaadin-grid-pro-flow/src/main/java/com/vaadin/flow/component/gridpro/GridPro.java +++ b/vaadin-grid-pro-flow-parent/vaadin-grid-pro-flow/src/main/java/com/vaadin/flow/component/gridpro/GridPro.java @@ -186,9 +186,10 @@ private Object getItemId(E item) { * Server-side component for the {@code } element. * *

- * Every added column sends data to the client side regardless of its - * visibility state. Don't add a new column at all or use - * {@link GridPro#removeColumn(Column)} to avoid sending extra data. + * By default, every added column sends data to the client side regardless + * of its visibility state. To avoid sending extra data, either remove the + * column using {@link #removeColumn(Column)} or use + * {@link Column#setGenerateDataWhenHidden(boolean)}. * * @param * type of the underlying grid this column is compatible with