@gp-grid/react 0.7.2

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