@reactorui/datagrid 1.0.20 โ†’ 1.0.22

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,10 +1,10 @@
1
1
  # @reactorui/datagrid
2
2
 
3
- [![npm](https://img.shields.io/npm/dw/@reactorui/datagrid)](https://www.npmjs.com/package/@reactorui/datagrid)
3
+ [![npm](https://img.shields.io/npm/dt/@reactorui/datagrid)](https://www.npmjs.com/package/@reactorui/datagrid)
4
4
  [![npm version](https://img.shields.io/npm/v/@reactorui/datagrid)](https://www.npmjs.com/package/@reactorui/datagrid)
5
5
  [![license](https://img.shields.io/npm/l/@reactorui/datagrid)](https://github.com/your-org/datagrid/blob/main/LICENSE)
6
6
 
7
- A high-performance, feature-rich React data grid component with TypeScript support, server-side integration, pagination and advanced filtering capabilities.
7
+ A high-performance, feature-rich React data grid component with TypeScript support, pagination, and advanced filtering capabilities. Designed as a **controlled presentation component** for maximum flexibility.
8
8
 
9
9
  ## ๐Ÿ–ผ๏ธ Screenshots
10
10
 
@@ -18,16 +18,17 @@ A high-performance, feature-rich React data grid component with TypeScript suppo
18
18
 
19
19
  ## โœจ Features
20
20
 
21
- - ๐Ÿš€ **High Performance** - Optimized rendering and data processing
21
+ - ๐Ÿš€ **High Performance** - Optimized rendering with memoization
22
22
  - ๐Ÿ” **Advanced Filtering** - Type-aware filters with multiple operators (string, number, date, boolean)
23
- - ๐Ÿ”„ **Flexible Data Sources** - Static data or server-side with any API
23
+ - ๐Ÿ”„ **Flexible Data Sources** - Works with any data fetching strategy (REST, GraphQL, local)
24
24
  - ๐Ÿ“ฑ **Responsive Design** - Mobile-first with touch-friendly interactions
25
25
  - ๐ŸŽจ **Customizable Theming** - Multiple built-in variants and custom styling
26
- - ๐ŸŒ™ **Dark Mode Ready** - Built-in dark mode support with CSS variables
26
+ - ๐ŸŒ™ **Dark Mode Ready** - Built-in dark mode support
27
27
  - โ™ฟ **Accessibility First** - WCAG compliant with keyboard navigation and ARIA labels
28
28
  - ๐Ÿ”ง **TypeScript Native** - Full type safety and comprehensive IntelliSense support
29
- - ๐ŸŽฏ **Rich Event System** - 15+ events covering every user interaction
30
- - ๐Ÿ” **Secure Authentication** - Bearer token, API key, and custom header support
29
+ - ๐ŸŽฏ **Rich Event System** - 20+ events covering every user interaction
30
+ - ๐Ÿ“Š **Granular Loading States** - Action-specific loading indicators
31
+ - ๐Ÿ“œ **Scrollable Layout** - Fixed headers with `maxHeight` and `stickyHeader` props
31
32
  - โšก **Zero Dependencies** - Only React as peer dependency
32
33
 
33
34
  ## ๐Ÿ“ฆ Installation
@@ -111,7 +112,7 @@ function App() {
111
112
  data={users}
112
113
  columns={columns}
113
114
  variant="bordered"
114
- size="comfortable"
115
+ size="lg"
115
116
  enableSelection={true}
116
117
  onSelectionChange={(selected) => console.log('Selected:', selected)}
117
118
  />
@@ -119,117 +120,207 @@ function App() {
119
120
  }
120
121
  ```
121
122
 
122
- ## ๐ŸŒ Server-Side Data
123
+ ## ๐ŸŒ Server-Side Data (Controlled Mode)
124
+
125
+ The DataGrid is a **controlled presentation component**. You handle data fetching; the grid handles display.
123
126
 
124
127
  ```tsx
125
- import { DataGrid } from '@reactorui/datagrid';
128
+ import { useState, useEffect } from 'react';
129
+ import { DataGrid, LoadingState, ActiveFilter, SortConfig } from '@reactorui/datagrid';
130
+
131
+ function ServerSideExample() {
132
+ const [data, setData] = useState([]);
133
+ const [loadingState, setLoadingState] = useState<LoadingState>({});
134
+ const [totalRecords, setTotalRecords] = useState(0);
135
+ const [currentPage, setCurrentPage] = useState(1);
136
+ const [error, setError] = useState<string | null>(null);
137
+
138
+ // Fetch data from your API
139
+ const fetchData = async (page: number, filters: ActiveFilter[], search: string) => {
140
+ setLoadingState({ data: true });
141
+ setError(null);
142
+
143
+ try {
144
+ const response = await fetch('/api/users', {
145
+ method: 'POST',
146
+ headers: { 'Content-Type': 'application/json' },
147
+ body: JSON.stringify({ page, pageSize: 25, filters, search }),
148
+ });
149
+
150
+ const result = await response.json();
151
+ setData(result.items);
152
+ setTotalRecords(result.totalRecords);
153
+ } catch (err) {
154
+ setError('Failed to load data');
155
+ } finally {
156
+ setLoadingState({});
157
+ }
158
+ };
159
+
160
+ useEffect(() => {
161
+ fetchData(1, [], '');
162
+ }, []);
126
163
 
127
- function App() {
128
164
  return (
129
165
  <DataGrid
130
- endpoint="/api/users"
131
- httpConfig={{
132
- bearerToken: 'your-jwt-token',
133
- method: 'POST',
134
- postDataFormat: 'json',
135
- customHeaders: {
136
- 'X-Custom-Header': 'value',
137
- },
138
- }}
139
- serverPageSize={100}
166
+ data={data}
167
+ loading={loadingState}
168
+ totalRecords={totalRecords}
169
+ currentPage={currentPage}
170
+ error={error}
140
171
  pageSize={25}
141
- onDataLoad={(data) => console.log(`Loaded ${data.items.length} items`)}
142
- onDataError={(error, context) => console.error(`Error in ${context}:`, error)}
172
+ // Filter callbacks - YOU handle the server call
173
+ onApplyFilter={(filter, allFilters) => {
174
+ setLoadingState({ filter: true });
175
+ fetchData(1, allFilters, '');
176
+ }}
177
+ onClearFilters={() => {
178
+ fetchData(1, [], '');
179
+ }}
180
+ onSearchChange={(term) => {
181
+ setLoadingState({ search: true });
182
+ fetchData(1, [], term);
183
+ }}
184
+ onPageChange={(page) => {
185
+ setCurrentPage(page);
186
+ fetchData(page, [], '');
187
+ }}
188
+ onTableRefresh={() => {
189
+ setLoadingState({ refresh: true });
190
+ fetchData(currentPage, [], '');
191
+ }}
143
192
  />
144
193
  );
145
194
  }
146
195
  ```
147
196
 
148
- ## ๐ŸŽฏ Comprehensive Event System
197
+ ## ๐Ÿ“Š Granular Loading States
198
+
199
+ Instead of a single `loading` boolean, use `loadingState` for action-specific feedback:
200
+
201
+ ```tsx
202
+ interface LoadingState {
203
+ data?: boolean; // Shows skeleton, disables all controls
204
+ filter?: boolean; // Spinner on "Apply Filter" button only
205
+ search?: boolean; // Spinner in search input only
206
+ refresh?: boolean; // Spinner on refresh button only
207
+ delete?: boolean; // Spinner on delete button only
208
+ }
209
+
210
+ // Usage
211
+ <DataGrid
212
+ data={data}
213
+ loadingState={{ filter: true }} // Only Apply Filter shows spinner
214
+ />
215
+
216
+ // Backward compatible - still works
217
+ <DataGrid data={data} loading={true} />
218
+ ```
219
+
220
+ **Visual Result:**
221
+
222
+ - Click "Apply Filter" โ†’ only that button shows `[โŸณ Applying...]`
223
+ - Click "Refresh" โ†’ only that button spins
224
+ - Click "Delete" โ†’ only that button shows `[โŸณ Deleting...]`
225
+ - Initial load โ†’ table skeleton, all controls disabled
226
+
227
+ ## ๐Ÿ“œ Scrollable Layout
149
228
 
150
- The DataGrid provides 15+ events covering every user interaction:
229
+ For large datasets, enable scrollable body with fixed headers:
230
+
231
+ ```tsx
232
+ // Fixed pixel height
233
+ <DataGrid data={data} maxHeight="400px" />
234
+
235
+ // Viewport-relative height
236
+ <DataGrid data={data} maxHeight="60vh" />
237
+
238
+ // Dynamic height
239
+ <DataGrid data={data} maxHeight="calc(100vh - 200px)" />
240
+
241
+ // Just sticky headers (browser determines scroll)
242
+ <DataGrid data={data} stickyHeader={true} />
243
+
244
+ // Numeric value (converted to pixels)
245
+ <DataGrid data={data} maxHeight={500} />
246
+ ```
247
+
248
+ ## ๐ŸŽฏ Event System
249
+
250
+ ### Filter Events (NEW)
151
251
 
152
252
  ```tsx
153
253
  <DataGrid
154
- data={users}
155
- // Data loading events
156
- onDataLoad={(data) => {
157
- console.log('Data loaded:', data.items.length);
158
- hideLoadingSpinner();
254
+ data={data}
255
+ enableFilters={true}
256
+ // Called when "Apply Filter" is clicked
257
+ onApplyFilter={(filter, allFilters) => {
258
+ console.log('New filter:', filter);
259
+ console.log('All active filters:', allFilters);
260
+ // Make your API call here
261
+ }}
262
+ // Called when a filter tag X is clicked
263
+ onRemoveFilter={(removedFilter, remainingFilters) => {
264
+ console.log('Removed:', removedFilter);
265
+ // Refetch with remaining filters
159
266
  }}
160
- onDataError={(error, context) => {
161
- console.error(`Error in ${context}:`, error.message);
162
- showErrorToast(error.message);
267
+ // Called when "Clear All" is clicked
268
+ onClearFilters={() => {
269
+ console.log('All filters cleared');
270
+ // Refetch without filters
163
271
  }}
164
- onLoadingStateChange={(loading, context) => {
165
- setIsLoading(loading);
166
- console.log(`${context} loading: ${loading}`);
272
+ // Called on any filter change (convenience callback)
273
+ onFilterChange={(filters) => {
274
+ console.log('Filters changed:', filters.length);
167
275
  }}
168
- // Pagination events
276
+ />
277
+ ```
278
+
279
+ ### Pagination & Sort Events
280
+
281
+ ```tsx
282
+ <DataGrid
283
+ data={data}
169
284
  onPageChange={(page, paginationInfo) => {
170
285
  console.log(`Page ${page} of ${paginationInfo.totalPages}`);
171
- updateUrl(`?page=${page}`);
172
286
  }}
173
- onPageSizeChange={(pageSize, paginationInfo) => {
174
- console.log(`Showing ${pageSize} items per page`);
175
- saveUserPreference('pageSize', pageSize);
287
+ onPageSizeChange={(pageSize) => {
288
+ console.log(`Now showing ${pageSize} per page`);
176
289
  }}
177
- // Interaction events
178
290
  onSortChange={(sortConfig) => {
179
291
  console.log(`Sorted by ${sortConfig.column} ${sortConfig.direction}`);
180
- updateUrl(`?sort=${sortConfig.column}&order=${sortConfig.direction}`);
181
- }}
182
- onFilterChange={(filters) => {
183
- console.log(`${filters.length} filters active`);
184
- setBadgeCount(filters.length);
185
292
  }}
186
293
  onSearchChange={(searchTerm) => {
187
- console.log(`Searching for: "${searchTerm}"`);
188
- trackSearchQuery(searchTerm);
294
+ console.log(`Searching: "${searchTerm}"`);
189
295
  }}
190
296
  onTableRefresh={() => {
191
- console.log('Table refreshed');
192
- showSuccessMessage('Data refreshed');
297
+ console.log('Refresh clicked');
193
298
  }}
194
299
  />
195
300
  ```
196
301
 
197
- **Row & Cell Interaction Events**<br>
302
+ ### Row & Cell Events
198
303
 
199
304
  ```tsx
200
305
  <DataGrid
201
- data={users}
202
- // Row interaction events
306
+ data={data}
203
307
  onTableRowClick={(row, event) => {
204
- console.log('Row clicked:', row.name);
205
- highlightRow(row.id);
308
+ console.log('Clicked:', row);
206
309
  }}
207
310
  onTableRowDoubleClick={(row, event) => {
208
- console.log('Row double-clicked:', row.name);
209
311
  openEditModal(row);
210
- return false; // Prevent default selection behavior
312
+ return false; // Prevent selection toggle
211
313
  }}
212
314
  onTableRowHover={(row, event) => {
213
- if (row) {
214
- console.log('Hovering over:', row.name);
215
- showPreviewTooltip(row);
216
- } else {
217
- hidePreviewTooltip();
218
- }
315
+ row ? showTooltip(row) : hideTooltip();
219
316
  }}
220
- // Selection events
221
317
  onRowSelect={(row, isSelected) => {
222
318
  console.log(`${row.name} ${isSelected ? 'selected' : 'deselected'}`);
223
- updateRowActions(row, isSelected);
224
319
  }}
225
320
  onSelectionChange={(selectedRows) => {
226
- console.log(`${selectedRows.length} rows selected`);
227
321
  setBulkActionsEnabled(selectedRows.length > 0);
228
- updateSelectionToolbar(selectedRows);
229
322
  }}
230
- // Cell interaction events
231
323
  onCellClick={(value, row, column, event) => {
232
- console.log(`Clicked ${column.label}: ${value}`);
233
324
  if (column.key === 'email') {
234
325
  window.open(`mailto:${value}`);
235
326
  }
@@ -237,165 +328,105 @@ The DataGrid provides 15+ events covering every user interaction:
237
328
  />
238
329
  ```
239
330
 
240
- ## Real-World Event Usage Example
331
+ ## ๐Ÿ—‘๏ธ Delete Functionality
241
332
 
242
333
  ```tsx
243
- import React, { useState } from 'react';
244
- import { DataGrid } from '@reactorui/datagrid';
245
-
246
- function UserManagement() {
247
- const [loading, setLoading] = useState(false);
248
- const [selectedUsers, setSelectedUsers] = useState([]);
249
- const [currentPage, setCurrentPage] = useState(1);
250
-
251
- return (
252
- <div>
253
- {/* Bulk Actions Toolbar */}
254
- {selectedUsers.length > 0 && (
255
- <div className="bg-blue-50 p-4 mb-4 rounded">
256
- <span className="font-medium">{selectedUsers.length} users selected</span>
257
- <div className="ml-4 space-x-2">
258
- <button onClick={() => bulkExport(selectedUsers)}>Export</button>
259
- <button onClick={() => bulkDeactivate(selectedUsers)}>Deactivate</button>
260
- <button onClick={() => bulkDelete(selectedUsers)}>Delete</button>
261
- </div>
262
- </div>
263
- )}
264
-
265
- {/* DataGrid with comprehensive event handling */}
266
- <DataGrid
267
- endpoint="/api/users"
268
- enableSelection={true}
269
- // Sync with component state
270
- onLoadingStateChange={(loading) => setLoading(loading)}
271
- onPageChange={(page) => setCurrentPage(page)}
272
- onSelectionChange={(users) => setSelectedUsers(users)}
273
- // User interaction handlers
274
- onTableRowDoubleClick={(user) => openUserProfile(user.id)}
275
- onDataError={(error) => showErrorNotification(error.message)}
276
- onTableRefresh={() => showSuccessMessage('Users refreshed')}
277
- // Analytics tracking
278
- onSearchChange={(term) => analytics.track('users_searched', { term })}
279
- onSortChange={(sort) => analytics.track('users_sorted', { column: sort.column })}
280
- onFilterChange={(filters) => analytics.track('users_filtered', { count: filters.length })}
281
- />
282
-
283
- {/* Status indicators */}
284
- <div className="mt-4 text-sm text-gray-500">
285
- Page {currentPage} โ€ข {selectedUsers.length} selected โ€ข {loading ? 'Loading...' : 'Ready'}
286
- </div>
287
- </div>
288
- );
289
- }
334
+ <DataGrid
335
+ data={data}
336
+ enableSelection={true}
337
+ enableDelete={true}
338
+ deleteConfirmation={true} // Shows confirm dialog
339
+ loadingState={{ delete: isDeleting }}
340
+ onBulkDelete={async (selectedRows) => {
341
+ setLoadingState({ delete: true });
342
+ await deleteUsers(selectedRows.map((r) => r.id));
343
+ setLoadingState({});
344
+ refetchData();
345
+ }}
346
+ />
290
347
  ```
291
348
 
349
+ ## ๐Ÿ“‹ Props Reference
350
+
351
+ ### Data & State
352
+
353
+ | Prop | Type | Default | Description |
354
+ | -------------- | ---------------- | ------------- | ---------------------------------------- |
355
+ | `data` | `T[]` | **Required** | Array of data to display |
356
+ | `columns` | `Column<T>[]` | Auto-detected | Column definitions |
357
+ | `loading` | `boolean` | `false` | Simple loading state (backward compat) |
358
+ | `loadingState` | `LoadingState` | `{}` | Granular loading states |
359
+ | `totalRecords` | `number` | - | Total records for server-side pagination |
360
+ | `currentPage` | `number` | - | Controlled current page |
361
+ | `error` | `string \| null` | - | Error message to display |
362
+
363
+ ### Layout
364
+
365
+ | Prop | Type | Default | Description |
366
+ | -------------- | -------------------------------------- | ----------- | --------------------------------- |
367
+ | `maxHeight` | `string \| number` | - | Fixed height with scrollable body |
368
+ | `stickyHeader` | `boolean` | `false` | Enable sticky table header |
369
+ | `className` | `string` | `''` | Additional CSS classes |
370
+ | `variant` | `'default' \| 'striped' \| 'bordered'` | `'default'` | Visual theme |
371
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Padding/text size |
372
+
373
+ ### Features
374
+
375
+ | Prop | Type | Default | Description |
376
+ | -------------------- | --------- | ------- | --------------------- |
377
+ | `enableSearch` | `boolean` | `true` | Show search input |
378
+ | `enableSorting` | `boolean` | `true` | Enable column sorting |
379
+ | `enableFilters` | `boolean` | `true` | Show filter controls |
380
+ | `enableSelection` | `boolean` | `true` | Show row checkboxes |
381
+ | `enableDelete` | `boolean` | `false` | Show delete button |
382
+ | `enableRefresh` | `boolean` | `false` | Show refresh button |
383
+ | `deleteConfirmation` | `boolean` | `false` | Confirm before delete |
384
+
385
+ ### Pagination
386
+
387
+ | Prop | Type | Default | Description |
388
+ | ----------------- | ---------- | ------------------ | -------------------------- |
389
+ | `pageSize` | `number` | `10` | Items per page |
390
+ | `pageSizeOptions` | `number[]` | `[5,10,25,50,100]` | Page size dropdown options |
391
+
392
+ ### Event Callbacks
393
+
394
+ | Event | Signature | Description |
395
+ | ----------------------- | ------------------------------------- | -------------------- |
396
+ | `onApplyFilter` | `(filter, allFilters) => void` | Filter applied |
397
+ | `onRemoveFilter` | `(removed, remaining) => void` | Filter tag removed |
398
+ | `onClearFilters` | `() => void` | Clear All clicked |
399
+ | `onFilterChange` | `(filters) => void` | Any filter change |
400
+ | `onSearchChange` | `(term) => void` | Search input changed |
401
+ | `onSortChange` | `(sortConfig) => void` | Column sort changed |
402
+ | `onPageChange` | `(page, info) => void` | Page navigation |
403
+ | `onPageSizeChange` | `(size) => void` | Page size changed |
404
+ | `onTableRefresh` | `() => void` | Refresh clicked |
405
+ | `onTableRowClick` | `(row, event) => void` | Row clicked |
406
+ | `onTableRowDoubleClick` | `(row, event) => boolean \| void` | Row double-clicked |
407
+ | `onTableRowHover` | `(row \| null, event) => void` | Row hover |
408
+ | `onRowSelect` | `(row, isSelected) => void` | Single row selection |
409
+ | `onSelectionChange` | `(rows) => void` | Selection changed |
410
+ | `onCellClick` | `(value, row, column, event) => void` | Cell clicked |
411
+ | `onBulkDelete` | `(rows) => void` | Delete clicked |
412
+
292
413
  ## Column Configuration
293
414
 
294
415
  ```tsx
295
416
  interface Column<T> {
296
- key: keyof T | string; // Data property key
297
- label: string; // Display header label
298
- sortable?: boolean; // Enable sorting (default: true)
299
- filterable?: boolean; // Enable in advanced filters (default: true)
417
+ key: keyof T | string;
418
+ label: string;
419
+ sortable?: boolean; // Default: true
420
+ filterable?: boolean; // Default: true
300
421
  dataType?: 'string' | 'number' | 'boolean' | 'date' | 'datetime';
301
- width?: string | number; // Fixed column width
302
- minWidth?: string | number; // Minimum column width
303
- maxWidth?: string | number; // Maximum column width
422
+ width?: string | number;
423
+ minWidth?: string | number;
424
+ maxWidth?: string | number;
304
425
  align?: 'left' | 'center' | 'right';
305
426
  render?: (value: any, row: T, index: number) => ReactNode;
306
427
  }
307
428
  ```
308
429
 
309
- ## HTTP Configuration
310
-
311
- ```tsx
312
- interface HttpConfig {
313
- bearerToken?: string; // Authorization: Bearer <token>
314
- apiKey?: string; // X-API-Key header
315
- customHeaders?: Record<string, string>;
316
- method?: 'GET' | 'POST'; // HTTP method (default: GET)
317
- postDataFormat?: 'form' | 'json'; // POST body format
318
- withCredentials?: boolean; // Include cookies
319
- timeout?: number; // Request timeout in ms
320
- }
321
- ```
322
-
323
- # Server Request & Response Format
324
-
325
- **Request sent to your API:**
326
-
327
- ```tsx
328
- interface ServerRequest {
329
- page: number; // Current page number
330
- pageSize: number; // Items per page
331
- search: string; // Global search term
332
- sortColumn: string; // Column to sort by
333
- filters: ActiveFilter[]; // Applied filters
334
- continuationToken: // Token to grab more records (For server side pagination, if enabled)
335
- }
336
- ```
337
-
338
- **Expected response format:**
339
-
340
- ```tsx
341
- interface ServerResponse<T> {
342
- items: T[]; // Data array for current page
343
- count: number; // Total number of records
344
- hasMore: boolean; // Whether more pages available
345
- continuationToken: string; // Token to grab more records (For server side pagination, if enabled)
346
- }
347
- ```
348
-
349
- ## Event Callbacks
350
-
351
- | **Event** | **Signature** | **Description** |
352
- | ----------------------- | -------------------------------------------------------------------- | -------------------------------------------- |
353
- | **Data & State Events** | | |
354
- | `onDataLoad` | `(data: ServerResponse<T>) => void` | Called when server data loads successfully |
355
- | `onDataError` | `(error: Error, context: string) => void` | Called when any error occurs |
356
- | `onLoadingStateChange` | `(loading: boolean, context: string) => void` | Called when loading state changes |
357
- | `onPageChange` | `(page: number, paginationInfo: PaginationInfo) => void` | Called when user navigates pages |
358
- | `onPageSizeChange` | `(size: number, paginationInfo: PaginationInfo) => void` | Called when page size changes |
359
- | `onSortChange` | `(sortConfig: SortConfig) => void` | Called when sorting changes |
360
- | `onFilterChange` | `(filters: ActiveFilter[]) => void` | Called when filters change |
361
- | `onSearchChange` | `(searchTerm: string) => void` | Called when search term changes |
362
- | `onTableRefresh` | `() => void` | Called when refresh is triggered |
363
- | **Row & Cell Events** | | |
364
- | `onTableRowClick` | `(row: T, event: MouseEvent) => void` | Called on single row click |
365
- | `onTableRowDoubleClick` | `(row: T, event: MouseEvent) => boolean \| void` | Called on row double-click |
366
- | `onRowSelect` | `(row: T, isSelected: boolean) => void` | Called when individual row selection changes |
367
- | `onSelectionChange` | `(selectedRows: T[]) => void` | Called when overall selection changes |
368
- | `onBulkDelete` | `(rows: T[]) => void` | Called when delete button is clicked |
369
- | `onTableRowHover` | `(row: T \| null, event: MouseEvent) => void` | Called when hovering over rows |
370
- | `onCellClick` | `(value: any, row: T, column: Column<T>, event: MouseEvent) => void` | Called when clicking individual cells |
371
-
372
- ### **DataGrid Props**
373
-
374
- | **Prop** | **Type** | **Default** | **Description** |
375
- | ---------------------- | -------------------------------------- | ------------------ | ---------------------------------------- |
376
- | **Data Configuration** | | | |
377
- | `data` | `T[]` | โ€“ | Static data array for client-side mode |
378
- | `endpoint` | `string` | โ€“ | API endpoint for server-side data |
379
- | `columns` | `Column<T>[]` | Auto-detected | Column configuration array |
380
- | **Feature Toggles** | | | |
381
- | `enableSearch` | `boolean` | `true` | Enable global search functionality |
382
- | `enableSorting` | `boolean` | `true` | Enable column sorting |
383
- | `enableFilters` | `boolean` | `true` | Enable advanced filtering |
384
- | `enableSelection` | `boolean` | `true` | Enable row selection with checkboxes |
385
- | `enableRefresh` | `boolean` | `false` | Show/hide the refresh button |
386
- | `enableDelete` | `boolean` | `false` | Enable bulk delete functionality |
387
- | `deleteConfirmation` | `boolean` | `false` | Show confirmation dialog for delete |
388
- | **Pagination** | | | |
389
- | `pageSize` | `number` | `10` | Client-side pagination size |
390
- | `serverPageSize` | `number` | `100` | Server request batch size |
391
- | `pageSizeOptions` | `number[]` | `[5,10,25,50,100]` | Available page size options |
392
- | **Styling** | | | |
393
- | `variant` | `'default' \| 'striped' \| 'bordered'` | `'default'` | Visual theme variant |
394
- | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size variant for padding and text |
395
- | `className` | `string` | `''` | Additional CSS classes |
396
- | **HTTP Configuration** | | | |
397
- | `httpConfig` | `HttpConfig` | โ€“ | Authentication and request configuration |
398
-
399
430
  ## ๐ŸŽจ Theming & Styling
400
431
 
401
432
  ```tsx
@@ -407,374 +438,60 @@ interface ServerResponse<T> {
407
438
 
408
439
  // Full borders around cells
409
440
  <DataGrid variant="bordered" data={data} />
410
- ```
411
-
412
- **Size Variants**
413
-
414
- ```tsx
415
- // Compact spacing
416
- <DataGrid size="sm" data={data} />
417
441
 
418
- // Standard spacing (default)
419
- <DataGrid size="md" data={data} />
442
+ // Size variants
443
+ <DataGrid size="sm" data={data} /> // Compact
444
+ <DataGrid size="md" data={data} /> // Standard
445
+ <DataGrid size="lg" data={data} /> // Comfortable
420
446
 
421
- // Comfortable spacing
422
- <DataGrid size="lg" data={data} />
423
- ```
424
-
425
- **Dark Mode Support**<br>
426
- The DataGrid automatically adapts to dark mode when using Tailwind CSS:
427
-
428
- ```tsx
429
- // Wrap in dark mode provider
447
+ // Dark mode (automatic with Tailwind)
430
448
  <div className="dark">
431
449
  <DataGrid data={data} />
432
450
  </div>
433
451
  ```
434
452
 
435
- **Custom Styling**<br>
436
-
437
- ```tsx
438
- <DataGrid
439
- data={data}
440
- className="shadow-xl rounded-xl overflow-hidden"
441
- // Add custom CSS classes for complete control
442
- />
443
- ```
444
-
445
- ## Delete Functionality
446
-
447
- The delete button appears next to the search input and shows the selection count:
448
-
449
- <pre> ``` [Show 10 entries] ........................... [๐Ÿ” Search...][๐Ÿ—‘๏ธ (3 selected)] ``` </pre>
450
-
451
- - Button is **disabled** when no rows are selected
452
- - Shows **selection count** when items are selected
453
- - Supports **built-in confirmation dialog**
454
- - Only appears when **both** `enableDelete={true}` and `enableSelection={true}`
455
-
456
- ## ๐Ÿ”ง Advanced Usage
457
-
458
- **Custom Styling**<br>
459
-
460
- ```tsx
461
- const columns: Column<Employee>[] = [
462
- {
463
- key: 'employee',
464
- label: 'Employee',
465
- render: (_, employee) => (
466
- <div className="flex items-center space-x-3">
467
- <img src={employee.avatar} alt={employee.name} className="w-10 h-10 rounded-full" />
468
- <div>
469
- <div className="font-medium">{employee.name}</div>
470
- <div className="text-sm text-gray-500">{employee.title}</div>
471
- </div>
472
- </div>
473
- ),
474
- },
475
- {
476
- key: 'performance',
477
- label: 'Performance',
478
- render: (score) => (
479
- <div className="flex items-center">
480
- <div className="flex-1 bg-gray-200 rounded-full h-2 mr-2">
481
- <div className="bg-blue-500 h-2 rounded-full" style={{ width: `${score}%` }} />
482
- </div>
483
- <span className="text-sm font-medium">{score}%</span>
484
- </div>
485
- ),
486
- },
487
- ];
488
- ```
489
-
490
- **Advanced Filtering**
491
-
492
- ```tsx
493
- // The DataGrid automatically provides appropriate filter inputs:
494
- // - String: text input with contains/equals/starts with/ends with
495
- // - Number: number input with comparison operators
496
- // - Date: date picker with before/after/on
497
- // - Boolean: dropdown with true/false options
498
-
499
- const columns = [
500
- { key: 'name', label: 'Name', dataType: 'string' },
501
- { key: 'salary', label: 'Salary', dataType: 'number' },
502
- { key: 'startDate', label: 'Start Date', dataType: 'date' },
503
- { key: 'isActive', label: 'Active', dataType: 'boolean' },
504
- ];
505
- ```
506
-
507
- **Real-time Data with WebSockets**
508
-
509
- ```tsx
510
- function LiveDataGrid() {
511
- const [data, setData] = useState([]);
512
-
513
- useEffect(() => {
514
- const ws = new WebSocket('ws://localhost:8080');
515
- ws.onmessage = (event) => {
516
- const updatedData = JSON.parse(event.data);
517
- setData(updatedData);
518
- };
519
- return () => ws.close();
520
- }, []);
521
-
522
- return (
523
- <DataGrid
524
- data={data}
525
- onTableRefresh={() => {
526
- // Trigger server refresh
527
- fetch('/api/refresh', { method: 'POST' });
528
- }}
529
- />
530
- );
531
- }
532
- ```
533
-
534
- ## ๐ŸŒ Server Integration Examples
535
-
536
- **Node.js/Expresse**
537
-
538
- ```tsx
539
- app.post('/api/users', async (req, res) => {
540
- const { page, pageSize, search, filters, continuationToken } = JSON.parse(req.body.request);
541
-
542
- let query = User.find();
543
-
544
- // Apply search
545
- if (search) {
546
- query = query.where({
547
- $or: [
548
- { name: { $regex: search, $options: 'i' } },
549
- { email: { $regex: search, $options: 'i' } },
550
- ],
551
- });
552
- }
553
-
554
- // Apply filters
555
- filters.forEach((filter) => {
556
- query = query.where(filter.column)[getOperator(filter.operator)](filter.value);
557
- });
558
-
559
- // Handle continuation token (simple ID-based cursor)
560
- if (continuationToken) {
561
- const { lastId } = JSON.parse(Buffer.from(continuationToken, 'base64').toString());
562
- query = query.where('_id').gt(lastId);
563
- }
564
-
565
- // Execute query
566
- const items = await query.limit(pageSize + 1);
567
- const hasMore = items.length > pageSize;
568
- const resultItems = hasMore ? items.slice(0, pageSize) : items;
569
-
570
- // Generate next token
571
- let nextToken;
572
- if (hasMore && resultItems.length > 0) {
573
- const lastItem = resultItems[resultItems.length - 1];
574
- nextToken = Buffer.from(JSON.stringify({ lastId: lastItem._id })).toString('base64');
575
- }
576
-
577
- res.json({
578
- items: resultItems, // lowercase works
579
- continuationToken: nextToken, // camelCase works
580
- hasMore: hasMore, // camelCase works
581
- count: resultItems.length, // lowercase works
582
- });
583
- });
584
- ```
585
-
586
- **ASP.Net Core**
587
-
588
- ```tsx
589
- [HttpPost("api/users")]
590
- public async Task<IActionResult> GetUsers([FromBody] DataTableRequest request)
591
- {
592
- var query = _context.Users.AsQueryable();
593
-
594
- // Apply search
595
- if (!string.IsNullOrEmpty(request.Search))
596
- query = query.Where(u => u.Name.Contains(request.Search) || u.Email.Contains(request.Search));
597
-
598
- // Apply filters
599
- foreach (var filter in request.Filters)
600
- query = ApplyFilter(query, filter);
601
-
602
- // Handle continuation token
603
- if (!string.IsNullOrEmpty(request.ContinuationToken))
604
- {
605
- var token = JsonSerializer.Deserialize<ContinuationToken>(
606
- Encoding.UTF8.GetString(Convert.FromBase64String(request.ContinuationToken)));
607
- query = query.Where(u => u.Id > token.LastId);
608
- }
609
-
610
- query = query.OrderBy(u => u.Id);
611
- var items = await query.Take(request.PageSize + 1).ToListAsync();
612
- var hasMore = items.Count > request.PageSize;
613
- var resultItems = hasMore ? items.Take(request.PageSize).ToList() : items;
614
-
615
- string nextToken = null;
616
- if (hasMore && resultItems.Any())
617
- {
618
- var lastItem = resultItems.Last();
619
- var tokenData = new ContinuationToken { LastId = lastItem.Id };
620
- nextToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(tokenData)));
621
- }
622
-
623
- return Ok(new
624
- {
625
- Items = resultItems, // PascalCase works
626
- ContinuationToken = nextToken, // PascalCase works
627
- HasMore = hasMore, // PascalCase works
628
- Count = resultItems.Count // PascalCase works
629
- });
630
- }
631
- ```
632
-
633
- **Laravel/PHP**
634
-
635
- ```tsx
636
- Route::post('/api/users', function (Request $request) {
637
- $requestData = json_decode($request->input('request'), true);
638
- $query = User::query();
639
-
640
- // Apply search
641
- if (!empty($requestData['search'])) {
642
- $query->where(function($q) use ($requestData) {
643
- $q->where('name', 'like', "%{$requestData['search']}%")
644
- ->orWhere('email', 'like', "%{$requestData['search']}%");
645
- });
646
- }
647
-
648
- // Apply filters
649
- foreach ($requestData['filters'] as $filter) {
650
- $query->where($filter['column'], $filter['operator'], $filter['value']);
651
- }
652
-
653
- // Handle continuation token
654
- if (!empty($requestData['continuationToken'])) {
655
- $tokenData = json_decode(base64_decode($requestData['continuationToken']), true);
656
- $query->where('id', '>', $tokenData['lastId']);
657
- }
658
-
659
- $query->orderBy('id', 'asc');
660
- $items = $query->take($requestData['pageSize'] + 1)->get();
661
- $hasMore = $items->count() > $requestData['pageSize'];
662
- $resultItems = $hasMore ? $items->take($requestData['pageSize']) : $items;
663
-
664
- $nextToken = null;
665
- if ($hasMore && $resultItems->isNotEmpty()) {
666
- $lastItem = $resultItems->last();
667
- $nextToken = base64_encode(json_encode(['lastId' => $lastItem->id]));
668
- }
669
-
670
- return response()->json([
671
- 'data' => $resultItems->values(), // Laravel convention works
672
- 'continuation_token' => $nextToken, // snake_case works
673
- 'has_more' => $hasMore, // snake_case works
674
- 'total' => $resultItems->count() // Laravel convention works
675
- ]);
676
- });
677
- ```
678
-
679
453
  ## ๐Ÿงช Testing
680
454
 
681
455
  ```bash
682
- # Run test suite
683
- npm test
684
-
685
- # Watch mode for development
686
- npm run test:watch
687
-
688
- # Coverage report
689
- npm run test:coverage
456
+ npm test # Run tests
457
+ npm run test:watch # Watch mode
458
+ npm run test:coverage # Coverage report
690
459
  ```
691
460
 
692
- **Testing with Jest & React Testing Library**
693
-
694
461
  ```tsx
695
462
  import { render, screen, fireEvent } from '@testing-library/react';
696
463
  import { DataGrid } from '@reactorui/datagrid';
697
464
 
698
- test('handles user interactions', () => {
699
- const onSelectionChange = jest.fn();
700
- const testData = [{ id: 1, name: 'John', email: 'john@test.com' }];
465
+ test('handles filter application', async () => {
466
+ const onApplyFilter = jest.fn();
467
+ render(<DataGrid data={testData} enableFilters={true} onApplyFilter={onApplyFilter} />);
701
468
 
702
- render(<DataGrid data={testData} onSelectionChange={onSelectionChange} />);
469
+ // Select column, enter value, click Apply
470
+ const selects = screen.getAllByRole('combobox');
471
+ fireEvent.change(selects[0], { target: { value: 'name' } });
703
472
 
704
- // Test search
705
- fireEvent.change(screen.getByPlaceholderText('Search...'), {
706
- target: { value: 'John' },
707
- });
473
+ const input = screen.getByPlaceholderText('Enter value');
474
+ fireEvent.change(input, { target: { value: 'John' } });
708
475
 
709
- // Test selection
710
- fireEvent.click(screen.getAllByRole('checkbox')[1]);
476
+ fireEvent.click(screen.getByRole('button', { name: /apply filter/i }));
711
477
 
712
- expect(onSelectionChange).toHaveBeenCalledWith([testData[0]]);
478
+ expect(onApplyFilter).toHaveBeenCalledWith(
479
+ expect.objectContaining({ column: 'name', value: 'John' }),
480
+ expect.any(Array)
481
+ );
713
482
  });
714
483
  ```
715
484
 
716
- ## ๐Ÿ“š Examples
717
-
718
- Check out the examples/ directory for complete working examples:
719
-
720
- examples/basic/ - Simple usage with auto-detected columns
721
- examples/advanced/ - Custom columns, renderers, and styling
722
- examples/events/ - Comprehensive event handling demonstration
723
-
724
- ## ๐Ÿš€ Performance Tips
725
-
726
- Use server-side pagination for datasets > 1000 records
727
- Implement custom renderers efficiently with React.memo
728
- Debounce search for better UX with large datasets
729
- Use specific column keys instead of auto-detection for better performance
730
-
731
- ```tsx
732
- const StatusBadge = React.memo(({ status }: { status: string }) => (
733
- <span className={`badge ${status === 'active' ? 'bg-green' : 'bg-red'}`}>{status}</span>
734
- ));
735
-
736
- // Debounced search
737
- const [debouncedSearch] = useDebounce(searchTerm, 300);
738
- ```
739
-
740
485
  ## ๐Ÿ”ง Development
741
486
 
742
487
  ```bash
743
- # Install dependencies
744
- npm install
745
-
746
- # Run tests
747
- npm test
748
-
749
- # Build library
750
- npm run build
751
-
752
- # Type checking
753
- npm run typecheck
754
-
755
- # Linting
756
- npm run lint
757
-
758
- # Format code
759
- npm run format
488
+ npm install # Install dependencies
489
+ npm test # Run tests
490
+ npm run build # Build library
491
+ npm run typecheck # Type checking
492
+ npm run lint # Linting
760
493
  ```
761
494
 
762
- ## ๐Ÿค Contributing
763
-
764
- We welcome contributions! Please see our Contributing Guide for details.
765
-
766
- Fork the repository
767
- Create your feature branch (git checkout -b feature/amazing-feature)
768
- Write tests for your changes
769
- Commit your changes (git commit -m 'Add amazing feature')
770
- Push to the branch (git push origin feature/amazing-feature)
771
- Open a Pull Request
772
-
773
- ## Support
774
-
775
- - ๐Ÿ“– **Documentation**: Check this README and inline TypeScript types
776
- - ๐Ÿ› **Bug Reports**: [GitHub Issues](https://github.com/ReactorUI/datagrid/issues)
777
-
778
495
  ## ๐Ÿ“„ License
779
496
 
780
497
  MIT License - see [LICENSE](LICENSE) file for details.
@@ -783,7 +500,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
783
500
 
784
501
  Part of the ReactorUI ecosystem:
785
502
 
786
- - ๐Ÿ“Š [@reactorui/recurrence](https://www.npmjs.com/package/@reactorui/recurrence) - A powerful, flexible recurrence rule builder for React applications
503
+ - ๐Ÿ“Š [@reactorui/recurrence](https://www.npmjs.com/package/@reactorui/recurrence) - Recurrence rule builder
787
504
  - ๐Ÿ”œ More components coming soon!
788
505
 
789
506
  ---