Skip to content

Commit ef666b2

Browse files
fix: AppTable remounting children on table state updates (#6331)
* fix: AppTable remounting children on table state updates (#6323) App wrappers (AppTable, AppCell, AppHeader, AppFooter) were memoized on the unstable `table` reference, causing them to be recreated every render. React then remounted their entire subtrees on every state change, destroying controlled input focus. Keep wrappers stable (created once) and read the current table from a ref updated each render. Fixes children remounting while preserving latest table state access. * also apply fix to preact * modify composable table examples to make this easier to test * update other composable table examples --------- Co-authored-by: Kevin Van Cott <kevinvandy656@gmail.com>
1 parent 26fc105 commit ef666b2

42 files changed

Lines changed: 1824 additions & 1104 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/angular/composable-tables/src/app/components/cell-components.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, computed } from '@angular/core'
1+
import { ChangeDetectionStrategy, Component, computed } from '@angular/core'
22
import { injectFlexRenderContext } from '@tanstack/angular-table'
33
import { CurrencyPipe } from '@angular/common'
44
import { injectTableCellContext } from '../table'
@@ -20,6 +20,27 @@ export class TextCell {
2020
injectFlexRenderContext<CellContext<TableFeatures, any, string>>()
2121
}
2222

23+
@Component({
24+
selector: 'table-select-cell',
25+
host: {
26+
class: 'selection-cell',
27+
},
28+
template: `
29+
<input
30+
type="checkbox"
31+
[checked]="row().getIsSelected()"
32+
[disabled]="!row().getCanSelect()"
33+
[indeterminate]="row().getIsSomeSelected()"
34+
(change)="row().getToggleSelectedHandler()($event)"
35+
/>
36+
`,
37+
changeDetection: ChangeDetectionStrategy.OnPush,
38+
})
39+
export class SelectCell {
40+
readonly cell = injectTableCellContext()
41+
readonly row = computed(() => this.cell().row)
42+
}
43+
2344
@Component({
2445
selector: 'span',
2546
host: {

examples/angular/composable-tables/src/app/components/header-components.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
// )
1010
// }
1111

12-
import { Component, computed } from '@angular/core'
12+
import { ChangeDetectionStrategy, Component, computed } from '@angular/core'
1313
import { flexRenderComponent } from '@tanstack/angular-table'
1414
import { FormsModule } from '@angular/forms'
1515
import { injectTableHeaderContext } from '../table'
@@ -24,6 +24,26 @@ export function SortIndicator(): string | null {
2424
return `<span class="sort-indicator">${sorted === 'asc' ? '🔼' : '🔽'}</span>`
2525
}
2626

27+
@Component({
28+
selector: 'table-select-header',
29+
host: {
30+
class: 'selection-cell',
31+
},
32+
template: `
33+
<input
34+
type="checkbox"
35+
[checked]="table().getIsAllRowsSelected()"
36+
[indeterminate]="table().getIsSomeRowsSelected()"
37+
(change)="table().getToggleAllRowsSelectedHandler()($event)"
38+
/>
39+
`,
40+
changeDetection: ChangeDetectionStrategy.OnPush,
41+
})
42+
export class SelectHeader {
43+
readonly header = injectTableHeaderContext()
44+
readonly table = computed(() => this.header().getContext().table)
45+
}
46+
2747
export function ColumnFilter(): FlexRenderComponent | null {
2848
const header = injectTableHeaderContext()
2949
if (!header().column.getCanFilter()) return null

examples/angular/composable-tables/src/app/components/products-table/products-table.html

Lines changed: 56 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,65 +6,67 @@
66
"
77
/>
88

9-
<table>
10-
<thead>
11-
@for (headerGroup of table.getHeaderGroups(); track headerGroup.id) {
12-
<tr>
13-
@for (_header of headerGroup.headers; track _header.id) {
14-
@let header = table.appHeader(_header);
15-
@if (!header.isPlaceholder) {
16-
<th [tanStackTableHeader]="header" (click)="header.column.toggleSorting()">
17-
<ng-container *flexRenderHeader="header; let header">
18-
{{ header }}
19-
</ng-container>
9+
<div class="table-scroll">
10+
<table>
11+
<thead>
12+
@for (headerGroup of table.getHeaderGroups(); track headerGroup.id) {
13+
<tr>
14+
@for (_header of headerGroup.headers; track _header.id) {
15+
@let header = table.appHeader(_header);
16+
@if (!header.isPlaceholder) {
17+
<th [tanStackTableHeader]="header" (click)="header.column.toggleSorting()">
18+
<ng-container *flexRenderHeader="header; let header">
19+
{{ header }}
20+
</ng-container>
2021

21-
<ng-container
22-
*flexRender="header.SortIndicator; props: header.getContext(); let value"
23-
>
24-
<div [innerHTML]="value"></div>
25-
</ng-container>
22+
<ng-container
23+
*flexRender="header.SortIndicator; props: header.getContext(); let value"
24+
>
25+
<div [innerHTML]="value"></div>
26+
</ng-container>
2627

27-
<ng-container
28-
*flexRender="header.ColumnFilter; props: header.getContext(); let value"
29-
>
30-
<div [innerHTML]="value"></div>
28+
<ng-container
29+
*flexRender="header.ColumnFilter; props: header.getContext(); let value"
30+
>
31+
<div [innerHTML]="value"></div>
32+
</ng-container>
33+
</th>
34+
}
35+
}
36+
</tr>
37+
}
38+
</thead>
39+
<tbody>
40+
@for (row of table.getRowModel().rows; track row.id) {
41+
<tr>
42+
@for (cell of row.getAllCells(); track cell.id) {
43+
<td [tanStackTableCell]="cell">
44+
<ng-container *flexRenderCell="cell; let cell">
45+
{{ cell }}
3146
</ng-container>
32-
</th>
47+
</td>
3348
}
34-
}
35-
</tr>
36-
}
37-
</thead>
38-
<tbody>
39-
@for (row of table.getRowModel().rows; track row.id) {
40-
<tr>
41-
@for (cell of row.getAllCells(); track cell.id) {
42-
<td [tanStackTableCell]="cell">
43-
<ng-container *flexRenderCell="cell; let cell">
44-
{{ cell }}
45-
</ng-container>
46-
</td>
47-
}
48-
</tr>
49-
}
50-
</tbody>
49+
</tr>
50+
}
51+
</tbody>
5152

52-
<tfoot>
53-
@for (footerGroup of table.getFooterGroups(); track footerGroup.id) {
54-
<tr>
55-
@for (footer of footerGroup.headers; track footer.id) {
56-
<th [colSpan]="footer.colSpan" [tanStackTableHeader]="footer">
57-
@if (!footer.isPlaceholder) {
58-
<ng-container *flexRenderFooter="footer; let footer">
59-
{{ footer }}
60-
</ng-container>
61-
}
62-
</th>
63-
}
64-
</tr>
65-
}
66-
</tfoot>
67-
</table>
53+
<tfoot>
54+
@for (footerGroup of table.getFooterGroups(); track footerGroup.id) {
55+
<tr>
56+
@for (footer of footerGroup.headers; track footer.id) {
57+
<th [colSpan]="footer.colSpan" [tanStackTableHeader]="footer">
58+
@if (!footer.isPlaceholder) {
59+
<ng-container *flexRenderFooter="footer; let footer">
60+
{{ footer }}
61+
</ng-container>
62+
}
63+
</th>
64+
}
65+
</tr>
66+
}
67+
</tfoot>
68+
</table>
69+
</div>
6870

6971
<ng-container *ngComponentOutlet="table.PaginationControls" />
7072

examples/angular/composable-tables/src/app/components/products-table/products-table.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
TanStackTable,
66
TanStackTableCell,
77
TanStackTableHeader,
8+
flexRenderComponent,
89
} from '@tanstack/angular-table'
910
import { injectTanStackTableDevtools } from '@tanstack/angular-table-devtools'
1011
import { makeProductData } from '../../makeData'
@@ -34,6 +35,11 @@ export class ProductsTable {
3435
readonly data = signal(makeProductData(1_000))
3536

3637
readonly columns = productColumnHelper.columns([
38+
productColumnHelper.display({
39+
id: 'select',
40+
header: ({ header }) => flexRenderComponent(header.SelectHeader),
41+
cell: ({ cell }) => flexRenderComponent(cell.SelectCell),
42+
}),
3743
productColumnHelper.accessor('name', {
3844
header: 'Product Name',
3945
footer: (props) => props.column.id,
@@ -66,6 +72,7 @@ export class ProductsTable {
6672
columns: this.columns,
6773
data: this.data(),
6874
getRowId: (row) => row.id,
75+
enableRowSelection: true,
6976
// more table options
7077
}))
7178

examples/angular/composable-tables/src/app/components/users-table/users-table.html

Lines changed: 56 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,65 +6,67 @@
66
"
77
/>
88

9-
<table>
10-
<thead>
11-
@for (headerGroup of table.getHeaderGroups(); track headerGroup.id) {
12-
<tr>
13-
@for (_header of headerGroup.headers; track _header.id) {
14-
@let header = table.appHeader(_header);
15-
@if (!header.isPlaceholder) {
16-
<th [tanStackTableHeader]="header" (click)="header.column.toggleSorting()">
17-
<ng-container *flexRenderHeader="header; let header">
18-
{{ header }}
19-
</ng-container>
9+
<div class="table-scroll">
10+
<table>
11+
<thead>
12+
@for (headerGroup of table.getHeaderGroups(); track headerGroup.id) {
13+
<tr>
14+
@for (_header of headerGroup.headers; track _header.id) {
15+
@let header = table.appHeader(_header);
16+
@if (!header.isPlaceholder) {
17+
<th [tanStackTableHeader]="header" (click)="header.column.toggleSorting()">
18+
<ng-container *flexRenderHeader="header; let header">
19+
{{ header }}
20+
</ng-container>
2021

21-
<ng-container
22-
*flexRender="header.SortIndicator; props: header.getContext(); let value"
23-
>
24-
<div [innerHTML]="value"></div>
25-
</ng-container>
22+
<ng-container
23+
*flexRender="header.SortIndicator; props: header.getContext(); let value"
24+
>
25+
<div [innerHTML]="value"></div>
26+
</ng-container>
2627

27-
<ng-container
28-
*flexRender="header.ColumnFilter; props: header.getContext(); let value"
29-
>
30-
<div [innerHTML]="value"></div>
28+
<ng-container
29+
*flexRender="header.ColumnFilter; props: header.getContext(); let value"
30+
>
31+
<div [innerHTML]="value"></div>
32+
</ng-container>
33+
</th>
34+
}
35+
}
36+
</tr>
37+
}
38+
</thead>
39+
<tbody>
40+
@for (row of table.getRowModel().rows; track row.id) {
41+
<tr>
42+
@for (cell of row.getAllCells(); track cell.id) {
43+
<td [tanStackTableCell]="cell">
44+
<ng-container *flexRenderCell="cell; let cell">
45+
{{ cell }}
3146
</ng-container>
32-
</th>
47+
</td>
3348
}
34-
}
35-
</tr>
36-
}
37-
</thead>
38-
<tbody>
39-
@for (row of table.getRowModel().rows; track row.id) {
40-
<tr>
41-
@for (cell of row.getAllCells(); track cell.id) {
42-
<td [tanStackTableCell]="cell">
43-
<ng-container *flexRenderCell="cell; let cell">
44-
{{ cell }}
45-
</ng-container>
46-
</td>
47-
}
48-
</tr>
49-
}
50-
</tbody>
49+
</tr>
50+
}
51+
</tbody>
5152

52-
<tfoot>
53-
@for (footerGroup of table.getFooterGroups(); track footerGroup.id) {
54-
<tr>
55-
@for (footer of footerGroup.headers; track footer.id) {
56-
<th [colSpan]="footer.colSpan" [tanStackTableHeader]="footer">
57-
@if (!footer.isPlaceholder) {
58-
<ng-container *flexRenderFooter="footer; let footer">
59-
{{ footer }}
60-
</ng-container>
61-
}
62-
</th>
63-
}
64-
</tr>
65-
}
66-
</tfoot>
67-
</table>
53+
<tfoot>
54+
@for (footerGroup of table.getFooterGroups(); track footerGroup.id) {
55+
<tr>
56+
@for (footer of footerGroup.headers; track footer.id) {
57+
<th [colSpan]="footer.colSpan" [tanStackTableHeader]="footer">
58+
@if (!footer.isPlaceholder) {
59+
<ng-container *flexRenderFooter="footer; let footer">
60+
{{ footer }}
61+
</ng-container>
62+
}
63+
</th>
64+
}
65+
</tr>
66+
}
67+
</tfoot>
68+
</table>
69+
</div>
6870

6971
<ng-container *ngComponentOutlet="table.PaginationControls" />
7072

examples/angular/composable-tables/src/app/components/users-table/users-table.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ export class UsersTable {
3535
readonly data = signal(makeData(1_000))
3636

3737
readonly columns = personColumnHelper.columns([
38+
personColumnHelper.display({
39+
id: 'select',
40+
header: ({ header }) => flexRenderComponent(header.SelectHeader),
41+
cell: ({ cell }) => flexRenderComponent(cell.SelectCell),
42+
}),
3843
personColumnHelper.accessor('firstName', {
3944
header: 'First Name',
4045
footer: ({ header }) => flexRenderComponent(header.FooterColumnId),
@@ -77,6 +82,7 @@ export class UsersTable {
7782
columns: this.columns,
7883
data: this.data(),
7984
debugTable: true,
85+
enableRowSelection: true,
8086
// more table options
8187
}))
8288

0 commit comments

Comments
 (0)