@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 +273 -556
- package/dist/components/DataGrid/DataGrid.d.ts +1 -1
- package/dist/components/DataGrid/DataGrid.d.ts.map +1 -1
- package/dist/components/DataGrid/DataGrid.js +82 -47
- package/dist/components/Filter/FilterControls.d.ts +6 -2
- package/dist/components/Filter/FilterControls.d.ts.map +1 -1
- package/dist/components/Filter/FilterControls.js +114 -62
- package/dist/components/Table/TableBody.d.ts.map +1 -1
- package/dist/components/Table/TableBody.js +3 -3
- package/dist/components/Table/TableHeader.d.ts +2 -1
- package/dist/components/Table/TableHeader.d.ts.map +1 -1
- package/dist/components/Table/TableHeader.js +4 -2
- package/dist/hooks/useDataGrid.d.ts +16 -18
- package/dist/hooks/useDataGrid.d.ts.map +1 -1
- package/dist/hooks/useDataGrid.js +139 -299
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/types/index.d.ts +76 -38
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/utils/index.d.ts +14 -5
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +166 -156
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @reactorui/datagrid
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@reactorui/datagrid)
|
|
4
4
|
[](https://www.npmjs.com/package/@reactorui/datagrid)
|
|
5
5
|
[](https://github.com/your-org/datagrid/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
A high-performance, feature-rich React data grid component with TypeScript support,
|
|
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
|
|
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** -
|
|
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
|
|
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** -
|
|
30
|
-
-
|
|
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="
|
|
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 {
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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={
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
267
|
+
// Called when "Clear All" is clicked
|
|
268
|
+
onClearFilters={() => {
|
|
269
|
+
console.log('All filters cleared');
|
|
270
|
+
// Refetch without filters
|
|
163
271
|
}}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
console.log(
|
|
272
|
+
// Called on any filter change (convenience callback)
|
|
273
|
+
onFilterChange={(filters) => {
|
|
274
|
+
console.log('Filters changed:', filters.length);
|
|
167
275
|
}}
|
|
168
|
-
|
|
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
|
|
174
|
-
console.log(`
|
|
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
|
|
188
|
-
trackSearchQuery(searchTerm);
|
|
294
|
+
console.log(`Searching: "${searchTerm}"`);
|
|
189
295
|
}}
|
|
190
296
|
onTableRefresh={() => {
|
|
191
|
-
console.log('
|
|
192
|
-
showSuccessMessage('Data refreshed');
|
|
297
|
+
console.log('Refresh clicked');
|
|
193
298
|
}}
|
|
194
299
|
/>
|
|
195
300
|
```
|
|
196
301
|
|
|
197
|
-
|
|
302
|
+
### Row & Cell Events
|
|
198
303
|
|
|
199
304
|
```tsx
|
|
200
305
|
<DataGrid
|
|
201
|
-
data={
|
|
202
|
-
// Row interaction events
|
|
306
|
+
data={data}
|
|
203
307
|
onTableRowClick={(row, event) => {
|
|
204
|
-
console.log('
|
|
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
|
|
312
|
+
return false; // Prevent selection toggle
|
|
211
313
|
}}
|
|
212
314
|
onTableRowHover={(row, event) => {
|
|
213
|
-
|
|
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
|
-
##
|
|
331
|
+
## ๐๏ธ Delete Functionality
|
|
241
332
|
|
|
242
333
|
```tsx
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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;
|
|
297
|
-
label: string;
|
|
298
|
-
sortable?: boolean; //
|
|
299
|
-
filterable?: boolean; //
|
|
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;
|
|
302
|
-
minWidth?: string | number;
|
|
303
|
-
maxWidth?: string | number;
|
|
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
|
-
//
|
|
419
|
-
<DataGrid size="
|
|
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
|
-
//
|
|
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
|
|
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
|
|
699
|
-
const
|
|
700
|
-
|
|
465
|
+
test('handles filter application', async () => {
|
|
466
|
+
const onApplyFilter = jest.fn();
|
|
467
|
+
render(<DataGrid data={testData} enableFilters={true} onApplyFilter={onApplyFilter} />);
|
|
701
468
|
|
|
702
|
-
|
|
469
|
+
// Select column, enter value, click Apply
|
|
470
|
+
const selects = screen.getAllByRole('combobox');
|
|
471
|
+
fireEvent.change(selects[0], { target: { value: 'name' } });
|
|
703
472
|
|
|
704
|
-
|
|
705
|
-
fireEvent.change(
|
|
706
|
-
target: { value: 'John' },
|
|
707
|
-
});
|
|
473
|
+
const input = screen.getByPlaceholderText('Enter value');
|
|
474
|
+
fireEvent.change(input, { target: { value: 'John' } });
|
|
708
475
|
|
|
709
|
-
|
|
710
|
-
fireEvent.click(screen.getAllByRole('checkbox')[1]);
|
|
476
|
+
fireEvent.click(screen.getByRole('button', { name: /apply filter/i }));
|
|
711
477
|
|
|
712
|
-
expect(
|
|
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
|
|
745
|
-
|
|
746
|
-
#
|
|
747
|
-
npm
|
|
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) -
|
|
503
|
+
- ๐ [@reactorui/recurrence](https://www.npmjs.com/package/@reactorui/recurrence) - Recurrence rule builder
|
|
787
504
|
- ๐ More components coming soon!
|
|
788
505
|
|
|
789
506
|
---
|