diff --git a/README.md b/README.md index ec8109b6..cfd5432f 100644 --- a/README.md +++ b/README.md @@ -402,11 +402,11 @@ on locators. Use whichever fits — they search the same component tree. `find(Class)`, `findInView(Class)` and the typed locators all walk the same thing: the server-side component tree. A component that another component -renders per item does not exist until something renders it, and the content of -an overlay is attached only while the overlay is open. Neither is in the tree -until then — reach it through that component's tester instead. The lookup -returns an empty result rather than an error, so the failure reads as "the -component was never created". +renders per item is rendered into that component and not into the tree, and +the content of an overlay is attached only while the overlay is open. Neither +is reachable that way — reach it through that component's tester instead. The +lookup returns an empty result rather than an error, so the failure reads as +"the component was never created". ### Components rendered per item @@ -415,8 +415,9 @@ grid.addComponentColumn(person -> new Checkbox(person.isSubscriber())) .setKey("subscriber"); ``` -No checkbox exists until the renderer is asked to render a *specific* item, so -`find(Checkbox.class)` finds none. `GridTester` renders the cell on demand: +A grid renders that checkbox into the column, not into the grid, so +`find(Checkbox.class)` finds none of them, no matter how many rows are on +screen. `GridTester` hands out the one the grid rendered: ```java var checkbox = (Checkbox) test(grid).getCellComponent(0, "subscriber"); @@ -424,11 +425,18 @@ test(checkbox).click(); ``` - `getCellComponent(int row, int column)` / `getCellComponent(int row, String - columnKey)` — the component a `ComponentRenderer` column renders for a row. - Every call renders the cell again and attaches the new instance to the grid, - so asking twice for the same cell leaves two instances behind, and a later - `find()` reports both. Hold on to the component the tester returns instead of - asking for it again. + columnKey)` — the component the grid rendered for the cell, which is the one + the browser shows. Reading the same cell twice gives the same instance, and + the instance is replaced when the row is rendered anew, for example after + `refreshItem(...)`. A row the client has not asked for yet is scrolled into + view first, the way a user reaches it. A cell the grid does not render at + all, such as one in a hidden column, throws. +- `renderCellComponent(int row, int column)` / `renderCellComponent(int row, + String columnKey)` — renders the cell on its own, without the grid, and + attaches the copy to the grid so that it can be used. Every call renders the + cell again and leaves the copy behind, so a later `find()` reports every one + of them. It is for the cells the grid does not render, and for tests written + against the old behaviour of `getCellComponent`. - `getCellText(int row, int column)` — the text the cell sends to the client, for both value and component renderers. - `getLitRendererPropertyValue(...)` / `invokeLitRendererFunction(...)` — for diff --git a/junit6/src/main/java/com/vaadin/browserless/AbstractBrowserlessExtension.java b/junit6/src/main/java/com/vaadin/browserless/AbstractBrowserlessExtension.java index 13c113fe..5d0f0ed4 100644 --- a/junit6/src/main/java/com/vaadin/browserless/AbstractBrowserlessExtension.java +++ b/junit6/src/main/java/com/vaadin/browserless/AbstractBrowserlessExtension.java @@ -270,10 +270,10 @@ public T navigate(String location, *

* The query walks the server-side component tree. A component that another * component renders per item, such as the component a - * {@code ComponentRenderer} column renders for a grid row, does not exist - * until something renders it, and the content of an overlay, such as a - * context menu, is attached only while the overlay is open. Neither is in - * the tree until then, and the lookup returns an empty result rather than + * {@code ComponentRenderer} column renders for a grid row, is rendered into + * the column and not into the tree, and the content of an overlay, such as + * a context menu, is attached only while the overlay is open. Neither is + * reachable this way, and the lookup returns an empty result rather than * failing, so reach those components through the owning component tester * instead: {@code GridTester.getCellComponent(row, column)} for grid cells, * {@code ContextMenuTester.open()} and then {@code clickItem(...)} for a diff --git a/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridTesterTest.java b/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridTesterTest.java index 823880ba..1bf9d5d2 100644 --- a/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridTesterTest.java +++ b/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridTesterTest.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.IntStream; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -374,23 +375,115 @@ void getCellComponent_columnByKey_returnsInstantiatedComponent() { } @Test - void find_componentRenderedIntoCell_notInTreeButReachableThroughTester() { - // A ComponentRenderer component does not exist until the renderer is - // asked to render a specific item, so there is nothing for find() to - // walk into. + void getCellComponent_readTwice_returnsTheComponentTheGridRendered() { + // A ComponentRenderer component is created when the grid renders the + // row for the client, and it is rendered into the column rather than + // into the grid, so there is nothing for find() to walk into. Assertions.assertEquals(0, find(Button.class).all().size(), "a component rendered into a grid cell should not be reachable through find()"); final Component cellComponent = test(view.basicGrid).getCellComponent(1, BasicGridView.BUTTON_KEY); - final Component renderedAgain = test(view.basicGrid).getCellComponent(1, + final Component readAgain = test(view.basicGrid).getCellComponent(1, BasicGridView.BUTTON_KEY); Assertions.assertInstanceOf(Button.class, cellComponent); - Assertions.assertNotSame(cellComponent, renderedAgain, + Assertions.assertSame(cellComponent, readAgain, + "reading the same cell twice should give the one component the grid renders for it"); + Assertions.assertEquals(0, find(Button.class).all().size(), + "reading a cell should not attach a component to the grid"); + } + + @Test + void getCellComponent_itemRefreshed_returnsTheComponentRenderedAnew() { + final Component beforeRefresh = test(view.basicGrid).getCellComponent(1, + BasicGridView.BUTTON_KEY); + + view.basicGrid.getListDataView().refreshItem(view.person2); + + final Component afterRefresh = test(view.basicGrid).getCellComponent(1, + BasicGridView.BUTTON_KEY); + Assertions.assertNotSame(beforeRefresh, afterRefresh, + "a refreshed row is rendered anew, so its cell has a new component"); + Assertions.assertFalse(beforeRefresh.isAttached(), + "the component the refreshed row replaced should be gone"); + Assertions.assertTrue(afterRefresh.isAttached(), + "the component of the refreshed row should be the rendered one"); + } + + @Test + void getCellComponent_rowOutsideRenderedRange_scrollsItIntoView() { + // the client has only asked for the first row + view.basicGrid.setPageSize(1); + + final Component cellComponent = test(view.basicGrid).getCellComponent(1, + 4); + + Assertions.assertInstanceOf(Button.class, cellComponent); + Assertions.assertSame(cellComponent, + test(view.basicGrid).getCellComponent(1, 4), + "the row scrolled into view should stay rendered"); + } + + @Test + void getCellComponent_hiddenColumn_throwsAndSuggestsRendering() { + GridTester, Person> grid_ = test(view.basicGrid); + + // the browser shows no cell for a hidden column, so the grid renders + // no component for it + Assertions.assertThrows(IllegalStateException.class, () -> grid_ + .getCellComponent(1, BasicGridView.HIDDEN_BUTTON_KEY)); + + Assertions.assertInstanceOf(Button.class, + grid_.renderCellComponent(1, BasicGridView.HIDDEN_BUTTON_KEY)); + } + + @Test + void getCellComponent_itemsNotEqualAcrossFetches_throwsAndSuggestsRendering() { + // a data provider that hands out a new item instance on every fetch: + // the grid cannot tell that the item on the row is the item it + // rendered the row for + final Grid lazyGrid = new Grid<>(); + lazyGrid.addComponentColumn(person -> new Button(person.getFirstName())) + .setKey(BasicGridView.BUTTON_KEY); + lazyGrid.setItems(query -> IntStream + .range(query.getOffset(), query.getOffset() + query.getLimit()) + .mapToObj(index -> { + final Person person = new Person(); + person.setFirstName("Person " + index); + return person; + }), query -> 100); + view.add(lazyGrid); + + GridTester, Person> lazyGrid_ = test(lazyGrid); + final IllegalStateException exception = Assertions.assertThrows( + IllegalStateException.class, + () -> lazyGrid_.getCellComponent(0, BasicGridView.BUTTON_KEY)); + Assertions.assertTrue( + exception.getMessage().contains("Grid rendered no component") + && exception.getMessage() + .contains("renderCellComponent"), + "the failure should say the grid rendered nothing and point at the way to render the cell anyway"); + + Assertions.assertInstanceOf(Button.class, + lazyGrid_.renderCellComponent(0, BasicGridView.BUTTON_KEY)); + } + + @Test + void renderCellComponent_rendersACopyAndAttachesItToTheGrid() { + final Component rendered = test(view.basicGrid).renderCellComponent(1, + 4); + final Component renderedAgain = test(view.basicGrid) + .renderCellComponent(1, 4); + + Assertions.assertNotSame(rendered, renderedAgain, "every call should render the cell anew"); Assertions.assertEquals(2, find(Button.class).all().size(), - "every rendered instance is attached to the grid and found from then on"); + "every rendered copy is attached to the grid and found from then on"); + Assertions.assertNotSame(rendered, + test(view.basicGrid).getCellComponent(1, + BasicGridView.BUTTON_KEY), + "a rendered copy is not the component the grid shows"); } @Test diff --git a/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridView.java b/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridView.java index 03d37908..c5cb8577 100644 --- a/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridView.java +++ b/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridView.java @@ -34,6 +34,7 @@ public class BasicGridView extends Component implements HasComponents { static final String SUBSCRIBER_KEY = "Subscriber"; static final String DECEASED_KEY = "Deceased"; static final String BUTTON_KEY = "Button"; + static final String HIDDEN_BUTTON_KEY = "Hidden Button"; final Grid basicGrid; final Person person1; @@ -57,6 +58,9 @@ public BasicGridView() { .addComponentColumn(person -> new Button("Click", e -> Notification.show("Clicked!"))) .setKey(BUTTON_KEY).setHeader("Button"); + basicGrid.addComponentColumn(person -> new Button("Hidden")) + .setKey(HIDDEN_BUTTON_KEY).setHeader("Hidden Button") + .setVisible(false); add(basicGrid); diff --git a/junit6/src/test/java/com/vaadin/flow/component/grid/GetTextCellRendererTest.java b/junit6/src/test/java/com/vaadin/flow/component/grid/GetTextCellRendererTest.java index 0358dd2e..5115d9fe 100644 --- a/junit6/src/test/java/com/vaadin/flow/component/grid/GetTextCellRendererTest.java +++ b/junit6/src/test/java/com/vaadin/flow/component/grid/GetTextCellRendererTest.java @@ -21,6 +21,7 @@ import com.vaadin.browserless.BrowserlessTest; import com.vaadin.browserless.ViewPackages; +import com.vaadin.flow.component.Text; import com.vaadin.flow.router.RouteConfiguration; @ViewPackages @@ -47,8 +48,17 @@ void getCellText_componentRenderer_getTextRecursively() { } @Test - void getCellText_renderNull_getsNull() { - Assertions.assertNull(grid_.getCellText(0, 1)); + void getCellText_renderNull_getsEmptyString() { + // a renderer that returns no component renders an empty cell: the + // grid puts an empty text node in its place + Assertions.assertEquals("", grid_.getCellText(0, 1)); + } + + @Test + void getCellComponent_renderNull_getsTheEmptyTextTheGridRenders() { + // the grid renders an empty text node where the renderer produced + // nothing, so there is a component to hand out + Assertions.assertInstanceOf(Text.class, grid_.getCellComponent(0, 1)); } @Test diff --git a/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java b/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java index f936c3fb..274f39b2 100644 --- a/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java +++ b/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java @@ -474,10 +474,10 @@ public , Y extends Component> T test( *

* The query walks the server-side component tree. A component that another * component renders per item, such as the component a - * {@code ComponentRenderer} column renders for a grid row, does not exist - * until something renders it, and the content of an overlay, such as a - * context menu, is attached only while the overlay is open. Neither is in - * the tree until then, and the lookup returns an empty result rather than + * {@code ComponentRenderer} column renders for a grid row, is rendered into + * the column and not into the tree, and the content of an overlay, such as + * a context menu, is attached only while the overlay is open. Neither is + * reachable this way, and the lookup returns an empty result rather than * failing, so reach those components through the owning component tester * instead: {@code GridTester.getCellComponent(row, column)} for grid cells, * {@code ContextMenuTester.open()} and then {@code clickItem(...)} for a diff --git a/shared/src/main/java/com/vaadin/browserless/BrowserlessUIContext.java b/shared/src/main/java/com/vaadin/browserless/BrowserlessUIContext.java index f4ddd451..f186da9c 100644 --- a/shared/src/main/java/com/vaadin/browserless/BrowserlessUIContext.java +++ b/shared/src/main/java/com/vaadin/browserless/BrowserlessUIContext.java @@ -316,10 +316,10 @@ public static BrowserlessUIContext forComponent( *

* The query walks the server-side component tree. A component that another * component renders per item, such as the component a - * {@code ComponentRenderer} column renders for a grid row, does not exist - * until something renders it, and the content of an overlay, such as a - * context menu, is attached only while the overlay is open. Neither is in - * the tree until then, and the lookup returns an empty result rather than + * {@code ComponentRenderer} column renders for a grid row, is rendered into + * the column and not into the tree, and the content of an overlay, such as + * a context menu, is attached only while the overlay is open. Neither is + * reachable this way, and the lookup returns an empty result rather than * failing, so reach those components through the owning component tester * instead: {@code GridTester.getCellComponent(row, column)} for grid cells, * {@code ContextMenuTester.open()} and then {@code clickItem(...)} for a diff --git a/shared/src/main/java/com/vaadin/browserless/ComponentTester.java b/shared/src/main/java/com/vaadin/browserless/ComponentTester.java index dc40ebc4..9f0f6a91 100644 --- a/shared/src/main/java/com/vaadin/browserless/ComponentTester.java +++ b/shared/src/main/java/com/vaadin/browserless/ComponentTester.java @@ -154,10 +154,10 @@ public void setModal(boolean modal) { *

* The query walks the server-side component tree. A component that another * component renders per item, such as the component a - * {@code ComponentRenderer} column renders for a grid row, does not exist - * until something renders it, and the content of an overlay, such as a - * context menu, is attached only while the overlay is open. Neither is in - * the tree until then, and the lookup returns an empty result rather than + * {@code ComponentRenderer} column renders for a grid row, is rendered into + * the column and not into the tree, and the content of an overlay, such as + * a context menu, is attached only while the overlay is open. Neither is + * reachable this way, and the lookup returns an empty result rather than * failing, so reach those components through the owning component tester * instead: {@code GridTester.getCellComponent(row, column)} for grid cells, * {@code ContextMenuTester.open()} and then {@code clickItem(...)} for a diff --git a/shared/src/main/java/com/vaadin/browserless/internal/RenderedComponentSupport.java b/shared/src/main/java/com/vaadin/browserless/internal/RenderedComponentSupport.java new file mode 100644 index 00000000..eb4261ba --- /dev/null +++ b/shared/src/main/java/com/vaadin/browserless/internal/RenderedComponentSupport.java @@ -0,0 +1,109 @@ +/* + * Copyright 2000-2026 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.browserless.internal; + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.Map; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.data.provider.AbstractComponentDataGenerator; +import com.vaadin.flow.data.provider.CompositeDataGenerator; +import com.vaadin.flow.data.provider.DataGenerator; +import com.vaadin.flow.data.renderer.ComponentRenderer; +import com.vaadin.flow.internal.ReflectTools; + +/** + * Reaches the components a {@link ComponentRenderer} has rendered for the rows + * the client has asked for. + *

+ * For internal use only. + */ +public final class RenderedComponentSupport { + + private RenderedComponentSupport() { + } + + /** + * Gets the component the column has rendered for the item with the given + * key. + *

+ * A component renderer keeps one component per item for as long as the + * client has the row, and that is the component the browser shows. Flow + * exposes no API for it: the mapping lives in + * {@code AbstractComponentDataGenerator.renderedComponents}, and the + * generator itself is reachable only through the private data generators of + * the column, so both are read reflectively. The accessor belongs upstream, + * on {@code Grid.Column} in {@code vaadin/flow-components} or on + * {@link AbstractComponentDataGenerator} in {@code vaadin/flow}. + * + * @param column + * the column that renders the cell + * @param itemKey + * key of the item the cell shows + * @return the rendered component, or {@literal null} when the column has + * rendered no component for the item + */ + public static Component getRenderedComponent(Grid.Column column, + String itemKey) { + DataGenerator columnDataGenerator = (DataGenerator) read( + Grid.Column.class, "compositeDataGenerator", column); + AbstractComponentDataGenerator generator = findComponentDataGenerator( + columnDataGenerator); + if (generator == null) { + return null; + } + if (read(AbstractComponentDataGenerator.class, "renderedComponents", + generator) instanceof Map rendered) { + return (Component) rendered.get(itemKey); + } + return null; + } + + private static AbstractComponentDataGenerator findComponentDataGenerator( + DataGenerator generator) { + if (generator instanceof AbstractComponentDataGenerator componentDataGenerator) { + return componentDataGenerator; + } + if (generator instanceof CompositeDataGenerator composite + && read(CompositeDataGenerator.class, "dataGenerators", + composite) instanceof Collection nested) { + for (Object child : nested) { + AbstractComponentDataGenerator found = findComponentDataGenerator( + (DataGenerator) child); + if (found != null) { + return found; + } + } + } + return null; + } + + private static Object read(Class owner, String fieldName, + Object instance) { + Field field = ReflectTools.findDeclaredField(owner, fieldName) + .orElseThrow(() -> new IllegalStateException("Unable to find " + + owner.getSimpleName() + "." + fieldName)); + try { + return field.get(instance); + } catch (IllegalAccessException e) { + throw new IllegalStateException( + "Unable to read " + owner.getSimpleName() + "." + fieldName, + e); + } + } +} diff --git a/shared/src/main/java/com/vaadin/flow/component/grid/GridTester.java b/shared/src/main/java/com/vaadin/flow/component/grid/GridTester.java index ef83e5d1..858aafb3 100644 --- a/shared/src/main/java/com/vaadin/flow/component/grid/GridTester.java +++ b/shared/src/main/java/com/vaadin/flow/component/grid/GridTester.java @@ -30,9 +30,11 @@ import com.vaadin.browserless.Tests; import com.vaadin.browserless.component.GridKt; import com.vaadin.browserless.internal.GridContextMenuSupport; +import com.vaadin.browserless.internal.RenderedComponentSupport; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.grid.contextmenu.GridContextMenu; import com.vaadin.flow.component.grid.contextmenu.GridContextMenuTester; +import com.vaadin.flow.data.provider.DataCommunicator; import com.vaadin.flow.data.provider.SortDirection; import com.vaadin.flow.data.provider.SortOrder; import com.vaadin.flow.data.renderer.ComponentRenderer; @@ -298,8 +300,12 @@ public void deselectAll() { * For the default renderer ColumnPathRenderer the result is the sent text * for defined object path. *

- * For a ComponentRenderer the result is the rendered component as - * prettyString. + * For a ComponentRenderer the result is the text of the component the grid + * rendered for the cell, read through {@link #getCellComponent(int, int)}: + * a row the client has not asked for yet is scrolled into view first, and a + * cell the grid renders no component for fails the same way. A renderer + * that returns no component renders an empty cell, so the text is empty + * rather than {@literal null}. *

* More to be added as we find other renderers that need handling. * @@ -309,17 +315,15 @@ public void deselectAll() { * column of cell * @return cell content that is sent to the client * @throws IllegalStateException - * if component is not visible + * if component is not visible, or if the grid renders no + * component for a ComponentRenderer cell */ public String getCellText(int row, int column) { ensureVisible(); final Grid.Column targetColumn = getColumns().get(column); if (targetColumn.getRenderer() instanceof ComponentRenderer) { - Component component = getCellComponent(row, column); - if (component == null) { - return null; - } - return component.getElement().getTextRecursively(); + return getCellComponent(row, column).getElement() + .getTextRecursively(); } else if (targetColumn.getRenderer() instanceof ColumnPathRenderer) { // This renderer just writes the object text using a path return getValueProviderString(row, targetColumn); @@ -328,77 +332,205 @@ public String getCellText(int row, int column) { } /** - * Get component for item in cell. + * Get the component the grid renders for the cell in the given position. * *

- * A component renderer only produces a component when it is asked to render - * a specific item, so until this method is called there is nothing in the + * A component renderer only produces a component when the grid renders a + * row for the client, so a renderer component is never part of the * component tree that {@code find(...)} walks, and this method is the way - * to reach it. Every call renders the cell again and attaches the new - * instance to the grid, so asking twice for the same cell leaves two - * instances behind and a later {@code find(...)} reports both. Hold on to - * the component this method returns instead of asking for it again. + * to reach it. What comes back is the very component the browser shows: + * asking for the same cell twice returns the same instance, and the + * instance is replaced when the grid re-renders the row, for example after + * {@code refreshItem(...)}. A row the client has not asked for yet is + * scrolled into view first, the way a user reaches it. + * + *

+ * Use {@link #renderCellComponent(int, int)} to render a cell on its own, + * without the grid. * * @param row * item row * @param column * column to get - * @return initialized component for the targeted cell + * @return the component the grid rendered for the targeted cell * @throws IllegalArgumentException * when the target column of the cell is not a component * renderer + * @throws IllegalStateException + * when the grid renders no component for the cell */ public Component getCellComponent(int row, int column) { ensureVisible(); final Grid.Column yColumn = getColumns().get(column); - return getRendererItem(row, yColumn); + return getRenderedCellComponent(row, yColumn); } /** - * Get component for item in column. + * Get the component the grid renders for the cell in the given row of the + * column with the given key. * *

- * A component renderer only produces a component when it is asked to render - * a specific item, so until this method is called there is nothing in the + * A component renderer only produces a component when the grid renders a + * row for the client, so a renderer component is never part of the * component tree that {@code find(...)} walks, and this method is the way - * to reach it. Every call renders the cell again and attaches the new - * instance to the grid, so asking twice for the same cell leaves two - * instances behind and a later {@code find(...)} reports both. Hold on to - * the component this method returns instead of asking for it again. + * to reach it. What comes back is the very component the browser shows: + * asking for the same cell twice returns the same instance, and the + * instance is replaced when the grid re-renders the row, for example after + * {@code refreshItem(...)}. A row the client has not asked for yet is + * scrolled into view first, the way a user reaches it. + * + *

+ * Use {@link #renderCellComponent(int, String)} to render a cell on its + * own, without the grid. * * @param row * item row * @param columnName * key/property of column - * @return initialized component for the target cell + * @return the component the grid rendered for the target cell * @throws IllegalArgumentException * when column for property doesn't exist or the target column * of the cell is not a component renderer + * @throws IllegalStateException + * when the grid renders no component for the cell, which is the + * case for a hidden column */ public Component getCellComponent(int row, String columnName) { ensureVisible(); - if (getComponent().getColumnByKey(columnName) == null) { + return getRenderedCellComponent(row, getColumnByKey(columnName)); + } + + /** + * Render the component for the cell in the given position on its own, + * without the grid, and attach it to the grid so that it can be used. + * + *

+ * This asks the column's component renderer for a component for the item on + * the row, which is not the instance the grid renders for the client: every + * call renders the cell again and attaches the new instance to the grid, so + * asking twice for the same cell leaves two instances behind and a later + * {@code find(...)} reports both. + * + *

+ * Prefer {@link #getCellComponent(int, int)}, which returns the component + * the browser shows. This method is for tests written against the older + * behaviour of that method. A column index addresses the visible columns, + * so the cells the grid renders nothing for, those of a hidden column, are + * only reachable through {@link #renderCellComponent(int, String)}. + * + * @param row + * item row + * @param column + * column to render + * @return a freshly rendered component for the targeted cell + * @throws IllegalArgumentException + * when the target column of the cell is not a component + * renderer + */ + public Component renderCellComponent(int row, int column) { + ensureVisible(); + return getRendererItem(row, getColumns().get(column)); + } + + /** + * Render the component for the cell in the given row of the column with the + * given key on its own, without the grid, and attach it to the grid so that + * it can be used. + * + *

+ * This asks the column's component renderer for a component for the item on + * the row, which is not the instance the grid renders for the client: every + * call renders the cell again and attaches the new instance to the grid, so + * asking twice for the same cell leaves two instances behind and a later + * {@code find(...)} reports both. + * + *

+ * Prefer {@link #getCellComponent(int, String)}, which returns the + * component the browser shows. This method is for the cases the grid does + * not render itself, such as a hidden column. + * + * @param row + * item row + * @param columnName + * key/property of column + * @return a freshly rendered component for the target cell + * @throws IllegalArgumentException + * when column for property doesn't exist or the target column + * of the cell is not a component renderer + */ + public Component renderCellComponent(int row, String columnName) { + ensureVisible(); + return getRendererItem(row, getColumnByKey(columnName)); + } + + private Grid.Column getColumnByKey(String columnName) { + final Grid.Column column = getComponent().getColumnByKey(columnName); + if (column == null) { throw new IllegalArgumentException( "No column for property '" + columnName + "' exists"); } + return column; + } + + private Component getRenderedCellComponent(int row, + Grid.Column yColumn) { + ensureComponentRenderer(yColumn); + if (!yColumn.isVisible()) { + throw new IllegalStateException("Column '" + yColumn.getKey() + + "' is not visible, so the grid renders no component for " + + "it. Use renderCellComponent to render the cell without " + + "the grid."); + } + // pending row rendering happens when the response is written + roundTrip(); + Component component = findRenderedCellComponent(row, yColumn); + if (component == null) { + // the client has not asked for the row yet, so the grid has not + // rendered it - bring it into the viewport as a user scrolling + // down to the row would + getComponent().scrollToIndex(row); + roundTrip(); + component = findRenderedCellComponent(row, yColumn); + } + if (component == null) { + throw new IllegalStateException("Grid rendered no component for " + + "row " + row + " in column '" + yColumn.getKey() + + "'. The item on the row is not the one the grid rendered," + + " which happens when the data provider returns items that" + + " are not equal across fetches. Use renderCellComponent" + + " to render the cell without the grid."); + } + return component; + } + + private Component findRenderedCellComponent(int row, + Grid.Column yColumn) { + final Y item = getRow(row); + final DataCommunicator dataCommunicator = getComponent() + .getDataCommunicator(); + if (!dataCommunicator.isItemActive(item)) { + return null; + } + return RenderedComponentSupport.getRenderedComponent(yColumn, + dataCommunicator.getKeyMapper().key(item)); + } - final Grid.Column yColumn = getComponent() - .getColumnByKey(columnName); - return getRendererItem(row, yColumn); + private void ensureComponentRenderer(Grid.Column yColumn) { + if (!(yColumn.getRenderer() instanceof ComponentRenderer)) { + throw new IllegalArgumentException( + "Target column doesn't have a ComponentRenderer."); + } } private Component getRendererItem(int row, Grid.Column yColumn) { - if (yColumn.getRenderer() instanceof ComponentRenderer) { - final Y item = getRow(row); - var component = ((ComponentRenderer) yColumn.getRenderer()) - .createComponent(item); - if (component != null) { - getComponent().getElement().appendChild(component.getElement()); - } - return component; + ensureComponentRenderer(yColumn); + final Y item = getRow(row); + var component = ((ComponentRenderer) yColumn.getRenderer()) + .createComponent(item); + if (component != null) { + getComponent().getElement().appendChild(component.getElement()); } - throw new IllegalArgumentException( - "Target column doesn't have a ComponentRenderer."); + return component; } private V getLitRendererPropertyValue(int row, Grid.Column column,