Skip to content

Commit 3620fe1

Browse files
Copilothotlong
andcommitted
Add virtual scrolling support to plugin-grid and optimize plugin-aggrid
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 9abd298 commit 3620fe1

8 files changed

Lines changed: 299 additions & 2 deletions

File tree

packages/plugin-aggrid/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"@object-ui/core": "workspace:*",
3737
"@object-ui/react": "workspace:*",
3838
"@object-ui/types": "workspace:*",
39-
"@object-ui/data-objectstack": "workspace:*"
39+
"@object-ui/data-objectstack": "workspace:*",
40+
"@tanstack/react-virtual": "^3.11.3"
4041
},
4142
"peerDependencies": {
4243
"react": "^18.0.0 || ^19.0.0",

packages/plugin-aggrid/src/AgGridImpl.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,9 @@ export default function AgGridImpl({
241241
suppressCellFocus: !editable,
242242
enableCellTextSelection: true,
243243
ensureDomOrder: true,
244+
// Virtual scrolling optimizations for large datasets
245+
rowBuffer: gridOptions.rowBuffer ?? 10,
246+
debounceVerticalScrollbar: gridOptions.debounceVerticalScrollbar ?? (rowData.length > 1000),
244247
// Event handlers
245248
onCellClicked: handleCellClicked,
246249
onRowClicked: handleRowClicked,
@@ -263,6 +266,7 @@ export default function AgGridImpl({
263266
contextMenu,
264267
getContextMenuItems,
265268
editable,
269+
rowData.length,
266270
handleCellClicked,
267271
handleRowClicked,
268272
handleSelectionChanged,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* Virtual Scrolling in AG Grid
11+
*
12+
* AG Grid provides built-in virtual scrolling by default, which renders only
13+
* the visible rows in the viewport. This is a core feature of AG Grid and
14+
* requires no additional configuration.
15+
*
16+
* ## How It Works
17+
*
18+
* - AG Grid automatically virtualizes rows by rendering only visible rows
19+
* - As you scroll, rows are recycled and reused for new data
20+
* - This provides excellent performance even with datasets of 100,000+ rows
21+
*
22+
* ## Performance Tips
23+
*
24+
* 1. **Row Buffer**: Adjust `rowBuffer` to control how many extra rows are rendered
25+
* ```ts
26+
* gridOptions: { rowBuffer: 10 } // Render 10 extra rows above/below viewport
27+
* ```
28+
*
29+
* 2. **Suppress Animations**: Disable animations for very large datasets
30+
* ```ts
31+
* animateRows: false
32+
* ```
33+
*
34+
* 3. **Debounce Vertical Scroll**: Add delay to vertical scroll updates
35+
* ```ts
36+
* gridOptions: { debounceVerticalScrollbar: true }
37+
* ```
38+
*
39+
* 4. **Row Height**: Use fixed row heights for better performance
40+
* ```ts
41+
* gridOptions: { rowHeight: 40 }
42+
* ```
43+
*
44+
* ## Example Usage
45+
*
46+
* ```tsx
47+
* <AgGrid
48+
* rowData={largeDataset} // 10,000+ items
49+
* columnDefs={columns}
50+
* gridOptions={{
51+
* rowBuffer: 10,
52+
* rowHeight: 40,
53+
* debounceVerticalScrollbar: true,
54+
* }}
55+
* animateRows={false}
56+
* />
57+
* ```
58+
*
59+
* ## References
60+
*
61+
* - [AG Grid Row Virtualisation](https://www.ag-grid.com/javascript-data-grid/dom-virtualisation/)
62+
* - [Performance Best Practices](https://www.ag-grid.com/javascript-data-grid/performance/)
63+
*/
64+
65+
export const VIRTUAL_SCROLLING_DOCS = {
66+
enabled: true,
67+
automatic: true,
68+
recommendedSettings: {
69+
rowBuffer: 10,
70+
rowHeight: 40,
71+
debounceVerticalScrollbar: true,
72+
animateRows: false,
73+
},
74+
};

packages/plugin-grid/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"@object-ui/fields": "workspace:*",
2626
"@object-ui/react": "workspace:*",
2727
"@object-ui/types": "workspace:*",
28+
"@tanstack/react-virtual": "^3.11.3",
2829
"lucide-react": "^0.563.0"
2930
},
3031
"peerDependencies": {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { describe, it, expect } from 'vitest';
10+
import { VirtualGrid } from './VirtualGrid';
11+
12+
describe('VirtualGrid', () => {
13+
it('should be exported', () => {
14+
expect(VirtualGrid).toBeDefined();
15+
expect(typeof VirtualGrid).toBe('function');
16+
});
17+
18+
it('should have the correct display name', () => {
19+
// Verify it's a React component
20+
expect(VirtualGrid.name).toBe('VirtualGrid');
21+
});
22+
});
23+
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* VirtualGrid Component
11+
*
12+
* Implements virtual scrolling using @tanstack/react-virtual for efficient
13+
* rendering of large datasets. Only renders visible rows, dramatically improving
14+
* performance with datasets of 1000+ items.
15+
*
16+
* Features:
17+
* - Virtual scrolling for rows
18+
* - Configurable row height
19+
* - Overscan for smooth scrolling
20+
* - Minimal DOM nodes (only visible items)
21+
*/
22+
23+
import React, { useRef } from 'react';
24+
import { useVirtualizer } from '@tanstack/react-virtual';
25+
26+
export interface VirtualGridColumn {
27+
header: string;
28+
accessorKey: string;
29+
cell?: (value: any, row: any) => React.ReactNode;
30+
width?: number | string;
31+
align?: 'left' | 'center' | 'right';
32+
}
33+
34+
export interface VirtualGridProps {
35+
data: any[];
36+
columns: VirtualGridColumn[];
37+
rowHeight?: number;
38+
className?: string;
39+
headerClassName?: string;
40+
rowClassName?: string | ((row: any, index: number) => string);
41+
onRowClick?: (row: any, index: number) => void;
42+
overscan?: number;
43+
}
44+
45+
/**
46+
* Virtual scrolling grid component
47+
*
48+
* @example
49+
* ```tsx
50+
* <VirtualGrid
51+
* data={items}
52+
* columns={[
53+
* { header: 'Name', accessorKey: 'name' },
54+
* { header: 'Age', accessorKey: 'age' },
55+
* ]}
56+
* rowHeight={40}
57+
* />
58+
* ```
59+
*/
60+
export const VirtualGrid: React.FC<VirtualGridProps> = ({
61+
data,
62+
columns,
63+
rowHeight = 40,
64+
className = '',
65+
headerClassName = '',
66+
rowClassName,
67+
onRowClick,
68+
overscan = 5,
69+
}) => {
70+
const parentRef = useRef<HTMLDivElement>(null);
71+
72+
const virtualizer = useVirtualizer({
73+
count: data.length,
74+
getScrollElement: () => parentRef.current,
75+
estimateSize: () => rowHeight,
76+
overscan,
77+
});
78+
79+
const items = virtualizer.getVirtualItems();
80+
81+
return (
82+
<div className={className}>
83+
{/* Header */}
84+
<div
85+
className={`grid border-b sticky top-0 bg-background z-10 ${headerClassName}`}
86+
style={{
87+
gridTemplateColumns: columns
88+
.map((col) => col.width || '1fr')
89+
.join(' '),
90+
}}
91+
>
92+
{columns.map((column, index) => (
93+
<div
94+
key={index}
95+
className={`px-4 py-2 font-semibold text-sm text-${column.align || 'left'}`}
96+
>
97+
{column.header}
98+
</div>
99+
))}
100+
</div>
101+
102+
{/* Virtual scrolling container */}
103+
<div
104+
ref={parentRef}
105+
className="h-[600px] overflow-auto"
106+
style={{ contain: 'strict' }}
107+
>
108+
<div
109+
style={{
110+
height: `${virtualizer.getTotalSize()}px`,
111+
width: '100%',
112+
position: 'relative',
113+
}}
114+
>
115+
{items.map((virtualRow) => {
116+
const row = data[virtualRow.index];
117+
const rowClasses =
118+
typeof rowClassName === 'function'
119+
? rowClassName(row, virtualRow.index)
120+
: rowClassName || '';
121+
122+
return (
123+
<div
124+
key={virtualRow.key}
125+
className={`grid border-b hover:bg-muted/50 cursor-pointer ${rowClasses}`}
126+
style={{
127+
position: 'absolute',
128+
top: 0,
129+
left: 0,
130+
width: '100%',
131+
height: `${virtualRow.size}px`,
132+
transform: `translateY(${virtualRow.start}px)`,
133+
gridTemplateColumns: columns
134+
.map((col) => col.width || '1fr')
135+
.join(' '),
136+
}}
137+
onClick={() => onRowClick?.(row, virtualRow.index)}
138+
>
139+
{columns.map((column, colIndex) => {
140+
const value = row[column.accessorKey];
141+
const cellContent = column.cell
142+
? column.cell(value, row)
143+
: value;
144+
145+
return (
146+
<div
147+
key={colIndex}
148+
className={`px-4 py-2 text-sm flex items-center text-${column.align || 'left'}`}
149+
>
150+
{cellContent}
151+
</div>
152+
);
153+
})}
154+
</div>
155+
);
156+
})}
157+
</div>
158+
</div>
159+
160+
{/* Footer info */}
161+
<div className="px-4 py-2 text-xs text-muted-foreground border-t">
162+
Showing {items.length} of {data.length} rows (virtual scrolling enabled)
163+
</div>
164+
</div>
165+
);
166+
};

packages/plugin-grid/src/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
import React from 'react';
1010
import { ComponentRegistry } from '@object-ui/core';
1111
import { ObjectGrid } from './ObjectGrid';
12+
import { VirtualGrid } from './VirtualGrid';
1213

13-
export { ObjectGrid };
14+
export { ObjectGrid, VirtualGrid };
1415
export type { ObjectGridProps } from './ObjectGrid';
16+
export type { VirtualGridProps, VirtualGridColumn } from './VirtualGrid';
1517

1618
// Register object-grid component
1719
const ObjectGridRenderer: React.FC<{ schema: any }> = ({ schema }) => {

pnpm-lock.yaml

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)