Skip to content

Commit 5020f1f

Browse files
committed
Fix DataGrid custom columns
1 parent e40f0a5 commit 5020f1f

File tree

4 files changed

+145
-34
lines changed

4 files changed

+145
-34
lines changed

src/components/DataGrid.tsx

Lines changed: 65 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export default function DataGrid({
3232
rowStyle,
3333
onHeaderSelected,
3434
onRowSelected,
35+
slots: slotsProp,
3536
children,
3637
}: DataGridProps & { children?: React.ReactNode }) {
3738

@@ -43,16 +44,36 @@ export default function DataGrid({
4344
const metaType = useMemo(() => typeOf(typeName), [typeName, typeOf])
4445
const typeProps = useMemo(() => typeProperties(metaType), [metaType, typeProperties])
4546

46-
// Extract slots from children
47+
// Extract slots from children or slots prop
4748
const slots = useMemo(() => {
48-
const slotMap: { [key: string]: React.ReactNode } = {}
49-
React.Children.forEach(children, child => {
50-
if (React.isValidElement(child) && (child.props as any).slot) {
51-
slotMap[(child.props as any).slot] = child
49+
// If slots prop is provided, use it directly
50+
if (slotsProp) {
51+
return slotsProp as { [key: string]: React.ReactNode }
52+
}
53+
54+
// If children is an object (slot map), use it directly
55+
if (children && typeof children === 'object' && !React.isValidElement(children) && !Array.isArray(children)) {
56+
// Check if it's a plain object with string keys (not a Promise, not a React element)
57+
const obj = children as any
58+
// Plain objects have Object.prototype or null prototype
59+
const proto = Object.getPrototypeOf(obj)
60+
if (obj && typeof obj === 'object' && (proto === Object.prototype || proto === null)) {
61+
return obj as { [key: string]: React.ReactNode }
5262
}
53-
})
63+
}
64+
65+
// Otherwise, extract slots from React children with slot prop
66+
const slotMap: { [key: string]: React.ReactNode } = {}
67+
// Only use React.Children if we have valid React children
68+
if (children && (React.isValidElement(children) || Array.isArray(children))) {
69+
React.Children.forEach(children, child => {
70+
if (React.isValidElement(child) && (child.props as any).slot) {
71+
slotMap[(child.props as any).slot] = child
72+
}
73+
})
74+
}
5475
return slotMap
55-
}, [children])
76+
}, [slotsProp, children])
5677

5778
const slotHeader = (column: string) => {
5879
const slotName = column.toLowerCase() + '-header'
@@ -162,25 +183,34 @@ export default function DataGrid({
162183
const isOpen = false // You may want to pass this from parent or manage it
163184

164185
return (
165-
<td
186+
<th
166187
key={column}
167188
className={`${cellClass(column)} ${theadCellClass} ${isOpen ? 'text-gray-900 dark:text-gray-50' : 'text-gray-500 dark:text-gray-400'}`}
189+
onClick={(e) => handleHeaderSelected(column, e)}
168190
>
169-
<div onClick={(e) => handleHeaderSelected(column, e)}>
170-
{slots[column + '-header'] ?
171-
slots[column + '-header'] :
172-
headerSlotName && slots[headerSlotName] ?
173-
slots[headerSlotName] :
174-
slots.header ?
175-
React.cloneElement(slots.header as React.ReactElement, { column, label: headerFormat(column) } as any) :
176-
<div className="flex justify-between items-center">
177-
<span className="mr-1 select-none">
178-
{headerFormat(column)}
179-
</span>
180-
</div>
191+
{(() => {
192+
const headerSlot = slots[column + '-header'] || (headerSlotName && slots[headerSlotName])
193+
if (headerSlot) {
194+
if (typeof headerSlot === 'function') {
195+
return (headerSlot as any)({ column, label: headerFormat(column) })
196+
}
197+
return headerSlot
181198
}
182-
</div>
183-
</td>
199+
if (slots.header) {
200+
if (typeof slots.header === 'function') {
201+
return (slots.header as any)({ column, label: headerFormat(column) })
202+
}
203+
return React.cloneElement(slots.header as React.ReactElement, { column, label: headerFormat(column) } as any)
204+
}
205+
return (
206+
<div className="flex justify-between items-center">
207+
<span className="mr-1 select-none">
208+
{headerFormat(column)}
209+
</span>
210+
</div>
211+
)
212+
})()}
213+
</th>
184214
)
185215
})}
186216
</tr>
@@ -201,14 +231,19 @@ export default function DataGrid({
201231
key={column}
202232
className={`${cellClass(column)} ${grid.tableCellClass}`}
203233
>
204-
{slots[column] ?
205-
React.cloneElement(slots[column] as React.ReactElement, item) :
206-
colSlotName && slots[colSlotName] ?
207-
React.cloneElement(slots[colSlotName] as React.ReactElement, item) :
208-
columnProp(column) ?
209-
<CellFormat type={metaType} propType={columnProp(column)!} value={item} /> :
210-
<PreviewFormat value={mapGet(item, column)} format={columnFormat(column)} />
211-
}
234+
{(() => {
235+
const colSlot = slots[column] || (colSlotName && slots[colSlotName])
236+
if (colSlot) {
237+
if (typeof colSlot === 'function') {
238+
return (colSlot as any)(item)
239+
}
240+
return React.cloneElement(colSlot as React.ReactElement, item)
241+
}
242+
if (columnProp(column)) {
243+
return <CellFormat type={metaType} propType={columnProp(column)!} value={item} />
244+
}
245+
return <PreviewFormat value={mapGet(item, column)} format={columnFormat(column)} />
246+
})()}
212247
</td>
213248
)
214249
})}

src/components/TagInput.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const TagInput: React.FC<TagInputProps & Omit<React.InputHTMLAttributes<HTMLInpu
8282
updateValue(modelArray.filter(x => x !== tag))
8383
}, [modelArray, updateValue])
8484

85-
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
85+
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
8686
if (document.activeElement === e.currentTarget) {
8787
txtInputRef.current?.focus()
8888
}
@@ -249,15 +249,18 @@ const TagInput: React.FC<TagInputProps & Omit<React.InputHTMLAttributes<HTMLInpu
249249
)}
250250
<div className="mt-1 relative">
251251
<input type="hidden" id={id} name={id} value={modelArray.join(',')} />
252-
<button className={cls} onClick={handleClick} onFocus={() => setExpanded(true)} tabIndex={-1} type="button">
252+
<div className={cls} onClick={handleClick} onFocus={onFocus} tabIndex={-1}>
253253
<div className="flex flex-wrap pb-1.5">
254254
{modelArray.map((tag, idx) => (
255255
<div key={idx} className="pt-1.5 pl-1">
256256
<span className="inline-flex rounded-full items-center py-0.5 pl-2.5 pr-1 text-sm font-medium bg-indigo-100 dark:bg-indigo-800 text-indigo-700 dark:text-indigo-300">
257257
{tag}
258258
<button
259259
type="button"
260-
onClick={() => removeTag(tag)}
260+
onClick={(e) => {
261+
e.stopPropagation()
262+
removeTag(tag)
263+
}}
261264
className="flex-shrink-0 ml-1 h-4 w-4 rounded-full inline-flex items-center justify-center text-indigo-400 dark:text-indigo-500 hover:bg-indigo-200 dark:hover:bg-indigo-800 hover:text-indigo-500 dark:hover:text-indigo-400 focus:outline-none focus:bg-indigo-500 focus:text-white dark:focus:text-black"
262265
>
263266
<svg className="h-2 w-2" stroke="currentColor" fill="none" viewBox="0 0 8 8">
@@ -294,7 +297,7 @@ const TagInput: React.FC<TagInputProps & Omit<React.InputHTMLAttributes<HTMLInpu
294297
/>
295298
</div>
296299
</div>
297-
</button>
300+
</div>
298301
{expanded && filteredValues.length > 0 && (
299302
<ul
300303
className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-black py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm"

src/components/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ export interface DataGridProps {
286286
rowStyle?:(model:any,i:number) => CSSProperties | undefined
287287
onHeaderSelected?: (name:string, ev:React.MouseEvent) => void
288288
onRowSelected?: (item:any, ev:React.MouseEvent) => void
289+
slots?: {[name:string]: React.ReactNode | ((props:any) => React.ReactNode)}
289290
}
290291

291292
export interface AutoQueryGridProps {

src/demo/App.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ import SlideOver from '../components/SlideOver'
1414
import DataGrid from '../components/DataGrid'
1515
import { tracks, RoomType } from './data'
1616

17+
class Forecast {
18+
public date?: string;
19+
public temperatureC?: number;
20+
public summary?: string;
21+
public temperatureF?: number;
22+
23+
public constructor(init?: Partial<Forecast>) { (Object as any).assign(this, init); }
24+
}
25+
1726
export default function App() {
1827
const [show, setShow] = useState(false)
1928
const [slideOver, setSlideOver] = useState(false)
@@ -54,6 +63,39 @@ export default function App() {
5463
{ id: 'isoDateOnly', type: 'date' }
5564
]
5665

66+
const forecasts = [
67+
{
68+
"date": "2025-11-08",
69+
"temperatureC": 26,
70+
"summary": "Scorching",
71+
"temperatureF": 78
72+
},
73+
{
74+
"date": "2025-11-09",
75+
"temperatureC": 8,
76+
"summary": "Sweltering",
77+
"temperatureF": 46
78+
},
79+
{
80+
"date": "2025-11-10",
81+
"temperatureC": -14,
82+
"summary": "Warm",
83+
"temperatureF": 7
84+
},
85+
{
86+
"date": "2025-11-11",
87+
"temperatureC": 20,
88+
"summary": "Mild",
89+
"temperatureF": 67
90+
},
91+
{
92+
"date": "2025-11-12",
93+
"temperatureC": 53,
94+
"summary": "Cool",
95+
"temperatureF": 127
96+
}
97+
]
98+
5799
const api = null
58100

59101
return (
@@ -73,6 +115,36 @@ export default function App() {
73115
<DataGrid items={tracks} />
74116
</div>
75117

118+
<div>
119+
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Custom DataGrid</h2>
120+
<DataGrid
121+
items={forecasts}
122+
className="max-w-screen-md"
123+
tableStyle={['stripedRows', 'uppercaseHeadings']}
124+
headerTitles={{
125+
temperatureC: 'TEMP. (C)',
126+
temperatureF: 'TEMP. (F)'
127+
}}
128+
slots={{
129+
'date-header': () => (
130+
<span className="text-green-600">Date</span>
131+
),
132+
'date': ({ date }: Forecast) => (
133+
<>{date ? new Intl.DateTimeFormat().format(new Date(date)) : ''}</>
134+
),
135+
'temperatureC': ({ temperatureC }: Forecast) => (
136+
<>{temperatureC}&deg;</>
137+
),
138+
'temperatureF': ({ temperatureF }: Forecast) => (
139+
<>{temperatureF}&deg;</>
140+
),
141+
'summary': ({ summary }: Forecast) => (
142+
<>{summary}</>
143+
)
144+
}}
145+
/>
146+
</div>
147+
76148
<div className="mt-8 mx-auto max-w-4xl flex flex-col gap-y-4">
77149
<h3>date</h3>
78150
<div className="grid grid-cols-6 gap-6">

0 commit comments

Comments
 (0)