@ceed/cds 1.18.0 → 1.19.0-next.1
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/dist/components/data-display/DataTable.md +1138 -234
- package/dist/index.cjs +77 -9
- package/dist/index.js +77 -9
- package/framer/index.js +32 -32
- package/package.json +3 -2
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# DataTable
|
|
2
2
|
|
|
3
|
-
DataTable
|
|
3
|
+
DataTable is a table component that displays structured data and provides various interactions such as sorting, selection, editing, and pagination.
|
|
4
|
+
The examples below can be tested in the story canvas, and the displayed code can be reused as-is.
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
+
## Basic Usage
|
|
6
7
|
|
|
7
8
|
```tsx
|
|
8
9
|
<DataTable
|
|
@@ -38,116 +39,1091 @@ DataTable은 구조화된 데이터를 효율적으로 표시하고 상호작용
|
|
|
38
39
|
| -------- | ----------- | ------- |
|
|
39
40
|
| editMode | — | false |
|
|
40
41
|
|
|
42
|
+
### Required Data Structure
|
|
43
|
+
|
|
44
|
+
- `rows` is an array, and by default, each row must include an `id` field.
|
|
45
|
+
- If there is no `id`, you can specify a row identifier using `getId`.
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
<DataTable
|
|
49
|
+
rows={rows}
|
|
50
|
+
columns={columns}
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
<DataTable
|
|
54
|
+
rows={rows}
|
|
55
|
+
columns={columns}
|
|
56
|
+
getId={(row) => row.uuid}
|
|
57
|
+
/>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Column Definition (ColumnDef)
|
|
61
|
+
|
|
62
|
+
### Basic Options
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
const columns = [
|
|
66
|
+
{
|
|
67
|
+
field: 'name',
|
|
68
|
+
headerName: 'Name',
|
|
69
|
+
width: '30%',
|
|
70
|
+
minWidth: '120px',
|
|
71
|
+
maxWidth: '320px',
|
|
72
|
+
sortable: true,
|
|
73
|
+
description: 'Description displayed as a tooltip',
|
|
74
|
+
headerClassName: 'table-head',
|
|
75
|
+
cellClassName: ({ row }) => (row.isHot ? 'cell-hot' : undefined),
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Column Types
|
|
81
|
+
|
|
82
|
+
DataTable supports various column types. Each type provides an appropriate input UI in edit mode.
|
|
83
|
+
|
|
84
|
+
#### text (default)
|
|
85
|
+
|
|
86
|
+
The default text type. If `type` is not specified, it defaults to text.
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
{
|
|
90
|
+
field: 'name',
|
|
91
|
+
headerName: 'Name',
|
|
92
|
+
type: 'text', // can be omitted
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### number
|
|
97
|
+
|
|
98
|
+
A type for numeric input.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
{
|
|
102
|
+
field: 'quantity',
|
|
103
|
+
headerName: 'Quantity',
|
|
104
|
+
type: 'number',
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### currency
|
|
109
|
+
|
|
110
|
+
A type for currency input. Uses the `CurrencyInput` component.
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
<DataTable rows={tableRows} columns={columns} editMode noWrap />
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
{
|
|
118
|
+
field: 'price',
|
|
119
|
+
headerName: 'Price',
|
|
120
|
+
type: 'currency',
|
|
121
|
+
componentProps: {
|
|
122
|
+
currencyDisplay: 'narrowSymbol',
|
|
123
|
+
currency: 'USD',
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### date
|
|
129
|
+
|
|
130
|
+
A type for date selection. Uses the `DatePicker` component.
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
<DataTable rows={tableRows} columns={columns} editMode noWrap />
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
{
|
|
138
|
+
field: 'dueDate',
|
|
139
|
+
headerName: 'Due Date',
|
|
140
|
+
type: 'date',
|
|
141
|
+
componentProps: {
|
|
142
|
+
format: 'yyyy-MM-dd',
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### select
|
|
148
|
+
|
|
149
|
+
A type for dropdown selection. Uses the `Select` component.
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
<DataTable rows={tableRows} columns={columns} editMode noWrap />
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
{
|
|
157
|
+
field: 'status',
|
|
158
|
+
headerName: 'Status',
|
|
159
|
+
type: 'select',
|
|
160
|
+
componentProps: {
|
|
161
|
+
options: ['Pending', 'In Progress', 'Completed', 'Cancelled'],
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### autocomplete
|
|
167
|
+
|
|
168
|
+
A type for autocomplete selection. Uses the `Autocomplete` component.
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
<DataTable rows={tableRows} columns={columns} editMode noWrap />
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
{
|
|
176
|
+
field: 'assignee',
|
|
177
|
+
headerName: 'Assignee',
|
|
178
|
+
type: 'autocomplete',
|
|
179
|
+
componentProps: {
|
|
180
|
+
options: ['Alice', 'Bob', 'Charlie', 'Diana'],
|
|
181
|
+
placeholder: 'Select assignee...',
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### longText
|
|
187
|
+
|
|
188
|
+
A type for long text input. Uses the `Textarea` component.
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
<DataTable rows={tableRows} columns={columns} editMode noWrap />
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
{
|
|
196
|
+
field: 'description',
|
|
197
|
+
headerName: 'Description',
|
|
198
|
+
type: 'longText',
|
|
199
|
+
componentProps: {
|
|
200
|
+
minRows: 2,
|
|
201
|
+
maxRows: 4,
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### link
|
|
207
|
+
|
|
208
|
+
A link type. Renders as a clickable link.
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
<DataTable rows={tableRows} columns={columns} noWrap />
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
{
|
|
216
|
+
field: 'website',
|
|
217
|
+
headerName: 'Website',
|
|
218
|
+
type: 'link',
|
|
219
|
+
componentProps: ({ row }) => ({
|
|
220
|
+
href: row.website,
|
|
221
|
+
target: '_blank',
|
|
222
|
+
}),
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### actions
|
|
227
|
+
|
|
228
|
+
A type for row-level action buttons.
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
<DataTable rows={rows4} columns={columns} pinnedColumns={{
|
|
232
|
+
right: ['actions']
|
|
233
|
+
}} />
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
{
|
|
238
|
+
field: 'actions',
|
|
239
|
+
type: 'actions',
|
|
240
|
+
getActions: ({ row }) => [
|
|
241
|
+
<Button key="view" size="sm">View</Button>,
|
|
242
|
+
<Button key="delete" size="sm" color="danger">Delete</Button>,
|
|
243
|
+
],
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Using componentProps
|
|
248
|
+
|
|
249
|
+
`componentProps` can be specified as an object or a function. When specified as a function, you can dynamically set props based on row data.
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
// Static props
|
|
253
|
+
componentProps: {
|
|
254
|
+
options: ['A', 'B', 'C'],
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Dynamic props (function)
|
|
258
|
+
componentProps: ({ row, id }) => ({
|
|
259
|
+
options: row.availableOptions,
|
|
260
|
+
disabled: row.isLocked,
|
|
261
|
+
})
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Row Interactions
|
|
265
|
+
|
|
266
|
+
### Row Hover
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
<DataTable {...args} />
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Checkbox Selection
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
<DataTable {...args} />
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
#### Controlling Selection Model
|
|
279
|
+
|
|
280
|
+
To control checkbox selection, use `selectionModel` and `onSelectionModelChange` together.
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
const [selectionModel, setSelectionModel] = useState<string[]>([]);
|
|
284
|
+
|
|
285
|
+
<DataTable
|
|
286
|
+
rows={rows}
|
|
287
|
+
columns={columns}
|
|
288
|
+
checkboxSelection
|
|
289
|
+
selectionModel={selectionModel}
|
|
290
|
+
onSelectionModelChange={setSelectionModel}
|
|
291
|
+
/>;
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### Notes on Selection Control
|
|
295
|
+
|
|
296
|
+
- `isRowSelectable` can control whether each row is selectable.
|
|
297
|
+
- `disableSelectionOnClick` prevents selection when clicking on a row.
|
|
298
|
+
- `isTotalSelected` is used to represent the total selection state in server pagination scenarios.
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
<DataTable
|
|
302
|
+
rows={rows}
|
|
303
|
+
columns={columns}
|
|
304
|
+
checkboxSelection
|
|
305
|
+
isRowSelectable={({ row }) => row.status !== 'Closed'}
|
|
306
|
+
disableSelectionOnClick
|
|
307
|
+
isTotalSelected
|
|
308
|
+
/>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Applying Selection Conditions
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
<Stack height="500px" gap={1}>
|
|
315
|
+
<Typography level="title-sm" textColor="text.secondary">
|
|
316
|
+
status가 Closed인 행은 선택할 수 없습니다.
|
|
317
|
+
</Typography>
|
|
318
|
+
<DataTable checkboxSelection stripe="even" hoverRow stickyHeader rows={rows3} columns={columns} selectionModel={selectionModel} onSelectionModelChange={newSelection => setSelectionModel(newSelection)} isRowSelectable={({
|
|
319
|
+
row
|
|
320
|
+
}) => row.status !== 'Closed'} />
|
|
321
|
+
</Stack>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Disabling Click Selection
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
<Stack height="500px">
|
|
328
|
+
<Typography level="title-sm" textColor="text.secondary">
|
|
329
|
+
Disable selection on click.
|
|
330
|
+
</Typography>
|
|
331
|
+
<DataTable checkboxSelection stripe="even" hoverRow selectionModel={selectedId} stickyHeader onSelectionModelChange={newSelection => setSelectedId(newSelection)} disableSelectionOnClick rows={rows4} columns={columns} />
|
|
332
|
+
</Stack>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Total Selection (isTotalSelected)
|
|
336
|
+
|
|
337
|
+
Used to manage the total selection state including data beyond the current page in server pagination environments.
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
<Stack gap={2}>
|
|
341
|
+
<Box sx={{
|
|
342
|
+
p: 2,
|
|
343
|
+
bgcolor: 'primary.50',
|
|
344
|
+
borderRadius: '8px'
|
|
345
|
+
}}>
|
|
346
|
+
<Typography level="body-sm">
|
|
347
|
+
{isTotalSelected ? `All ${allRows.length} items are selected (across all pages)` : `${selectionModel.length} items selected on current page`}
|
|
348
|
+
</Typography>
|
|
349
|
+
</Box>
|
|
350
|
+
<DataTable rows={pagedRows} columns={columns} checkboxSelection selectionModel={selectionModel} onSelectionModelChange={(newSelection, totalSelected) => {
|
|
351
|
+
setSelectionModel(newSelection);
|
|
352
|
+
if (totalSelected !== undefined) {
|
|
353
|
+
setIsTotalSelected(totalSelected);
|
|
354
|
+
}
|
|
355
|
+
}} isTotalSelected={isTotalSelected} pagination paginationMode="server" rowCount={allRows.length} paginationModel={paginationModel} onPaginationModelChange={setPaginationModel} noWrap />
|
|
356
|
+
</Stack>
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
const [isTotalSelected, setIsTotalSelected] = useState(false);
|
|
361
|
+
const [selectionModel, setSelectionModel] = useState<string[]>([]);
|
|
362
|
+
|
|
363
|
+
<DataTable
|
|
364
|
+
rows={rows}
|
|
365
|
+
columns={columns}
|
|
366
|
+
checkboxSelection
|
|
367
|
+
pagination
|
|
368
|
+
paginationMode="server"
|
|
369
|
+
isTotalSelected={isTotalSelected}
|
|
370
|
+
selectionModel={selectionModel}
|
|
371
|
+
onSelectionModelChange={(newModel, totalSelected) => {
|
|
372
|
+
setSelectionModel(newModel);
|
|
373
|
+
if (totalSelected !== undefined) {
|
|
374
|
+
setIsTotalSelected(totalSelected);
|
|
375
|
+
}
|
|
376
|
+
}}
|
|
377
|
+
/>;
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Styles and Layout
|
|
381
|
+
|
|
382
|
+
### Back Office Style
|
|
383
|
+
|
|
384
|
+
```tsx
|
|
385
|
+
<DataTable rows={args.rows} columns={args.columns} checkboxSelection={args.checkboxSelection} hoverRow={args.hoverRow} noWrap={args.noWrap} stripe={args.stripe} stickyHeader={args.stickyHeader} slots={args.slots} slotProps={args.slotProps} selectionModel={selectionModel} onSelectionModelChange={setSelectionModel} />
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
#### Commonly Used Table Options
|
|
389
|
+
|
|
390
|
+
- `stickyHeader`: Fixes the header
|
|
391
|
+
- `stripe`: Row striping (`"even" | "odd"`)
|
|
392
|
+
- `noWrap`: Prevents cell text wrapping
|
|
393
|
+
- `slots`, `slotProps`: Customize toolbar/footer/loading overlay/background
|
|
394
|
+
|
|
395
|
+
```tsx
|
|
396
|
+
<DataTable
|
|
397
|
+
rows={rows}
|
|
398
|
+
columns={columns}
|
|
399
|
+
stickyHeader
|
|
400
|
+
stripe="even"
|
|
401
|
+
noWrap
|
|
402
|
+
slots={{
|
|
403
|
+
toolbar: CustomToolbar,
|
|
404
|
+
footer: CustomFooter,
|
|
405
|
+
loadingOverlay: CustomLoading,
|
|
406
|
+
}}
|
|
407
|
+
slotProps={{
|
|
408
|
+
background: { style: { height: '300px' } },
|
|
409
|
+
}}
|
|
410
|
+
/>
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Loading State
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
<DataTable {...args} />
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Virtual Scrolling for Large Data
|
|
420
|
+
|
|
421
|
+
```tsx
|
|
422
|
+
<Stack height="500px">
|
|
423
|
+
<Typography level="title-sm" textColor="text.secondary">
|
|
424
|
+
Virtualized DataTable
|
|
425
|
+
</Typography>
|
|
426
|
+
<DataTable checkboxSelection stripe="even" hoverRow selectionModel={selectedId} stickyHeader onSelectionModelChange={newSelection => setSelectedId(newSelection)} rows={[...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4].map((row, i) => ({
|
|
427
|
+
...row,
|
|
428
|
+
id: i
|
|
429
|
+
}))} columns={columns} sortModel={[{
|
|
430
|
+
field: 'number',
|
|
431
|
+
sort: 'asc'
|
|
432
|
+
}]} />
|
|
433
|
+
</Stack>
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
## Slots & SlotProps Customization
|
|
437
|
+
|
|
438
|
+
DataTable allows customization of various parts through `slots` and `slotProps`.
|
|
439
|
+
|
|
440
|
+
### Available Slots
|
|
441
|
+
|
|
442
|
+
| Slot | Description |
|
|
443
|
+
| ---------------- | ---------------------------- |
|
|
444
|
+
| `checkbox` | Customize checkbox component |
|
|
445
|
+
| `toolbar` | Table top toolbar |
|
|
446
|
+
| `footer` | Table bottom footer |
|
|
447
|
+
| `loadingOverlay` | Loading overlay |
|
|
448
|
+
|
|
449
|
+
### Custom Toolbar
|
|
450
|
+
|
|
451
|
+
```tsx
|
|
452
|
+
<DataTable rows={filteredRows} columns={columns} slots={{
|
|
453
|
+
toolbar: CustomToolbar
|
|
454
|
+
}} noWrap stickyHeader stripe="even" />
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
```tsx
|
|
458
|
+
const CustomToolbar = () => (
|
|
459
|
+
<Box sx={{ p: 2, display: 'flex', gap: 1, borderBottom: '1px solid', borderColor: 'neutral.200' }}>
|
|
460
|
+
<Button size="sm">Export</Button>
|
|
461
|
+
<Button size="sm">Filter</Button>
|
|
462
|
+
</Box>
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
<DataTable rows={rows} columns={columns} slots={{ toolbar: CustomToolbar }} />;
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Custom Footer
|
|
469
|
+
|
|
470
|
+
```tsx
|
|
471
|
+
<DataTable rows={tableRows} columns={columns} slots={{
|
|
472
|
+
footer: CustomFooter
|
|
473
|
+
}} noWrap />
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
```tsx
|
|
477
|
+
const CustomFooter = () => (
|
|
478
|
+
<Box sx={{ p: 2, textAlign: 'center', borderTop: '1px solid', borderColor: 'neutral.200' }}>
|
|
479
|
+
<Typography level="body-sm">Total {rows.length} items</Typography>
|
|
480
|
+
</Box>
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
<DataTable rows={rows} columns={columns} slots={{ footer: CustomFooter }} />;
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Custom Loading Overlay
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
<Stack gap={2}>
|
|
490
|
+
<Button onClick={() => setLoading(!loading)}>{loading ? 'Stop Loading' : 'Start Loading'}</Button>
|
|
491
|
+
<DataTable rows={tableRows} columns={columns} loading={loading} slots={{
|
|
492
|
+
loadingOverlay: CustomLoadingOverlay
|
|
493
|
+
}} noWrap slotProps={{
|
|
494
|
+
background: {
|
|
495
|
+
style: {
|
|
496
|
+
height: '300px'
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}} />
|
|
500
|
+
</Stack>
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
```tsx
|
|
504
|
+
const CustomLoadingOverlay = () => (
|
|
505
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
|
|
506
|
+
<CircularProgress />
|
|
507
|
+
<Typography>Loading data...</Typography>
|
|
508
|
+
</Box>
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
<DataTable rows={rows} columns={columns} loading slots={{ loadingOverlay: CustomLoadingOverlay }} />;
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Custom Checkbox
|
|
515
|
+
|
|
516
|
+
```tsx
|
|
517
|
+
<DataTable rows={tableRows} columns={columns} checkboxSelection selectionModel={selectionModel} onSelectionModelChange={newSelection => setSelectionModel(newSelection)} slots={{
|
|
518
|
+
checkbox: CustomCheckbox
|
|
519
|
+
}} noWrap />
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
```tsx
|
|
523
|
+
const CustomCheckbox = (props) => (
|
|
524
|
+
<Box
|
|
525
|
+
onClick={props.onChange}
|
|
526
|
+
sx={{
|
|
527
|
+
width: 20,
|
|
528
|
+
height: 20,
|
|
529
|
+
borderRadius: '4px',
|
|
530
|
+
border: '2px solid',
|
|
531
|
+
borderColor: props.checked ? 'primary.500' : 'neutral.400',
|
|
532
|
+
backgroundColor: props.checked ? 'primary.500' : 'transparent',
|
|
533
|
+
cursor: 'pointer',
|
|
534
|
+
}}
|
|
535
|
+
>
|
|
536
|
+
{props.checked && <span>✓</span>}
|
|
537
|
+
</Box>
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
<DataTable rows={rows} columns={columns} checkboxSelection slots={{ checkbox: CustomCheckbox }} />;
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Background Styling (slotProps.background)
|
|
544
|
+
|
|
545
|
+
You can style the table background area using `slotProps.background`. Height specification is required when used with `stickyHeader`.
|
|
546
|
+
|
|
547
|
+
```tsx
|
|
548
|
+
<DataTable
|
|
549
|
+
rows={rows}
|
|
550
|
+
columns={columns}
|
|
551
|
+
stickyHeader
|
|
552
|
+
slotProps={{
|
|
553
|
+
background: {
|
|
554
|
+
style: { height: '400px' },
|
|
555
|
+
},
|
|
556
|
+
}}
|
|
557
|
+
/>
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
## Column Configuration
|
|
561
|
+
|
|
562
|
+
### Displaying id Column
|
|
563
|
+
|
|
564
|
+
```tsx
|
|
565
|
+
<DataTable {...args} />
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Customizing Column Rendering
|
|
569
|
+
|
|
570
|
+
```tsx
|
|
571
|
+
<DataTable rows={useMemo(() => [...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3, ...rows3].map((row, i) => ({
|
|
572
|
+
...row,
|
|
573
|
+
id: i + 1
|
|
574
|
+
})), [])} columns={columns} checkboxSelection stickyHeader stripe="even" noWrap slotProps={{
|
|
575
|
+
background: {
|
|
576
|
+
style: {
|
|
577
|
+
height: '600px'
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}} />
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Complex UI in Cells
|
|
584
|
+
|
|
585
|
+
```tsx
|
|
586
|
+
<DataTable rows={rows4} columns={columns} />
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Description (tooltip)
|
|
590
|
+
|
|
591
|
+
```tsx
|
|
592
|
+
<DataTable rows={rows4} columns={columns} />
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### Customizing Cell/Header Classes
|
|
596
|
+
|
|
597
|
+
```tsx
|
|
598
|
+
<DataTable
|
|
599
|
+
onPaginationModelChange={fn()}
|
|
600
|
+
onSelectionModelChange={fn()}
|
|
601
|
+
onRowClick={fn()}
|
|
602
|
+
columns={[{
|
|
603
|
+
field: 'id',
|
|
604
|
+
headerName: 'ID',
|
|
605
|
+
width: '150px',
|
|
606
|
+
cellClassName: ({
|
|
607
|
+
row
|
|
608
|
+
}) => row.number > 5 ? 'red' : 'blue'
|
|
609
|
+
}, {
|
|
610
|
+
field: 'number',
|
|
611
|
+
headerName: 'Number',
|
|
612
|
+
type: 'number',
|
|
613
|
+
width: '150px',
|
|
614
|
+
cellClassName: ({
|
|
615
|
+
value
|
|
616
|
+
}) => value > 5 ? 'red' : 'blue'
|
|
617
|
+
}, {
|
|
618
|
+
field: 'string',
|
|
619
|
+
headerName: 'String',
|
|
620
|
+
width: '600px',
|
|
621
|
+
cellClassName: ({
|
|
622
|
+
row
|
|
623
|
+
}) => row.number > 5 ? 'red' : 'blue'
|
|
624
|
+
}]}
|
|
625
|
+
rows={rows4}
|
|
626
|
+
/>
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
```tsx
|
|
630
|
+
<DataTable
|
|
631
|
+
onPaginationModelChange={fn()}
|
|
632
|
+
onSelectionModelChange={fn()}
|
|
633
|
+
onRowClick={fn()}
|
|
634
|
+
columns={[{
|
|
635
|
+
field: 'id',
|
|
636
|
+
headerName: 'ID',
|
|
637
|
+
width: '150px',
|
|
638
|
+
headerClassName: 'red'
|
|
639
|
+
}, {
|
|
640
|
+
field: 'number',
|
|
641
|
+
headerName: 'Number',
|
|
642
|
+
type: 'number',
|
|
643
|
+
width: '150px',
|
|
644
|
+
headerClassName: 'red'
|
|
645
|
+
}, {
|
|
646
|
+
field: 'string',
|
|
647
|
+
headerName: 'String',
|
|
648
|
+
width: '600px',
|
|
649
|
+
headerClassName: 'red'
|
|
650
|
+
}, {
|
|
651
|
+
field: 'date',
|
|
652
|
+
headerName: 'Date Sort',
|
|
653
|
+
width: '150px',
|
|
654
|
+
renderCell: ({
|
|
655
|
+
value
|
|
656
|
+
}) => value.toLocaleDateString(),
|
|
657
|
+
headerClassName: 'blue'
|
|
658
|
+
}, {
|
|
659
|
+
field: 'object',
|
|
660
|
+
headerName: 'Object Sort',
|
|
661
|
+
width: '150px',
|
|
662
|
+
renderCell: ({
|
|
663
|
+
value
|
|
664
|
+
}) => value.age,
|
|
665
|
+
type: 'number',
|
|
666
|
+
sortComparator: ({
|
|
667
|
+
rowA,
|
|
668
|
+
rowB
|
|
669
|
+
}) => rowA.object.age - rowB.object.age,
|
|
670
|
+
headerClassName: 'blue'
|
|
671
|
+
}]}
|
|
672
|
+
rows={rows4}
|
|
673
|
+
checkboxSelection
|
|
674
|
+
hoverRow
|
|
675
|
+
noWrap
|
|
676
|
+
stripe="even"
|
|
677
|
+
stickyHeader
|
|
678
|
+
columnGroupingModel={[{
|
|
679
|
+
groupId: 'group1',
|
|
680
|
+
headerName: 'Group 1',
|
|
681
|
+
headerClassName: 'red',
|
|
682
|
+
children: [{
|
|
683
|
+
field: 'id'
|
|
684
|
+
}, {
|
|
685
|
+
field: 'number'
|
|
686
|
+
}, {
|
|
687
|
+
field: 'string'
|
|
688
|
+
}]
|
|
689
|
+
}, {
|
|
690
|
+
groupId: 'group2',
|
|
691
|
+
headerName: '',
|
|
692
|
+
children: [{
|
|
693
|
+
field: 'date'
|
|
694
|
+
}, {
|
|
695
|
+
field: 'object'
|
|
696
|
+
}]
|
|
697
|
+
}]}
|
|
698
|
+
/>
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### Column Groups
|
|
702
|
+
|
|
41
703
|
```tsx
|
|
42
|
-
|
|
704
|
+
<DataTable
|
|
705
|
+
onPaginationModelChange={fn()}
|
|
706
|
+
onSelectionModelChange={fn()}
|
|
707
|
+
onRowClick={fn()}
|
|
708
|
+
columns={[{
|
|
709
|
+
field: 'id',
|
|
710
|
+
headerName: 'ID',
|
|
711
|
+
width: '150px'
|
|
712
|
+
}, {
|
|
713
|
+
field: 'number',
|
|
714
|
+
headerName: 'Number Sort',
|
|
715
|
+
type: 'number',
|
|
716
|
+
width: '900px',
|
|
717
|
+
cellClassName: () => 'warn'
|
|
718
|
+
}, {
|
|
719
|
+
field: 'string',
|
|
720
|
+
headerName: 'String Sort',
|
|
721
|
+
width: '150px'
|
|
722
|
+
}, {
|
|
723
|
+
field: 'date',
|
|
724
|
+
headerName: 'Date Sort',
|
|
725
|
+
width: '150px',
|
|
726
|
+
renderCell: ({
|
|
727
|
+
value
|
|
728
|
+
}) => value.toLocaleDateString(),
|
|
729
|
+
cellClassName: () => 'primary'
|
|
730
|
+
}, {
|
|
731
|
+
field: 'object',
|
|
732
|
+
headerName: 'Object Sort',
|
|
733
|
+
width: '150px',
|
|
734
|
+
renderCell: ({
|
|
735
|
+
value
|
|
736
|
+
}) => value.age,
|
|
737
|
+
type: 'number',
|
|
738
|
+
sortComparator: ({
|
|
739
|
+
rowA,
|
|
740
|
+
rowB
|
|
741
|
+
}) => rowA.object.age - rowB.object.age,
|
|
742
|
+
cellClassName: () => 'primary'
|
|
743
|
+
}]}
|
|
744
|
+
rows={[...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4, ...rows4].map((row, i) => ({
|
|
745
|
+
...row,
|
|
746
|
+
id: i,
|
|
747
|
+
date: new Date(row.date),
|
|
748
|
+
object: {
|
|
749
|
+
age: row.object.age + i
|
|
750
|
+
}
|
|
751
|
+
}))}
|
|
752
|
+
checkboxSelection
|
|
753
|
+
hoverRow
|
|
754
|
+
noWrap
|
|
755
|
+
stripe="even"
|
|
756
|
+
stickyHeader
|
|
757
|
+
columnGroupingModel={[{
|
|
758
|
+
groupId: 'group1',
|
|
759
|
+
headerName: 'Group 1',
|
|
760
|
+
children: [{
|
|
761
|
+
field: 'id'
|
|
762
|
+
}, {
|
|
763
|
+
field: 'number'
|
|
764
|
+
}, {
|
|
765
|
+
field: 'string'
|
|
766
|
+
}]
|
|
767
|
+
}, {
|
|
768
|
+
groupId: 'group2',
|
|
769
|
+
headerName: '',
|
|
770
|
+
children: [{
|
|
771
|
+
field: 'date'
|
|
772
|
+
}, {
|
|
773
|
+
field: 'object'
|
|
774
|
+
}]
|
|
775
|
+
}]}
|
|
776
|
+
slotProps={{
|
|
777
|
+
background: {
|
|
778
|
+
style: {
|
|
779
|
+
height: '300px'
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}}
|
|
783
|
+
/>
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
```tsx
|
|
787
|
+
const columnGroupingModel = [
|
|
788
|
+
{
|
|
789
|
+
groupId: 'nutrition',
|
|
790
|
+
headerName: 'Nutrition',
|
|
791
|
+
children: [{ field: 'calories' }, { field: 'fat' }, { field: 'carbs' }],
|
|
792
|
+
},
|
|
793
|
+
];
|
|
794
|
+
|
|
795
|
+
<DataTable rows={rows} columns={columns} columnGroupingModel={columnGroupingModel} />;
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
### Column Resizing
|
|
799
|
+
|
|
800
|
+
Setting `resizable: true` on a column allows you to adjust the column width by dragging.
|
|
801
|
+
|
|
802
|
+
```tsx
|
|
803
|
+
<Stack gap={1}>
|
|
804
|
+
<Typography level="body-sm" textColor="text.secondary">
|
|
805
|
+
Drag the column borders to resize columns
|
|
806
|
+
</Typography>
|
|
807
|
+
<DataTable rows={tableRows} columns={columns} noWrap />
|
|
808
|
+
</Stack>
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
```tsx
|
|
812
|
+
const columns = [
|
|
813
|
+
{
|
|
814
|
+
field: 'name',
|
|
815
|
+
headerName: 'Name',
|
|
816
|
+
width: '200px',
|
|
817
|
+
resizable: true,
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
field: 'description',
|
|
821
|
+
headerName: 'Description',
|
|
822
|
+
width: '300px',
|
|
823
|
+
resizable: true,
|
|
824
|
+
},
|
|
825
|
+
];
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
## Editing Features
|
|
829
|
+
|
|
830
|
+
### Inline Editing
|
|
831
|
+
|
|
832
|
+
```tsx
|
|
833
|
+
<div>
|
|
834
|
+
<DataTable rows={rows3} columns={columns} checkboxSelection={false} noWrap editMode />
|
|
835
|
+
<Button onClick={() => {
|
|
836
|
+
console.log(editedRows);
|
|
837
|
+
setIsCellEditable(true);
|
|
838
|
+
}}>
|
|
839
|
+
Submit
|
|
840
|
+
</Button>
|
|
841
|
+
</div>
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
#### Edit Types
|
|
43
845
|
|
|
846
|
+
- `text`, `number`, `currency`, `select`, `autocomplete`, `date`, `longText`, `link`
|
|
847
|
+
|
|
848
|
+
#### Notes on Editing
|
|
849
|
+
|
|
850
|
+
- `editMode` must be enabled for the editing UI to be active.
|
|
851
|
+
- Use `onCellEditStop` or `onCellEditStart` to reflect changed values.
|
|
852
|
+
- Specifying `renderEditCell` allows you to customize the editing UI.
|
|
853
|
+
|
|
854
|
+
```tsx
|
|
44
855
|
const columns = [
|
|
45
|
-
{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
856
|
+
{
|
|
857
|
+
field: 'status',
|
|
858
|
+
type: 'select',
|
|
859
|
+
componentProps: { options: ['Opened', 'Preparing', 'Closed'] },
|
|
860
|
+
onCellEditStop: ({ row }) => updateRow(row),
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
field: 'note',
|
|
864
|
+
type: 'longText',
|
|
865
|
+
renderEditCell: ({ value }) => value.slice(0, 100),
|
|
866
|
+
},
|
|
50
867
|
];
|
|
51
868
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
869
|
+
<DataTable rows={rows} columns={columns} editMode />;
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
### Conditional Editing
|
|
873
|
+
|
|
874
|
+
```tsx
|
|
875
|
+
<div>
|
|
876
|
+
<Typography level="title-lg" textColor="text.secondary">
|
|
877
|
+
expectedSales 100초과만 수정 가능
|
|
878
|
+
</Typography>
|
|
879
|
+
<DataTable rows={rows} columns={columns} checkboxSelection={false} noWrap editMode />
|
|
880
|
+
<Button onClick={() => {
|
|
881
|
+
console.log(rows);
|
|
882
|
+
}}>
|
|
883
|
+
Submit
|
|
884
|
+
</Button>
|
|
885
|
+
</div>
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
### Real-time Data Updates
|
|
57
889
|
|
|
58
|
-
|
|
890
|
+
```tsx
|
|
891
|
+
<DataTable rows={editRows} columns={columns} checkboxSelection={false} noWrap />
|
|
59
892
|
```
|
|
60
893
|
|
|
61
|
-
|
|
894
|
+
### Required Field Indicator
|
|
62
895
|
|
|
63
|
-
|
|
896
|
+
Setting `required: true` on a column displays a required indicator (\*) in the header.
|
|
64
897
|
|
|
65
|
-
|
|
898
|
+
```tsx
|
|
899
|
+
<Stack gap={1}>
|
|
900
|
+
<Typography level="body-sm" textColor="text.secondary">
|
|
901
|
+
Fields marked with * are required
|
|
902
|
+
</Typography>
|
|
903
|
+
<DataTable rows={tableRows} columns={columns} editMode noWrap />
|
|
904
|
+
</Stack>
|
|
905
|
+
```
|
|
66
906
|
|
|
67
907
|
```tsx
|
|
68
|
-
|
|
908
|
+
const columns = [
|
|
909
|
+
{
|
|
910
|
+
field: 'name',
|
|
911
|
+
headerName: 'Name',
|
|
912
|
+
required: true,
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
field: 'email',
|
|
916
|
+
headerName: 'Email',
|
|
917
|
+
required: true,
|
|
918
|
+
},
|
|
919
|
+
];
|
|
69
920
|
```
|
|
70
921
|
|
|
71
|
-
|
|
922
|
+
### Customizing renderEditCell
|
|
923
|
+
|
|
924
|
+
Using `renderEditCell` allows you to fully customize the editing UI.
|
|
925
|
+
|
|
926
|
+
```tsx
|
|
927
|
+
<DataTable rows={tableRows} columns={columns} editMode noWrap />
|
|
928
|
+
```
|
|
72
929
|
|
|
73
930
|
```tsx
|
|
74
|
-
|
|
931
|
+
{
|
|
932
|
+
field: 'rating',
|
|
933
|
+
headerName: 'Rating',
|
|
934
|
+
renderEditCell: ({ value, row }) => (
|
|
935
|
+
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
|
936
|
+
{[1, 2, 3, 4, 5].map((star) => (
|
|
937
|
+
<IconButton
|
|
938
|
+
key={star}
|
|
939
|
+
size="sm"
|
|
940
|
+
onClick={() => updateRating(row.id, star)}
|
|
941
|
+
>
|
|
942
|
+
{star <= value ? '★' : '☆'}
|
|
943
|
+
</IconButton>
|
|
944
|
+
))}
|
|
945
|
+
</Box>
|
|
946
|
+
),
|
|
947
|
+
}
|
|
75
948
|
```
|
|
76
949
|
|
|
77
|
-
|
|
950
|
+
### Handling Edit Events
|
|
951
|
+
|
|
952
|
+
You can use `onCellEditStart` and `onCellEditStop` to detect when editing starts and ends.
|
|
78
953
|
|
|
79
954
|
```tsx
|
|
80
|
-
<
|
|
955
|
+
<Stack gap={2}>
|
|
956
|
+
<DataTable rows={tableRows} columns={columns} editMode noWrap />
|
|
957
|
+
<Box sx={{
|
|
958
|
+
p: 2,
|
|
959
|
+
bgcolor: 'neutral.100',
|
|
960
|
+
borderRadius: '8px',
|
|
961
|
+
fontFamily: 'monospace',
|
|
962
|
+
fontSize: '12px'
|
|
963
|
+
}}>
|
|
964
|
+
<Typography level="title-sm" sx={{
|
|
965
|
+
mb: 1
|
|
966
|
+
}}>
|
|
967
|
+
Edit Log:
|
|
968
|
+
</Typography>
|
|
969
|
+
{editLog.length === 0 ? <Typography level="body-xs" textColor="text.secondary">
|
|
970
|
+
No edits yet. Click on a cell to edit.
|
|
971
|
+
</Typography> : editLog.map((log, i) => <Typography key={i} level="body-xs">
|
|
972
|
+
{log}
|
|
973
|
+
</Typography>)}
|
|
974
|
+
</Box>
|
|
975
|
+
</Stack>
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
```tsx
|
|
979
|
+
{
|
|
980
|
+
field: 'value',
|
|
981
|
+
headerName: 'Value',
|
|
982
|
+
onCellEditStart: ({ row, originalRow, value }) => {
|
|
983
|
+
console.log('Edit started:', { row, originalRow, value });
|
|
984
|
+
},
|
|
985
|
+
onCellEditStop: ({ row, originalRow, value }) => {
|
|
986
|
+
console.log('Edit stopped:', { row, originalRow, value });
|
|
987
|
+
// Save changes to server
|
|
988
|
+
saveChanges(row);
|
|
989
|
+
},
|
|
990
|
+
}
|
|
81
991
|
```
|
|
82
992
|
|
|
83
|
-
|
|
993
|
+
## Event Handlers
|
|
994
|
+
|
|
995
|
+
### onRowClick
|
|
996
|
+
|
|
997
|
+
Handles row click events. Useful for implementing Inspector patterns or detail view features.
|
|
998
|
+
|
|
999
|
+
```tsx
|
|
1000
|
+
<Stack gap={2}>
|
|
1001
|
+
<DataTable rows={tableRows} columns={columns} hoverRow onRowClick={({
|
|
1002
|
+
row
|
|
1003
|
+
}) => setSelectedRow(row)} noWrap />
|
|
1004
|
+
{selectedRow && <Box sx={{
|
|
1005
|
+
p: 2,
|
|
1006
|
+
border: '1px solid',
|
|
1007
|
+
borderColor: 'neutral.300',
|
|
1008
|
+
borderRadius: '8px'
|
|
1009
|
+
}}>
|
|
1010
|
+
<Typography level="title-md">Selected Employee Details</Typography>
|
|
1011
|
+
<Typography level="body-sm">Name: {selectedRow.name}</Typography>
|
|
1012
|
+
<Typography level="body-sm">Email: {selectedRow.email}</Typography>
|
|
1013
|
+
<Typography level="body-sm">Department: {selectedRow.department}</Typography>
|
|
1014
|
+
</Box>}
|
|
1015
|
+
</Stack>
|
|
1016
|
+
```
|
|
84
1017
|
|
|
85
1018
|
```tsx
|
|
1019
|
+
const [selectedRow, setSelectedRow] = useState(null);
|
|
1020
|
+
|
|
86
1021
|
<DataTable
|
|
87
|
-
columns={columns}
|
|
88
1022
|
rows={rows}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
1023
|
+
columns={columns}
|
|
1024
|
+
hoverRow
|
|
1025
|
+
onRowClick={({ row, rowId }, event) => {
|
|
1026
|
+
console.log('Clicked row:', row, 'ID:', rowId);
|
|
1027
|
+
setSelectedRow(row);
|
|
1028
|
+
}}
|
|
1029
|
+
/>;
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
## Sorting Features
|
|
1033
|
+
|
|
1034
|
+
### Basic Sorting
|
|
1035
|
+
|
|
1036
|
+
```tsx
|
|
1037
|
+
<DataTable rows={rows4} columns={columns} />
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
### Custom Sort Order
|
|
1041
|
+
|
|
1042
|
+
```tsx
|
|
1043
|
+
<DataTable rows={rows4} columns={columns} sortOrder={['desc', 'asc']} />
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
### Column-specific Sort Order
|
|
1047
|
+
|
|
1048
|
+
```tsx
|
|
1049
|
+
<DataTable rows={rows4} columns={columns} />
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
### Disabling Sorting
|
|
1053
|
+
|
|
1054
|
+
```tsx
|
|
1055
|
+
<DataTable rows={rows4} columns={columns} />
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
### Multiple Sorting
|
|
1059
|
+
|
|
1060
|
+
```tsx
|
|
1061
|
+
<DataTable rows={rows4} columns={columns} />
|
|
93
1062
|
```
|
|
94
1063
|
|
|
95
|
-
###
|
|
1064
|
+
### Custom Sort Logic
|
|
96
1065
|
|
|
97
1066
|
```tsx
|
|
98
|
-
<DataTable {
|
|
99
|
-
setSelectionModel(v);
|
|
100
|
-
args.onSelectionModelChange?.(v, ...params);
|
|
101
|
-
}, [args.onSelectionModelChange])} />
|
|
1067
|
+
<DataTable rows={rows4} columns={columns} />
|
|
102
1068
|
```
|
|
103
1069
|
|
|
104
|
-
|
|
1070
|
+
### Controlling Sort State
|
|
105
1071
|
|
|
106
|
-
|
|
107
|
-
- 스트라이프 패턴 (stripe="even")
|
|
108
|
-
- 텍스트 줄바꿈 방지 (noWrap)
|
|
109
|
-
- 커스텀 툴바
|
|
1072
|
+
#### Uncontrolled Initial Sort
|
|
110
1073
|
|
|
111
1074
|
```tsx
|
|
112
1075
|
<DataTable
|
|
113
|
-
|
|
1076
|
+
onPaginationModelChange={fn()}
|
|
1077
|
+
onSelectionModelChange={fn()}
|
|
1078
|
+
onRowClick={fn()}
|
|
1079
|
+
columns={[{
|
|
1080
|
+
field: 'dessert',
|
|
1081
|
+
headerName: 'Dessert (100g serving)',
|
|
1082
|
+
width: '40%'
|
|
1083
|
+
}, {
|
|
1084
|
+
field: 'calories',
|
|
1085
|
+
headerName: 'Calories',
|
|
1086
|
+
type: 'number'
|
|
1087
|
+
}, {
|
|
1088
|
+
field: 'fat',
|
|
1089
|
+
headerName: 'Fat (g)',
|
|
1090
|
+
type: 'number'
|
|
1091
|
+
}, {
|
|
1092
|
+
field: 'carbs',
|
|
1093
|
+
headerName: 'Carbs (g)',
|
|
1094
|
+
type: 'number'
|
|
1095
|
+
}, {
|
|
1096
|
+
field: 'protein',
|
|
1097
|
+
headerName: 'Protein (g)',
|
|
1098
|
+
type: 'number'
|
|
1099
|
+
}]}
|
|
114
1100
|
rows={rows}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
<Button variant="plain" color="neutral" size="sm">
|
|
124
|
-
Action
|
|
125
|
-
</Button>
|
|
126
|
-
<Button variant="plain" color="danger" size="sm">
|
|
127
|
-
Delete
|
|
128
|
-
</Button>
|
|
129
|
-
</Stack>
|
|
130
|
-
),
|
|
131
|
-
}}
|
|
132
|
-
slotProps={{
|
|
133
|
-
background: { style: { height: '300px' } },
|
|
134
|
-
}}
|
|
1101
|
+
initialState={{
|
|
1102
|
+
sorting: {
|
|
1103
|
+
sortModel: [{
|
|
1104
|
+
field: 'calories',
|
|
1105
|
+
sort: 'asc'
|
|
1106
|
+
}]
|
|
1107
|
+
}
|
|
1108
|
+
}}
|
|
135
1109
|
/>
|
|
136
1110
|
```
|
|
137
1111
|
|
|
138
|
-
|
|
1112
|
+
#### Controlled Sort
|
|
139
1113
|
|
|
140
1114
|
```tsx
|
|
141
|
-
<DataTable {
|
|
1115
|
+
<DataTable rows={rows4} columns={columns} sortModel={sortModel} onSortModelChange={handleSortModelChange} />
|
|
142
1116
|
```
|
|
143
1117
|
|
|
144
|
-
데이터 로딩 중일 때의 상태를 표시합니다.
|
|
145
|
-
|
|
146
1118
|
```tsx
|
|
147
|
-
|
|
1119
|
+
const [sortModel, setSortModel] = useState([{ field: 'calories', sort: 'asc' }]);
|
|
1120
|
+
|
|
1121
|
+
<DataTable rows={rows} columns={columns} sortModel={sortModel} onSortModelChange={setSortModel} />;
|
|
148
1122
|
```
|
|
149
1123
|
|
|
150
|
-
|
|
1124
|
+
## Pagination
|
|
1125
|
+
|
|
1126
|
+
### Basic Pagination
|
|
151
1127
|
|
|
152
1128
|
```tsx
|
|
153
1129
|
<DataTable
|
|
@@ -191,19 +1167,7 @@ const rows = [
|
|
|
191
1167
|
/>
|
|
192
1168
|
```
|
|
193
1169
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
```tsx
|
|
197
|
-
<DataTable
|
|
198
|
-
columns={columns}
|
|
199
|
-
rows={rows}
|
|
200
|
-
pagination={true}
|
|
201
|
-
paginationModel={{ page: 0, pageSize: 20 }}
|
|
202
|
-
onPaginationModelChange={setPaginationModel}
|
|
203
|
-
/>
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
### 빈 데이터 상태
|
|
1170
|
+
### Empty State
|
|
207
1171
|
|
|
208
1172
|
```tsx
|
|
209
1173
|
<DataTable
|
|
@@ -244,209 +1208,149 @@ const rows = [
|
|
|
244
1208
|
/>
|
|
245
1209
|
```
|
|
246
1210
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
## 고급 기능
|
|
250
|
-
|
|
251
|
-
### 커스텀 셀 렌더링
|
|
1211
|
+
### Sorting + Pagination
|
|
252
1212
|
|
|
253
1213
|
```tsx
|
|
254
|
-
<DataTable rows={
|
|
255
|
-
...row,
|
|
256
|
-
id: i + 1
|
|
257
|
-
})), [])} columns={columns} checkboxSelection stickyHeader stripe="even" noWrap slotProps={{
|
|
1214
|
+
<DataTable rows={rows} columns={columns} pagination slotProps={{
|
|
258
1215
|
background: {
|
|
259
1216
|
style: {
|
|
260
|
-
height: '
|
|
1217
|
+
height: '300px'
|
|
261
1218
|
}
|
|
262
1219
|
}
|
|
263
|
-
}} />
|
|
1220
|
+
}} getId={(row: RowForSort) => row.id} />
|
|
264
1221
|
```
|
|
265
1222
|
|
|
266
|
-
|
|
1223
|
+
### Server Pagination
|
|
267
1224
|
|
|
268
1225
|
```tsx
|
|
269
|
-
|
|
270
|
-
{ field: 'id', headerName: 'ID' },
|
|
271
|
-
{
|
|
272
|
-
field: 'dessert',
|
|
273
|
-
renderCell: ({ value, row }) => <Link href={row.storeLink}>{value}</Link>,
|
|
274
|
-
},
|
|
275
|
-
{
|
|
276
|
-
field: 'location',
|
|
277
|
-
renderCell: ({ value }) => value.join(', '),
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
field: 'expectedSales',
|
|
281
|
-
renderCell: ({ value }) => (value ? `$${value.toFixed(2)}` : ''),
|
|
282
|
-
},
|
|
283
|
-
];
|
|
1226
|
+
<DataTable rows={pagedRows} columns={columns as any} pagination paginationMode="server" rowCount={allRows.length} paginationModel={paginationModel} onPaginationModelChange={setPaginationModel} loading={loading} />
|
|
284
1227
|
```
|
|
285
1228
|
|
|
286
|
-
|
|
1229
|
+
You can specify the total row count using `paginationMode="server"` and `rowCount`.
|
|
287
1230
|
|
|
288
1231
|
```tsx
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
1232
|
+
<DataTable
|
|
1233
|
+
rows={rows}
|
|
1234
|
+
columns={columns}
|
|
1235
|
+
pagination
|
|
1236
|
+
paginationMode="server"
|
|
1237
|
+
rowCount={totalRowCount}
|
|
1238
|
+
paginationModel={{ page: 0, pageSize: 20 }}
|
|
1239
|
+
onPaginationModelChange={setPaginationModel}
|
|
1240
|
+
/>
|
|
298
1241
|
```
|
|
299
1242
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
- **text**: 기본 텍스트 입력
|
|
303
|
-
- **number/currency**: 숫자/통화 입력
|
|
304
|
-
- **select**: 드롭다운 선택
|
|
305
|
-
- **autocomplete**: 자동완성 입력
|
|
306
|
-
- **date**: 날짜 선택
|
|
307
|
-
- **longText**: 긴 텍스트 입력
|
|
308
|
-
- **link**: 링크 렌더링
|
|
1243
|
+
### Dynamic rowCount
|
|
309
1244
|
|
|
310
1245
|
```tsx
|
|
311
|
-
|
|
312
|
-
{
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
required: true,
|
|
319
|
-
onCellEditStop: handleCellEdit,
|
|
320
|
-
},
|
|
321
|
-
{
|
|
322
|
-
field: 'status',
|
|
323
|
-
type: 'select',
|
|
324
|
-
componentProps: {
|
|
325
|
-
options: ['Opened', 'Preparing', 'Closed'],
|
|
326
|
-
},
|
|
327
|
-
onCellEditStop: handleCellEdit,
|
|
328
|
-
},
|
|
329
|
-
{
|
|
330
|
-
field: 'expectedSales',
|
|
331
|
-
type: 'currency',
|
|
332
|
-
onCellEditStop: handleCellEdit,
|
|
333
|
-
},
|
|
334
|
-
];
|
|
335
|
-
|
|
336
|
-
<DataTable rows={rows} columns={columns} editMode={true} />;
|
|
1246
|
+
<Stack gap={4} height="500px">
|
|
1247
|
+
<Stack direction="row" gap={2}>
|
|
1248
|
+
<Button onClick={handleIncrease}>Increase</Button>
|
|
1249
|
+
<Button onClick={handleDecrease}>Decrease</Button>
|
|
1250
|
+
</Stack>
|
|
1251
|
+
<DataTable rows={rows} columns={columns} pagination getId={(row: RowForSort) => row.id} />
|
|
1252
|
+
</Stack>
|
|
337
1253
|
```
|
|
338
1254
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
```tsx
|
|
342
|
-
<div>
|
|
343
|
-
<Typography level="title-lg" textColor="text.secondary">
|
|
344
|
-
expectedSales 100초과만 수정 가능
|
|
345
|
-
</Typography>
|
|
346
|
-
<DataTable rows={rows} columns={columns} checkboxSelection={false} noWrap editMode />
|
|
347
|
-
<Button onClick={() => {
|
|
348
|
-
console.log(rows);
|
|
349
|
-
}}>
|
|
350
|
-
Submit
|
|
351
|
-
</Button>
|
|
352
|
-
</div>
|
|
353
|
-
```
|
|
1255
|
+
## Advanced Layout
|
|
354
1256
|
|
|
355
|
-
|
|
1257
|
+
### Pinned Columns
|
|
356
1258
|
|
|
357
1259
|
```tsx
|
|
358
|
-
|
|
359
|
-
{
|
|
360
|
-
field: 'dessert',
|
|
361
|
-
type: 'autocomplete',
|
|
362
|
-
isCellEditable: ({ row }) => row.expectedSales > 100,
|
|
363
|
-
onCellEditStop: handleRowChange,
|
|
364
|
-
},
|
|
365
|
-
{
|
|
366
|
-
field: 'expectedSales',
|
|
367
|
-
type: 'currency',
|
|
368
|
-
isCellEditable: true, // 항상 편집 가능
|
|
369
|
-
onCellEditStop: handleRowChange,
|
|
370
|
-
},
|
|
371
|
-
{
|
|
372
|
-
field: 'note',
|
|
373
|
-
type: 'longText',
|
|
374
|
-
isCellEditable: ({ row }) => row.expectedSales > 100,
|
|
375
|
-
},
|
|
376
|
-
];
|
|
1260
|
+
<DataTable rows={args.rows} columns={args.columns} pinnedColumns={args.pinnedColumns} stickyHeader={args.stickyHeader} stripe={args.stripe} noWrap={args.noWrap} hoverRow={args.hoverRow} selectionModel={selectionModel} onSelectionModelChange={setSelectionModel} />
|
|
377
1261
|
```
|
|
378
1262
|
|
|
379
|
-
## 정렬 기능
|
|
380
|
-
|
|
381
|
-
### 기본 정렬
|
|
382
|
-
|
|
383
1263
|
```tsx
|
|
384
|
-
<DataTable rows={
|
|
1264
|
+
<DataTable rows={rows} columns={columns} pinnedColumns={{ left: ['id', 'name'], right: ['actions'] }} />
|
|
385
1265
|
```
|
|
386
1266
|
|
|
387
|
-
|
|
1267
|
+
## API Usage
|
|
388
1268
|
|
|
389
|
-
|
|
390
|
-
const columns = [
|
|
391
|
-
{ field: 'id', headerName: 'ID' },
|
|
392
|
-
{ field: 'number', headerName: 'Number Sort', type: 'number' },
|
|
393
|
-
{ field: 'string', headerName: 'String Sort' },
|
|
394
|
-
{ field: 'date', headerName: 'Date Sort', renderCell: ({ value }) => value.toLocaleDateString() },
|
|
395
|
-
{
|
|
396
|
-
field: 'object',
|
|
397
|
-
headerName: 'Object Sort',
|
|
398
|
-
renderCell: ({ value }) => value.age,
|
|
399
|
-
type: 'number',
|
|
400
|
-
sortComparator: ({ rowA, rowB }) => rowA.object.age - rowB.object.age,
|
|
401
|
-
},
|
|
402
|
-
];
|
|
403
|
-
```
|
|
1269
|
+
### DataTableApi Interface
|
|
404
1270
|
|
|
405
|
-
|
|
1271
|
+
DataTable supports programmatic control through `apiRef`.
|
|
406
1272
|
|
|
407
1273
|
```tsx
|
|
408
|
-
|
|
1274
|
+
interface DataTableApi {
|
|
1275
|
+
getRowIndexRelativeToVisibleRows(rowId: string): number;
|
|
1276
|
+
setCellFocus(rowId: string): void;
|
|
1277
|
+
}
|
|
409
1278
|
```
|
|
410
1279
|
|
|
411
|
-
|
|
1280
|
+
### Scrolling/Focusing to a Specific Row
|
|
412
1281
|
|
|
413
1282
|
```tsx
|
|
414
|
-
<
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
1283
|
+
<Box sx={{
|
|
1284
|
+
height: '400px'
|
|
1285
|
+
}}>
|
|
1286
|
+
<DataTable ref={ref} rows={args.rows} columns={args.columns} checkboxSelection={args.checkboxSelection} disableSelectionOnClick={args.disableSelectionOnClick} selectionModel={args.selectionModel} isTotalSelected={args.isTotalSelected} pagination={args.pagination} paginationMode={args.paginationMode} paginationModel={args.paginationModel} noWrap={args.noWrap} hoverRow={args.hoverRow} stripe={args.stripe} stickyHeader={args.stickyHeader} initialState={args.initialState} />
|
|
1287
|
+
</Box>
|
|
419
1288
|
```
|
|
420
1289
|
|
|
421
|
-
###
|
|
1290
|
+
### Inspector Pattern
|
|
422
1291
|
|
|
423
1292
|
```tsx
|
|
424
|
-
<
|
|
1293
|
+
<Stack sx={{
|
|
1294
|
+
height: '100vh'
|
|
1295
|
+
}}>
|
|
1296
|
+
<Typography level="title-sm" textColor="text.secondary">
|
|
1297
|
+
Virtualized & Inspector DataTable
|
|
1298
|
+
</Typography>
|
|
1299
|
+
<Button onClick={() => apiRef.current?.setCellFocus('0')}>Row ID 0 Focus</Button>
|
|
1300
|
+
<Button onClick={() => apiRef.current?.setCellFocus('100')}>Row ID 100 Focus</Button>
|
|
1301
|
+
<div style={{
|
|
1302
|
+
flex: 1,
|
|
1303
|
+
maxHeight: '100%',
|
|
1304
|
+
overflow: 'auto'
|
|
1305
|
+
}}>
|
|
1306
|
+
<DataTable ref={apiRef} rows={args.rows} columns={args.columns} stripe={args.stripe} hoverRow={args.hoverRow} stickyHeader={args.stickyHeader} disableSelectionOnClick={args.disableSelectionOnClick} checkboxSelection={args.checkboxSelection} noWrap={args.noWrap} selectionModel={selectedRowId === -1 ? [] : [selectedRowId]} onRowClick={({
|
|
1307
|
+
rowId
|
|
1308
|
+
}) => {
|
|
1309
|
+
if (selectedRowId === rowId) {
|
|
1310
|
+
setSelectedRowId(-1);
|
|
1311
|
+
} else {
|
|
1312
|
+
console.log(rowId);
|
|
1313
|
+
setSelectedRowId(rowId);
|
|
1314
|
+
setTimeout(() => {
|
|
1315
|
+
apiRef.current?.setCellFocus(String(rowId));
|
|
1316
|
+
}, 0);
|
|
1317
|
+
}
|
|
1318
|
+
}} />
|
|
1319
|
+
</div>
|
|
1320
|
+
{selectedRowId !== -1 && <Box sx={{
|
|
1321
|
+
backgroundColor: 'aqua',
|
|
1322
|
+
border: 'black 1px solid',
|
|
1323
|
+
height: '300px'
|
|
1324
|
+
}}>
|
|
1325
|
+
This is inspector: {selectedRowId}
|
|
1326
|
+
</Box>}
|
|
1327
|
+
</Stack>
|
|
425
1328
|
```
|
|
426
1329
|
|
|
427
|
-
|
|
1330
|
+
## Constraints and Considerations
|
|
428
1331
|
|
|
429
|
-
|
|
430
|
-
const columns = [
|
|
431
|
-
{ field: 'id', headerName: 'ID' },
|
|
432
|
-
{ field: 'number', headerName: 'Number Sort', type: 'number', sortable: false },
|
|
433
|
-
];
|
|
434
|
-
```
|
|
1332
|
+
### Required Conditions
|
|
435
1333
|
|
|
436
|
-
|
|
1334
|
+
- Using `useMemo` to maintain references when `rows` changes can reduce unnecessary re-renders.
|
|
1335
|
+
- When using custom `getId`, values must be unique; duplicates can cause selection/sorting/editing to malfunction.
|
|
1336
|
+
- Without height on `slotProps.background`, `stickyHeader` may not display properly.
|
|
437
1337
|
|
|
438
|
-
|
|
439
|
-
<DataTable rows={rows4} columns={columns} />
|
|
440
|
-
```
|
|
1338
|
+
### Feature Combination Constraints
|
|
441
1339
|
|
|
442
|
-
|
|
1340
|
+
- `pinnedColumns` and `columnGroupingModel` cannot be used simultaneously.
|
|
1341
|
+
- `editMode` must be `true` for editing-related UI to be active.
|
|
1342
|
+
- When `paginationMode="server"`, `rowCount` must be specified.
|
|
443
1343
|
|
|
444
|
-
|
|
1344
|
+
### Performance Optimization
|
|
445
1345
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
1346
|
+
- Use virtual scrolling for large data (1000+ rows).
|
|
1347
|
+
- Memoize the `columns` array with `useMemo`.
|
|
1348
|
+
- Be careful with `renderCell` and `componentProps` functions as they can cause unnecessary re-renders.
|
|
449
1349
|
|
|
450
|
-
|
|
1350
|
+
```tsx
|
|
1351
|
+
// Good
|
|
1352
|
+
const columns = useMemo(() => [...], [dependencies]);
|
|
451
1353
|
|
|
452
|
-
|
|
1354
|
+
// Bad - creates a new array on every render
|
|
1355
|
+
const columns = [...];
|
|
1356
|
+
```
|