gp-grid-react 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,520 +1,593 @@
1
- # gp-grid-react
2
-
3
- A high-performance React data grid component built on [gp-grid-core](../core/README.md), featuring virtual scrolling, cell selection, sorting, filtering, editing, and Excel-like fill handle.
4
-
5
- ## Features
6
-
7
- - **Virtual Scrolling**: Efficiently handles 150,000+ rows through slot-based recycling
8
- - **Cell Selection**: Single cell, range selection, Shift+click extend, Ctrl+click toggle
9
- - **Multi-Column Sorting**: Click to sort, Shift+click for multi-column sort
10
- - **Column Filtering**: Built-in filter row with debounced input
11
- - **Cell Editing**: Double-click or press Enter to edit, with custom editor support
12
- - **Fill Handle**: Excel-like drag-to-fill for editable cells
13
- - **Keyboard Navigation**: Arrow keys, Tab, Enter, Escape, Ctrl+A, Ctrl+C
14
- - **Custom Renderers**: Registry-based cell, edit, and header renderers
15
- - **Dark Mode**: Built-in dark theme support
16
- - **TypeScript**: Full type safety with exported types
17
-
18
- ## Installation
19
-
20
- Use `npm`, `yarn` or `pnpm`
21
- ```bash
22
- pnpm add gp-grid-react
23
- ```
24
-
25
- ## Quick Start
26
-
27
- ```tsx
28
- import { Grid, type ColumnDefinition } from "gp-grid-react";
29
-
30
- interface Person {
31
- id: number;
32
- name: string;
33
- age: number;
34
- email: string;
35
- }
36
-
37
- const columns: ColumnDefinition[] = [
38
- { field: "id", cellDataType: "number", width: 80, headerName: "ID" },
39
- { field: "name", cellDataType: "text", width: 150, headerName: "Name" },
40
- { field: "age", cellDataType: "number", width: 80, headerName: "Age" },
41
- { field: "email", cellDataType: "text", width: 250, headerName: "Email" },
42
- ];
43
-
44
- const data: Person[] = [
45
- { id: 1, name: "Alice", age: 30, email: "alice@example.com" },
46
- { id: 2, name: "Bob", age: 25, email: "bob@example.com" },
47
- { id: 3, name: "Charlie", age: 35, email: "charlie@example.com" },
48
- ];
49
-
50
- function App() {
51
- return (
52
- <div style={{ width: "800px", height: "400px" }}>
53
- <Grid
54
- columns={columns}
55
- rowData={data}
56
- rowHeight={36}
57
- />
58
- </div>
59
- );
60
- }
61
- ```
62
-
63
- ## Examples
64
-
65
- ### Client-Side Data Source with Sorting and Filtering
66
-
67
- For larger datasets with client-side sort/filter operations:
68
-
69
- ```tsx
70
- import { useMemo } from "react";
71
- import { Grid, createClientDataSource, type ColumnDefinition } from "gp-grid-react";
72
-
73
- interface Product {
74
- id: number;
75
- name: string;
76
- price: number;
77
- category: string;
78
- }
79
-
80
- const columns: ColumnDefinition[] = [
81
- { field: "id", cellDataType: "number", width: 80, headerName: "ID" },
82
- { field: "name", cellDataType: "text", width: 200, headerName: "Product" },
83
- { field: "price", cellDataType: "number", width: 100, headerName: "Price" },
84
- { field: "category", cellDataType: "text", width: 150, headerName: "Category" },
85
- ];
86
-
87
- function ProductGrid() {
88
- const products: Product[] = useMemo(() =>
89
- Array.from({ length: 10000 }, (_, i) => ({
90
- id: i + 1,
91
- name: `Product ${i + 1}`,
92
- price: Math.round(Math.random() * 1000) / 10,
93
- category: ["Electronics", "Clothing", "Food", "Books"][i % 4],
94
- })),
95
- []);
96
-
97
- const dataSource = useMemo(
98
- () => createClientDataSource(products),
99
- [products]
100
- );
101
-
102
- return (
103
- <div style={{ width: "100%", height: "500px" }}>
104
- <Grid
105
- columns={columns}
106
- dataSource={dataSource}
107
- rowHeight={36}
108
- headerHeight={40}
109
- showFilters={true}
110
- filterDebounce={300}
111
- />
112
- </div>
113
- );
114
- }
115
- ```
116
-
117
- ### Server-Side Data Source
118
-
119
- For datasets too large to load entirely in memory, use a server-side data source:
120
-
121
- ```tsx
122
- import { useMemo } from "react";
123
- import {
124
- Grid,
125
- createServerDataSource,
126
- type ColumnDefinition,
127
- type DataSourceRequest,
128
- type DataSourceResponse,
129
- } from "gp-grid-react";
130
-
131
- interface User {
132
- id: number;
133
- name: string;
134
- email: string;
135
- role: string;
136
- createdAt: string;
137
- }
138
-
139
- const columns: ColumnDefinition[] = [
140
- { field: "id", cellDataType: "number", width: 80, headerName: "ID" },
141
- { field: "name", cellDataType: "text", width: 150, headerName: "Name" },
142
- { field: "email", cellDataType: "text", width: 250, headerName: "Email" },
143
- { field: "role", cellDataType: "text", width: 120, headerName: "Role" },
144
- { field: "createdAt", cellDataType: "dateString", width: 150, headerName: "Created" },
145
- ];
146
-
147
- // API fetch function that handles pagination, sorting, and filtering
148
- async function fetchUsers(request: DataSourceRequest): Promise<DataSourceResponse<User>> {
149
- const { pagination, sort, filter } = request;
150
-
151
- // Build query parameters
152
- const params = new URLSearchParams({
153
- page: String(pagination.pageIndex),
154
- limit: String(pagination.pageSize),
155
- });
156
-
157
- // Add sorting parameters
158
- if (sort && sort.length > 0) {
159
- // Format: sortBy=name:asc,email:desc
160
- const sortString = sort
161
- .map((s) => `${s.colId}:${s.direction}`)
162
- .join(",");
163
- params.set("sortBy", sortString);
164
- }
165
-
166
- // Add filter parameters
167
- if (filter) {
168
- Object.entries(filter).forEach(([field, value]) => {
169
- if (value) {
170
- params.set(`filter[${field}]`, value);
171
- }
172
- });
173
- }
174
-
175
- // Make API request
176
- const response = await fetch(`https://api.example.com/users?${params}`);
177
-
178
- if (!response.ok) {
179
- throw new Error(`API error: ${response.status}`);
180
- }
181
-
182
- const data = await response.json();
183
-
184
- // Return in DataSourceResponse format
185
- return {
186
- rows: data.users, // Array of User objects
187
- totalRows: data.total, // Total count for virtual scrolling
188
- };
189
- }
190
-
191
- function UserGrid() {
192
- // Create server data source - memoize to prevent recreation
193
- const dataSource = useMemo(
194
- () => createServerDataSource<User>(fetchUsers),
195
- []
196
- );
197
-
198
- return (
199
- <div style={{ width: "100%", height: "600px" }}>
200
- <Grid
201
- columns={columns}
202
- dataSource={dataSource}
203
- rowHeight={36}
204
- headerHeight={40}
205
- showFilters={true}
206
- filterDebounce={500} // Debounce filter requests
207
- darkMode={true}
208
- />
209
- </div>
210
- );
211
- }
212
- ```
213
-
214
- ### Custom Cell Renderers
215
-
216
- Use the registry pattern to define reusable renderers:
217
-
218
- ```tsx
219
- import { Grid, type ColumnDefinition, type CellRendererParams } from "gp-grid-react";
220
-
221
- interface Order {
222
- id: number;
223
- customer: string;
224
- total: number;
225
- status: "pending" | "shipped" | "delivered" | "cancelled";
226
- }
227
-
228
- // Define reusable renderers
229
- const cellRenderers = {
230
- // Currency formatter
231
- currency: (params: CellRendererParams) => {
232
- const value = params.value as number;
233
- return (
234
- <span style={{ color: "#047857", fontWeight: 600 }}>
235
- ${value.toLocaleString("en-US", { minimumFractionDigits: 2 })}
236
- </span>
237
- );
238
- },
239
-
240
- // Status badge
241
- statusBadge: (params: CellRendererParams) => {
242
- const status = params.value as Order["status"];
243
- const colors: Record<string, { bg: string; text: string }> = {
244
- pending: { bg: "#fef3c7", text: "#92400e" },
245
- shipped: { bg: "#dbeafe", text: "#1e40af" },
246
- delivered: { bg: "#dcfce7", text: "#166534" },
247
- cancelled: { bg: "#fee2e2", text: "#991b1b" },
248
- };
249
- const color = colors[status] ?? { bg: "#f3f4f6", text: "#374151" };
250
-
251
- return (
252
- <span
253
- style={{
254
- backgroundColor: color.bg,
255
- color: color.text,
256
- padding: "2px 8px",
257
- borderRadius: "12px",
258
- fontSize: "12px",
259
- fontWeight: 600,
260
- }}
261
- >
262
- {status.toUpperCase()}
263
- </span>
264
- );
265
- },
266
-
267
- // Bold text
268
- bold: (params: CellRendererParams) => (
269
- <strong>{String(params.value ?? "")}</strong>
270
- ),
271
- };
272
-
273
- const columns: ColumnDefinition[] = [
274
- { field: "id", cellDataType: "number", width: 80, headerName: "ID", cellRenderer: "bold" },
275
- { field: "customer", cellDataType: "text", width: 200, headerName: "Customer" },
276
- { field: "total", cellDataType: "number", width: 120, headerName: "Total", cellRenderer: "currency" },
277
- { field: "status", cellDataType: "text", width: 120, headerName: "Status", cellRenderer: "statusBadge" },
278
- ];
279
-
280
- function OrderGrid({ orders }: { orders: Order[] }) {
281
- return (
282
- <div style={{ width: "100%", height: "400px" }}>
283
- <Grid
284
- columns={columns}
285
- rowData={orders}
286
- rowHeight={40}
287
- cellRenderers={cellRenderers}
288
- />
289
- </div>
290
- );
291
- }
292
- ```
293
-
294
- ### Editable Cells with Custom Editors
295
-
296
- ```tsx
297
- import { useState } from "react";
298
- import {
299
- Grid,
300
- createClientDataSource,
301
- type ColumnDefinition,
302
- type EditRendererParams
303
- } from "gp-grid-react";
304
-
305
- interface Task {
306
- id: number;
307
- title: string;
308
- priority: "low" | "medium" | "high";
309
- completed: boolean;
310
- }
311
-
312
- // Custom select editor for priority field
313
- const editRenderers = {
314
- prioritySelect: (params: EditRendererParams) => {
315
- const [value, setValue] = useState(params.initialValue as string);
316
-
317
- return (
318
- <select
319
- autoFocus
320
- value={value}
321
- onChange={(e) => {
322
- setValue(e.target.value);
323
- params.onValueChange(e.target.value);
324
- }}
325
- onBlur={() => params.onCommit()}
326
- onKeyDown={(e) => {
327
- if (e.key === "Enter") params.onCommit();
328
- if (e.key === "Escape") params.onCancel();
329
- }}
330
- style={{
331
- width: "100%",
332
- height: "100%",
333
- border: "none",
334
- outline: "none",
335
- padding: "0 8px",
336
- }}
337
- >
338
- <option value="low">Low</option>
339
- <option value="medium">Medium</option>
340
- <option value="high">High</option>
341
- </select>
342
- );
343
- },
344
-
345
- checkbox: (params: EditRendererParams) => (
346
- <input
347
- type="checkbox"
348
- autoFocus
349
- defaultChecked={params.initialValue as boolean}
350
- onChange={(e) => {
351
- params.onValueChange(e.target.checked);
352
- params.onCommit();
353
- }}
354
- style={{ width: 20, height: 20 }}
355
- />
356
- ),
357
- };
358
-
359
- const columns: ColumnDefinition[] = [
360
- { field: "id", cellDataType: "number", width: 60, headerName: "ID" },
361
- {
362
- field: "title",
363
- cellDataType: "text",
364
- width: 300,
365
- headerName: "Title",
366
- editable: true, // Uses default text input
367
- },
368
- {
369
- field: "priority",
370
- cellDataType: "text",
371
- width: 120,
372
- headerName: "Priority",
373
- editable: true,
374
- editRenderer: "prioritySelect", // Custom editor
375
- },
376
- {
377
- field: "completed",
378
- cellDataType: "boolean",
379
- width: 100,
380
- headerName: "Done",
381
- editable: true,
382
- editRenderer: "checkbox", // Custom editor
383
- },
384
- ];
385
-
386
- function TaskGrid() {
387
- const tasks: Task[] = [
388
- { id: 1, title: "Write documentation", priority: "high", completed: false },
389
- { id: 2, title: "Fix bugs", priority: "medium", completed: true },
390
- { id: 3, title: "Add tests", priority: "low", completed: false },
391
- ];
392
-
393
- const dataSource = createClientDataSource(tasks);
394
-
395
- return (
396
- <div style={{ width: "600px", height: "300px" }}>
397
- <Grid
398
- columns={columns}
399
- dataSource={dataSource}
400
- rowHeight={40}
401
- editRenderers={editRenderers}
402
- />
403
- </div>
404
- );
405
- }
406
- ```
407
-
408
- ### Dark Mode
409
-
410
- ```tsx
411
- <Grid
412
- columns={columns}
413
- rowData={data}
414
- rowHeight={36}
415
- darkMode={true}
416
- />
417
- ```
418
-
419
- ## API Reference
420
-
421
- ### GridProps
422
-
423
- | Prop | Type | Default | Description |
424
- |------|------|---------|-------------|
425
- | `columns` | `ColumnDefinition[]` | required | Column definitions |
426
- | `dataSource` | `DataSource<TData>` | - | Data source for fetching data |
427
- | `rowData` | `TData[]` | - | Alternative: raw data array (wrapped in client data source) |
428
- | `rowHeight` | `number` | required | Height of each row in pixels |
429
- | `headerHeight` | `number` | `rowHeight` | Height of header row |
430
- | `overscan` | `number` | `3` | Number of rows to render outside viewport |
431
- | `showFilters` | `boolean` | `false` | Show filter row below headers |
432
- | `filterDebounce` | `number` | `300` | Debounce time for filter input (ms) |
433
- | `darkMode` | `boolean` | `false` | Enable dark theme |
434
- | `cellRenderers` | `Record<string, ReactCellRenderer>` | `{}` | Cell renderer registry |
435
- | `editRenderers` | `Record<string, ReactEditRenderer>` | `{}` | Edit renderer registry |
436
- | `headerRenderers` | `Record<string, ReactHeaderRenderer>` | `{}` | Header renderer registry |
437
- | `cellRenderer` | `ReactCellRenderer` | - | Global fallback cell renderer |
438
- | `editRenderer` | `ReactEditRenderer` | - | Global fallback edit renderer |
439
- | `headerRenderer` | `ReactHeaderRenderer` | - | Global fallback header renderer |
440
-
441
- ### ColumnDefinition
442
-
443
- | Property | Type | Description |
444
- |----------|------|-------------|
445
- | `field` | `string` | Property path in row data (supports dot notation: `"address.city"`) |
446
- | `colId` | `string` | Unique column ID (defaults to `field`) |
447
- | `cellDataType` | `CellDataType` | `"text"` \| `"number"` \| `"boolean"` \| `"date"` \| `"object"` |
448
- | `width` | `number` | Column width in pixels |
449
- | `headerName` | `string` | Display name in header (defaults to `field`) |
450
- | `editable` | `boolean` | Enable cell editing |
451
- | `cellRenderer` | `string` | Key in `cellRenderers` registry |
452
- | `editRenderer` | `string` | Key in `editRenderers` registry |
453
- | `headerRenderer` | `string` | Key in `headerRenderers` registry |
454
-
455
- ### Renderer Types
456
-
457
- ```typescript
458
- // Cell renderer receives these params
459
- interface CellRendererParams {
460
- value: CellValue; // Current cell value
461
- rowData: Row; // Full row data
462
- column: ColumnDefinition; // Column definition
463
- rowIndex: number; // Row index
464
- colIndex: number; // Column index
465
- isActive: boolean; // Is this the active cell?
466
- isSelected: boolean; // Is this cell in selection?
467
- isEditing: boolean; // Is this cell being edited?
468
- }
469
-
470
- // Edit renderer receives additional callbacks
471
- interface EditRendererParams extends CellRendererParams {
472
- initialValue: CellValue;
473
- onValueChange: (newValue: CellValue) => void;
474
- onCommit: () => void;
475
- onCancel: () => void;
476
- }
477
-
478
- // Header renderer params
479
- interface HeaderRendererParams {
480
- column: ColumnDefinition;
481
- colIndex: number;
482
- sortDirection?: "asc" | "desc";
483
- sortIndex?: number; // For multi-column sort
484
- onSort: (direction: "asc" | "desc" | null, addToExisting: boolean) => void;
485
- }
486
- ```
487
-
488
- ## Keyboard Shortcuts
489
-
490
- | Key | Action |
491
- |-----|--------|
492
- | Arrow keys | Navigate between cells |
493
- | Shift + Arrow | Extend selection |
494
- | Enter | Start editing / Commit edit |
495
- | Escape | Cancel edit / Clear selection |
496
- | Tab | Commit and move right |
497
- | Shift + Tab | Commit and move left |
498
- | F2 | Start editing |
499
- | Delete / Backspace | Start editing with empty value |
500
- | Ctrl + A | Select all |
501
- | Ctrl + C | Copy selection to clipboard |
502
- | Any character | Start editing with that character |
503
-
504
- ## Styling
505
-
506
- The grid injects its own styles automatically. The main container uses these CSS classes:
507
-
508
- - `.gp-grid-container` - Main container
509
- - `.gp-grid-container--dark` - Dark mode modifier
510
- - `.gp-grid-header` - Header row container
511
- - `.gp-grid-header-cell` - Individual header cell
512
- - `.gp-grid-row` - Row container
513
- - `.gp-grid-cell` - Cell container
514
- - `.gp-grid-cell--active` - Active cell
515
- - `.gp-grid-cell--selected` - Selected cell
516
- - `.gp-grid-cell--editing` - Cell in edit mode
517
- - `.gp-grid-filter-row` - Filter row container
518
- - `.gp-grid-filter-input` - Filter input field
519
- - `.gp-grid-fill-handle` - Fill handle element
520
-
1
+ # gp-grid-react 🏁 🏎️
2
+
3
+ A high-performance, feature lean React data grid component built to manage grids with huge amount (millions) of rows. It's based on its core dependency: `gp-grid-core`, featuring virtual scrolling, cell selection, sorting, filtering, editing, and Excel-like fill handle.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Features](#features)
8
+ - [Installation](#installation)
9
+ - [Quick Start](#quick-start)
10
+ - [Examples](#examples)
11
+ - [API Reference](#api-reference)
12
+ - [Keyboard Shortcuts](#keyboard-shortcuts)
13
+ - [Styling](#styling)
14
+ - [Donations](#donations)
15
+
16
+ ## Features
17
+
18
+ - **Virtual Scrolling**: Efficiently handles 150,000+ rows through slot-based recycling
19
+ - **Cell Selection**: Single cell, range selection, Shift+click extend, Ctrl+click toggle
20
+ - **Multi-Column Sorting**: Click to sort, Shift+click for multi-column sort
21
+ - **Column Filtering**: Built-in filter row with debounced input
22
+ - **Cell Editing**: Double-click or press Enter to edit, with custom editor support
23
+ - **Fill Handle**: Excel-like drag-to-fill for editable cells
24
+ - **Keyboard Navigation**: Arrow keys, Tab, Enter, Escape, Ctrl+A, Ctrl+C
25
+ - **Custom Renderers**: Registry-based cell, edit, and header renderers
26
+ - **Dark Mode**: Built-in dark theme support
27
+ - **TypeScript**: Full type safety with exported types
28
+
29
+ ## Installation
30
+
31
+ Use `npm`, `yarn` or `pnpm`
32
+
33
+ ```bash
34
+ pnpm add gp-grid-react
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```tsx
40
+ import { Grid, type ColumnDefinition } from "gp-grid-react";
41
+
42
+ interface Person {
43
+ id: number;
44
+ name: string;
45
+ age: number;
46
+ email: string;
47
+ }
48
+
49
+ const columns: ColumnDefinition[] = [
50
+ { field: "id", cellDataType: "number", width: 80, headerName: "ID" },
51
+ { field: "name", cellDataType: "text", width: 150, headerName: "Name" },
52
+ { field: "age", cellDataType: "number", width: 80, headerName: "Age" },
53
+ { field: "email", cellDataType: "text", width: 250, headerName: "Email" },
54
+ ];
55
+
56
+ const data: Person[] = [
57
+ { id: 1, name: "Alice", age: 30, email: "alice@example.com" },
58
+ { id: 2, name: "Bob", age: 25, email: "bob@example.com" },
59
+ { id: 3, name: "Charlie", age: 35, email: "charlie@example.com" },
60
+ ];
61
+
62
+ function App() {
63
+ return (
64
+ <div style={{ width: "800px", height: "400px" }}>
65
+ <Grid columns={columns} rowData={data} rowHeight={36} />
66
+ </div>
67
+ );
68
+ }
69
+ ```
70
+
71
+ ## Examples
72
+
73
+ ### Client-Side Data Source with Sorting and Filtering
74
+
75
+ For larger datasets with client-side sort/filter operations:
76
+
77
+ ```tsx
78
+ import { useMemo } from "react";
79
+ import {
80
+ Grid,
81
+ createClientDataSource,
82
+ type ColumnDefinition,
83
+ } from "gp-grid-react";
84
+
85
+ interface Product {
86
+ id: number;
87
+ name: string;
88
+ price: number;
89
+ category: string;
90
+ }
91
+
92
+ const columns: ColumnDefinition[] = [
93
+ { field: "id", cellDataType: "number", width: 80, headerName: "ID" },
94
+ { field: "name", cellDataType: "text", width: 200, headerName: "Product" },
95
+ { field: "price", cellDataType: "number", width: 100, headerName: "Price" },
96
+ {
97
+ field: "category",
98
+ cellDataType: "text",
99
+ width: 150,
100
+ headerName: "Category",
101
+ },
102
+ ];
103
+
104
+ function ProductGrid() {
105
+ const products: Product[] = useMemo(
106
+ () =>
107
+ Array.from({ length: 10000 }, (_, i) => ({
108
+ id: i + 1,
109
+ name: `Product ${i + 1}`,
110
+ price: Math.round(Math.random() * 1000) / 10,
111
+ category: ["Electronics", "Clothing", "Food", "Books"][i % 4],
112
+ })),
113
+ [],
114
+ );
115
+
116
+ const dataSource = useMemo(
117
+ () => createClientDataSource(products),
118
+ [products],
119
+ );
120
+
121
+ return (
122
+ <div style={{ width: "100%", height: "500px" }}>
123
+ <Grid
124
+ columns={columns}
125
+ dataSource={dataSource}
126
+ rowHeight={36}
127
+ headerHeight={40}
128
+ showFilters={true}
129
+ filterDebounce={300}
130
+ />
131
+ </div>
132
+ );
133
+ }
134
+ ```
135
+
136
+ ### Server-Side Data Source
137
+
138
+ For datasets too large to load entirely in memory, use a server-side data source:
139
+
140
+ ```tsx
141
+ import { useMemo } from "react";
142
+ import {
143
+ Grid,
144
+ createServerDataSource,
145
+ type ColumnDefinition,
146
+ type DataSourceRequest,
147
+ type DataSourceResponse,
148
+ } from "gp-grid-react";
149
+
150
+ interface User {
151
+ id: number;
152
+ name: string;
153
+ email: string;
154
+ role: string;
155
+ createdAt: string;
156
+ }
157
+
158
+ const columns: ColumnDefinition[] = [
159
+ { field: "id", cellDataType: "number", width: 80, headerName: "ID" },
160
+ { field: "name", cellDataType: "text", width: 150, headerName: "Name" },
161
+ { field: "email", cellDataType: "text", width: 250, headerName: "Email" },
162
+ { field: "role", cellDataType: "text", width: 120, headerName: "Role" },
163
+ {
164
+ field: "createdAt",
165
+ cellDataType: "dateString",
166
+ width: 150,
167
+ headerName: "Created",
168
+ },
169
+ ];
170
+
171
+ // API fetch function that handles pagination, sorting, and filtering
172
+ async function fetchUsers(
173
+ request: DataSourceRequest,
174
+ ): Promise<DataSourceResponse<User>> {
175
+ const { pagination, sort, filter } = request;
176
+
177
+ // Build query parameters
178
+ const params = new URLSearchParams({
179
+ page: String(pagination.pageIndex),
180
+ limit: String(pagination.pageSize),
181
+ });
182
+
183
+ // Add sorting parameters
184
+ if (sort && sort.length > 0) {
185
+ // Format: sortBy=name:asc,email:desc
186
+ const sortString = sort.map((s) => `${s.colId}:${s.direction}`).join(",");
187
+ params.set("sortBy", sortString);
188
+ }
189
+
190
+ // Add filter parameters
191
+ if (filter) {
192
+ Object.entries(filter).forEach(([field, value]) => {
193
+ if (value) {
194
+ params.set(`filter[${field}]`, value);
195
+ }
196
+ });
197
+ }
198
+
199
+ // Make API request
200
+ const response = await fetch(`https://api.example.com/users?${params}`);
201
+
202
+ if (!response.ok) {
203
+ throw new Error(`API error: ${response.status}`);
204
+ }
205
+
206
+ const data = await response.json();
207
+
208
+ // Return in DataSourceResponse format
209
+ return {
210
+ rows: data.users, // Array of User objects
211
+ totalRows: data.total, // Total count for virtual scrolling
212
+ };
213
+ }
214
+
215
+ function UserGrid() {
216
+ // Create server data source - memoize to prevent recreation
217
+ const dataSource = useMemo(
218
+ () => createServerDataSource<User>(fetchUsers),
219
+ [],
220
+ );
221
+
222
+ return (
223
+ <div style={{ width: "100%", height: "600px" }}>
224
+ <Grid
225
+ columns={columns}
226
+ dataSource={dataSource}
227
+ rowHeight={36}
228
+ headerHeight={40}
229
+ showFilters={true}
230
+ filterDebounce={500} // Debounce filter requests
231
+ darkMode={true}
232
+ />
233
+ </div>
234
+ );
235
+ }
236
+ ```
237
+
238
+ ### Custom Cell Renderers
239
+
240
+ Use the registry pattern to define reusable renderers:
241
+
242
+ ```tsx
243
+ import {
244
+ Grid,
245
+ type ColumnDefinition,
246
+ type CellRendererParams,
247
+ } from "gp-grid-react";
248
+
249
+ interface Order {
250
+ id: number;
251
+ customer: string;
252
+ total: number;
253
+ status: "pending" | "shipped" | "delivered" | "cancelled";
254
+ }
255
+
256
+ // Define reusable renderers
257
+ const cellRenderers = {
258
+ // Currency formatter
259
+ currency: (params: CellRendererParams) => {
260
+ const value = params.value as number;
261
+ return (
262
+ <span style={{ color: "#047857", fontWeight: 600 }}>
263
+ ${value.toLocaleString("en-US", { minimumFractionDigits: 2 })}
264
+ </span>
265
+ );
266
+ },
267
+
268
+ // Status badge
269
+ statusBadge: (params: CellRendererParams) => {
270
+ const status = params.value as Order["status"];
271
+ const colors: Record<string, { bg: string; text: string }> = {
272
+ pending: { bg: "#fef3c7", text: "#92400e" },
273
+ shipped: { bg: "#dbeafe", text: "#1e40af" },
274
+ delivered: { bg: "#dcfce7", text: "#166534" },
275
+ cancelled: { bg: "#fee2e2", text: "#991b1b" },
276
+ };
277
+ const color = colors[status] ?? { bg: "#f3f4f6", text: "#374151" };
278
+
279
+ return (
280
+ <span
281
+ style={{
282
+ backgroundColor: color.bg,
283
+ color: color.text,
284
+ padding: "2px 8px",
285
+ borderRadius: "12px",
286
+ fontSize: "12px",
287
+ fontWeight: 600,
288
+ }}
289
+ >
290
+ {status.toUpperCase()}
291
+ </span>
292
+ );
293
+ },
294
+
295
+ // Bold text
296
+ bold: (params: CellRendererParams) => (
297
+ <strong>{String(params.value ?? "")}</strong>
298
+ ),
299
+ };
300
+
301
+ const columns: ColumnDefinition[] = [
302
+ {
303
+ field: "id",
304
+ cellDataType: "number",
305
+ width: 80,
306
+ headerName: "ID",
307
+ cellRenderer: "bold",
308
+ },
309
+ {
310
+ field: "customer",
311
+ cellDataType: "text",
312
+ width: 200,
313
+ headerName: "Customer",
314
+ },
315
+ {
316
+ field: "total",
317
+ cellDataType: "number",
318
+ width: 120,
319
+ headerName: "Total",
320
+ cellRenderer: "currency",
321
+ },
322
+ {
323
+ field: "status",
324
+ cellDataType: "text",
325
+ width: 120,
326
+ headerName: "Status",
327
+ cellRenderer: "statusBadge",
328
+ },
329
+ ];
330
+
331
+ function OrderGrid({ orders }: { orders: Order[] }) {
332
+ return (
333
+ <div style={{ width: "100%", height: "400px" }}>
334
+ <Grid
335
+ columns={columns}
336
+ rowData={orders}
337
+ rowHeight={40}
338
+ cellRenderers={cellRenderers}
339
+ />
340
+ </div>
341
+ );
342
+ }
343
+ ```
344
+
345
+ ### Editable Cells with Custom Editors
346
+
347
+ ```tsx
348
+ import { useState } from "react";
349
+ import {
350
+ Grid,
351
+ createClientDataSource,
352
+ type ColumnDefinition,
353
+ type EditRendererParams,
354
+ } from "gp-grid-react";
355
+
356
+ interface Task {
357
+ id: number;
358
+ title: string;
359
+ priority: "low" | "medium" | "high";
360
+ completed: boolean;
361
+ }
362
+
363
+ // Custom select editor for priority field
364
+ const editRenderers = {
365
+ prioritySelect: (params: EditRendererParams) => {
366
+ const [value, setValue] = useState(params.initialValue as string);
367
+
368
+ return (
369
+ <select
370
+ autoFocus
371
+ value={value}
372
+ onChange={(e) => {
373
+ setValue(e.target.value);
374
+ params.onValueChange(e.target.value);
375
+ }}
376
+ onBlur={() => params.onCommit()}
377
+ onKeyDown={(e) => {
378
+ if (e.key === "Enter") params.onCommit();
379
+ if (e.key === "Escape") params.onCancel();
380
+ }}
381
+ style={{
382
+ width: "100%",
383
+ height: "100%",
384
+ border: "none",
385
+ outline: "none",
386
+ padding: "0 8px",
387
+ }}
388
+ >
389
+ <option value="low">Low</option>
390
+ <option value="medium">Medium</option>
391
+ <option value="high">High</option>
392
+ </select>
393
+ );
394
+ },
395
+
396
+ checkbox: (params: EditRendererParams) => (
397
+ <input
398
+ type="checkbox"
399
+ autoFocus
400
+ defaultChecked={params.initialValue as boolean}
401
+ onChange={(e) => {
402
+ params.onValueChange(e.target.checked);
403
+ params.onCommit();
404
+ }}
405
+ style={{ width: 20, height: 20 }}
406
+ />
407
+ ),
408
+ };
409
+
410
+ const columns: ColumnDefinition[] = [
411
+ { field: "id", cellDataType: "number", width: 60, headerName: "ID" },
412
+ {
413
+ field: "title",
414
+ cellDataType: "text",
415
+ width: 300,
416
+ headerName: "Title",
417
+ editable: true, // Uses default text input
418
+ },
419
+ {
420
+ field: "priority",
421
+ cellDataType: "text",
422
+ width: 120,
423
+ headerName: "Priority",
424
+ editable: true,
425
+ editRenderer: "prioritySelect", // Custom editor
426
+ },
427
+ {
428
+ field: "completed",
429
+ cellDataType: "boolean",
430
+ width: 100,
431
+ headerName: "Done",
432
+ editable: true,
433
+ editRenderer: "checkbox", // Custom editor
434
+ },
435
+ ];
436
+
437
+ function TaskGrid() {
438
+ const tasks: Task[] = [
439
+ { id: 1, title: "Write documentation", priority: "high", completed: false },
440
+ { id: 2, title: "Fix bugs", priority: "medium", completed: true },
441
+ { id: 3, title: "Add tests", priority: "low", completed: false },
442
+ ];
443
+
444
+ const dataSource = createClientDataSource(tasks);
445
+
446
+ return (
447
+ <div style={{ width: "600px", height: "300px" }}>
448
+ <Grid
449
+ columns={columns}
450
+ dataSource={dataSource}
451
+ rowHeight={40}
452
+ editRenderers={editRenderers}
453
+ />
454
+ </div>
455
+ );
456
+ }
457
+ ```
458
+
459
+ ### Dark Mode
460
+
461
+ ```tsx
462
+ <Grid columns={columns} rowData={data} rowHeight={36} darkMode={true} />
463
+ ```
464
+
465
+ ## API Reference
466
+
467
+ ### GridProps
468
+
469
+ | Prop | Type | Default | Description |
470
+ | ----------------- | ------------------------------------- | ----------- | ----------------------------------------------------------- |
471
+ | `columns` | `ColumnDefinition[]` | required | Column definitions |
472
+ | `dataSource` | `DataSource<TData>` | - | Data source for fetching data |
473
+ | `rowData` | `TData[]` | - | Alternative: raw data array (wrapped in client data source) |
474
+ | `rowHeight` | `number` | required | Height of each row in pixels |
475
+ | `headerHeight` | `number` | `rowHeight` | Height of header row |
476
+ | `overscan` | `number` | `3` | Number of rows to render outside viewport |
477
+ | `showFilters` | `boolean` | `false` | Show filter row below headers |
478
+ | `filterDebounce` | `number` | `300` | Debounce time for filter input (ms) |
479
+ | `darkMode` | `boolean` | `false` | Enable dark theme |
480
+ | `cellRenderers` | `Record<string, ReactCellRenderer>` | `{}` | Cell renderer registry |
481
+ | `editRenderers` | `Record<string, ReactEditRenderer>` | `{}` | Edit renderer registry |
482
+ | `headerRenderers` | `Record<string, ReactHeaderRenderer>` | `{}` | Header renderer registry |
483
+ | `cellRenderer` | `ReactCellRenderer` | - | Global fallback cell renderer |
484
+ | `editRenderer` | `ReactEditRenderer` | - | Global fallback edit renderer |
485
+ | `headerRenderer` | `ReactHeaderRenderer` | - | Global fallback header renderer |
486
+
487
+ ### ColumnDefinition
488
+
489
+ | Property | Type | Description |
490
+ | ---------------- | -------------- | ------------------------------------------------------------------- |
491
+ | `field` | `string` | Property path in row data (supports dot notation: `"address.city"`) |
492
+ | `colId` | `string` | Unique column ID (defaults to `field`) |
493
+ | `cellDataType` | `CellDataType` | `"text"` \| `"number"` \| `"boolean"` \| `"date"` \| `"object"` |
494
+ | `width` | `number` | Column width in pixels |
495
+ | `headerName` | `string` | Display name in header (defaults to `field`) |
496
+ | `editable` | `boolean` | Enable cell editing |
497
+ | `cellRenderer` | `string` | Key in `cellRenderers` registry |
498
+ | `editRenderer` | `string` | Key in `editRenderers` registry |
499
+ | `headerRenderer` | `string` | Key in `headerRenderers` registry |
500
+
501
+ ### Renderer Types
502
+
503
+ ```typescript
504
+ // Cell renderer receives these params
505
+ interface CellRendererParams {
506
+ value: CellValue; // Current cell value
507
+ rowData: Row; // Full row data
508
+ column: ColumnDefinition; // Column definition
509
+ rowIndex: number; // Row index
510
+ colIndex: number; // Column index
511
+ isActive: boolean; // Is this the active cell?
512
+ isSelected: boolean; // Is this cell in selection?
513
+ isEditing: boolean; // Is this cell being edited?
514
+ }
515
+
516
+ // Edit renderer receives additional callbacks
517
+ interface EditRendererParams extends CellRendererParams {
518
+ initialValue: CellValue;
519
+ onValueChange: (newValue: CellValue) => void;
520
+ onCommit: () => void;
521
+ onCancel: () => void;
522
+ }
523
+
524
+ // Header renderer params
525
+ interface HeaderRendererParams {
526
+ column: ColumnDefinition;
527
+ colIndex: number;
528
+ sortDirection?: "asc" | "desc";
529
+ sortIndex?: number; // For multi-column sort
530
+ onSort: (direction: "asc" | "desc" | null, addToExisting: boolean) => void;
531
+ }
532
+ ```
533
+
534
+ ## Keyboard Shortcuts
535
+
536
+ | Key | Action |
537
+ | ------------------ | --------------------------------- |
538
+ | Arrow keys | Navigate between cells |
539
+ | Shift + Arrow | Extend selection |
540
+ | Enter | Start editing / Commit edit |
541
+ | Escape | Cancel edit / Clear selection |
542
+ | Tab | Commit and move right |
543
+ | Shift + Tab | Commit and move left |
544
+ | F2 | Start editing |
545
+ | Delete / Backspace | Start editing with empty value |
546
+ | Ctrl + A | Select all |
547
+ | Ctrl + C | Copy selection to clipboard |
548
+ | Any character | Start editing with that character |
549
+
550
+ ## Styling
551
+
552
+ The grid injects its own styles automatically. The main container uses these CSS classes:
553
+
554
+ - `.gp-grid-container` - Main container
555
+ - `.gp-grid-container--dark` - Dark mode modifier
556
+ - `.gp-grid-header` - Header row container
557
+ - `.gp-grid-header-cell` - Individual header cell
558
+ - `.gp-grid-row` - Row container
559
+ - `.gp-grid-cell` - Cell container
560
+ - `.gp-grid-cell--active` - Active cell
561
+ - `.gp-grid-cell--selected` - Selected cell
562
+ - `.gp-grid-cell--editing` - Cell in edit mode
563
+ - `.gp-grid-filter-row` - Filter row container
564
+ - `.gp-grid-filter-input` - Filter input field
565
+ - `.gp-grid-fill-handle` - Fill handle element
566
+
567
+ ## Donations
568
+
569
+ Keeping this library requires effort and passion, I'm a full time engineer employed on other project and I'm trying my best to keep this work free! For all the features.
570
+
571
+ If you think this project helped you achieve your goals, it's hopefully worth a beer! 🍻
572
+
573
+ <div align="center">
574
+
575
+ ### Paypal
576
+
577
+ [![Paypal QR Code](../../public/images/donazione_paypal.png "Paypal QR Code donation")](https://www.paypal.com/donate/?hosted_button_id=XCNMG6BR4ZMLY)
578
+
579
+ [https://www.paypal.com/donate/?hosted_button_id=XCNMG6BR4ZMLY](https://www.paypal.com/donate/?hosted_button_id=XCNMG6BR4ZMLY)
580
+
581
+ ### Bitcoin
582
+
583
+ [![Bitcoin QR Donation](../../public/images/bc1qcukwmzver59eyqq442xyzscmxavqjt568kkc9m.png "Bitcoin QR Donation")](bitcoin:bc1qcukwmzver59eyqq442xyzscmxavqjt568kkc9m)
584
+
585
+ bitcoin:bc1qcukwmzver59eyqq442xyzscmxavqjt568kkc9m
586
+
587
+ ### Lightning Network
588
+
589
+ [![Lightning Network QR Donation](../../public/images/lightning.png "Lightning Network QR Donation")](lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhhx6rpvanhjetdvfjhyvf4xs0xu5p7)
590
+
591
+ lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhhx6rpvanhjetdvfjhyvf4xs0xu5p7
592
+
593
+ </div>