From 525e32bb718b1be76b2ba67fef6970f05c402fe9 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Fri, 17 Jul 2026 15:46:04 +0400 Subject: [PATCH 1/3] refactor: allow grid header and footer renderers to run multiple times The grid web component is moving to rendering its header and footer declaratively with Lit (vaadin/web-components#12134). With that change, header and footer cells can be re-created, which re-runs the column renderers. Rendering only once would leave re-created cells empty. The sorter element is cached between runs because re-creating it would reset the sort direction and notify the server. Co-Authored-By: Claude Fable 5 --- .../META-INF/frontend/gridConnector.ts | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/frontend/gridConnector.ts b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/frontend/gridConnector.ts index 95740fbe6f6..61918d65ef8 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/frontend/gridConnector.ts +++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/frontend/gridConnector.ts @@ -8,15 +8,6 @@ function isRangeEqual(range1, range2) { return range1?.[0] === range2?.[0] && range1?.[1] === range2?.[1]; } -function singleTimeRenderer(renderer) { - return (root) => { - if (renderer) { - renderer(root); - renderer = null; - } - }; -} - function renderContent(root, content) { if (content instanceof Node) { root.appendChild(content); @@ -430,11 +421,15 @@ window.Vaadin.Flow.gridConnector.initLazy = (grid) => { return; } - column.headerRenderer = singleTimeRenderer((root) => { - // Render sorter + // The renderer can run multiple times, e.g. when the grid re-creates + // header cells. The sorter is cached between runs: re-creating it would + // reset its direction and fire a sorters-changed round-trip to the server. + let sorter; + + column.headerRenderer = (root) => { let contentRoot = root; if (showSorter) { - const sorter = document.createElement('vaadin-grid-sorter'); + sorter ??= document.createElement('vaadin-grid-sorter'); sorter.setAttribute('path', sorterPath); root.appendChild(sorter); @@ -443,7 +438,7 @@ window.Vaadin.Flow.gridConnector.initLazy = (grid) => { } renderContent(contentRoot, content); - }); + }; }; // This method is overridden to prevent the grid web component from @@ -488,7 +483,7 @@ window.Vaadin.Flow.gridConnector.initLazy = (grid) => { return; } - column.footerRenderer = singleTimeRenderer((root) => renderContent(root, content)); + column.footerRenderer = (root) => renderContent(root, content); }; grid.addEventListener('vaadin-context-menu-before-open', function (e) { From 788cc03f2e3cc2a83a86c2080990d6b5a31ebd45 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Mon, 20 Jul 2026 10:16:29 +0400 Subject: [PATCH 2/3] test: cover sorter node content and reuse in grid header renderer --- .../test/grid-connector-header-footer.test.ts | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/grid-connector-header-footer.test.ts b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/grid-connector-header-footer.test.ts index 6554fda5bc5..52dadeb8610 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/grid-connector-header-footer.test.ts +++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/grid-connector-header-footer.test.ts @@ -66,14 +66,43 @@ describe('grid connector - header and footer renderers', () => { }); describe('sorter', () => { - it('should render content inside the sorter', async () => { + it('should render sorter', async () => { grid.$connector.setHeaderRenderer(column, { content: 'Name', showSorter: true, sorterPath: 'name' }); await nextFrame(); const sorter = getHeaderCellContent(column).querySelector('vaadin-grid-sorter')!; expect(sorter).to.exist; + expect(sorter.path).to.equal('name'); + }); + + it('should render text content inside sorter', async () => { + grid.$connector.setHeaderRenderer(column, { content: 'Name', showSorter: true, sorterPath: 'name' }); + await nextFrame(); + + const sorter = getHeaderCellContent(column).querySelector('vaadin-grid-sorter')!; expect(sorter.textContent).to.equal('Name'); }); + + it('should render node content inside sorter', async () => { + const span = document.createElement('span'); + span.textContent = 'Name'; + grid.$connector.setHeaderRenderer(column, { content: span, showSorter: true, sorterPath: 'name' }); + await nextFrame(); + + const sorter = getHeaderCellContent(column).querySelector('vaadin-grid-sorter')!; + expect(sorter.contains(span)).to.be.true; + }); + + it('should reuse sorter element when renderer runs again', async () => { + grid.$connector.setHeaderRenderer(column, { content: 'Name', showSorter: true, sorterPath: 'name' }); + await nextFrame(); + + const oldSorter = getHeaderCellContent(column).querySelector('vaadin-grid-sorter')!; + grid.requestContentUpdate(); + const newSorter = getHeaderCellContent(column).querySelector('vaadin-grid-sorter')!; + + expect(newSorter).to.equal(oldSorter); + }); }); }); From 09f44e2a997258d6a125e6590e438e391f711c72 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Mon, 20 Jul 2026 10:41:01 +0400 Subject: [PATCH 3/3] test: cover header and footer component move when adding rows Covers moving component content into a column group after the grid has rendered, which re-runs the header and footer renderers on re-created cells. Co-Authored-By: Claude Fable 5 --- .../grid/it/GridHeaderFooterRowPage.java | 7 ++++ .../grid/it/GridHeaderFooterRowIT.java | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/GridHeaderFooterRowPage.java b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/GridHeaderFooterRowPage.java index 08b8d115aff..46fa7223e38 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/GridHeaderFooterRowPage.java +++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/GridHeaderFooterRowPage.java @@ -97,6 +97,13 @@ public GridHeaderFooterRowPage() { button.setId("set-components-for-headers"); add(button); + button = new NativeButton("Set components for footers", + event -> grid.getFooterRows().stream() + .flatMap(row -> row.getCells().stream()) + .forEach(cell -> cell.setComponent(new Span("foo")))); + button.setId("set-components-for-footers"); + add(button); + button = new NativeButton("Set text for headers", event -> grid.getHeaderRows().stream() .flatMap(row -> row.getCells().stream()) diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/GridHeaderFooterRowIT.java b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/GridHeaderFooterRowIT.java index 37bbca9ee75..9112b9f9dec 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/GridHeaderFooterRowIT.java +++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/GridHeaderFooterRowIT.java @@ -116,6 +116,42 @@ public void addFootersAfterGridIsRendered_cellsAreRenderedInCorrectOrder() { assertFooterOrder(2, 0, 1); } + @Test + public void setHeaderComponent_appendHeader_componentIsPreserved() { + clickButton("append-header"); + clickButton("set-components-for-headers"); + clickButton("append-header"); + + List headerCells = getHeaderCells(); + Assert.assertEquals("Unexpected amount of header cells", 2, + headerCells.size()); + Assert.assertEquals( + "The first header row should contain the moved component", + "foo", + headerCells.get(0).getDomProperty("innerHTML")); + Assert.assertEquals( + "The appended header row should contain only its own text", "1", + headerCells.get(1).getDomProperty("innerHTML")); + } + + @Test + public void setFooterComponent_prependFooter_componentIsPreserved() { + clickButton("append-footer"); + clickButton("set-components-for-footers"); + clickButton("prepend-footer"); + + List footerCells = getFooterCells(); + Assert.assertEquals("Unexpected amount of footer cells", 2, + footerCells.size()); + Assert.assertEquals( + "The prepended footer row should contain only its own text", + "1", footerCells.get(0).getDomProperty("innerHTML")); + Assert.assertEquals( + "The last footer row should contain the moved component", + "foo", + footerCells.get(1).getDomProperty("innerHTML")); + } + @Test public void appendHeaderAfterGridIsRendered_lastHeaderIsEmpty() { clickButton("append-header");