Table (components/ui/table.tsx)¶
Overview¶
A comprehensive table component that provides sorting, filtering, pagination, row selection, and virtual scrolling capabilities for managing tabular data.
Features¶
- Column sorting
- Data filtering
- Pagination
- Row selection
- Virtual scrolling
- Fixed headers
- Column resizing
- Custom cell rendering
- Row actions
- Bulk actions
- Loading states
- Empty states
- Error handling
- Responsive design
- Accessibility support
- Keyboard navigation
Dependencies¶
import * as React from 'react'
import { ChevronUpDown, MoreVertical } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from './button'
import { Checkbox } from './checkbox'
import { Select, SelectItem } from './select'
import { Spinner } from './spinner'
Props¶
interface TableProps {
columns: TableColumn[]
rows: any[]
showSelection?: boolean
showPagination?: boolean
defaultSort?: { column: string, direction: 'asc' | 'desc' }
onSort?: (column: string, direction: 'asc' | 'desc') => void
onSelect?: (selectedRows: string[]) => void
onPageChange?: (page: number) => void
loading?: boolean
error?: string
className?: string
}
Implementation¶
State Management¶
interface TableState {
sortColumn: string | null
sortDirection: 'asc' | 'desc'
selectedRows: string[]
page: number
pageSize: number
filters: Record<string, any>
loading: boolean
error: string | null
}
const [tableState, setTableState] = useState<TableState>({
sortColumn: null,
sortDirection: 'asc',
selectedRows: [],
page: 1,
pageSize: 10,
filters: {},
loading: false,
error: null
})
Methods¶
// Handles column sort
const handleSort = (column: string) => {
setTableState(prev => ({
...prev,
sortColumn: column,
sortDirection: prev.sortColumn === column && prev.sortDirection === 'asc' ? 'desc' : 'asc'
}))
}
// Handles row selection
const handleRowSelect = (rowId: string) => {
setTableState(prev => ({
...prev,
selectedRows: prev.selectedRows.includes(rowId)
? prev.selectedRows.filter(id => id !== rowId)
: [...prev.selectedRows, rowId]
}))
}
// Handles bulk selection
const handleBulkSelect = (checked: boolean) => {
setTableState(prev => ({
...prev,
selectedRows: checked ? rows.map(row => row.id) : []
}))
}
// Handles pagination
const handlePageChange = (page: number) => {
setTableState(prev => ({
...prev,
page
}))
}
// Handles filter change
const handleFilterChange = (column: string, value: any) => {
setTableState(prev => ({
...prev,
filters: {
...prev.filters,
[column]: value
},
page: 1
}))
}
Unique Functionality¶
Virtual Scrolling System¶
const VirtualTable = memo(({ rows, rowHeight = 40 }) => {
const containerRef = useRef<HTMLDivElement>(null)
const [visibleRows, setVisibleRows] = useState<typeof rows>([])
const [scrollTop, setScrollTop] = useState(0)
useEffect(() => {
if (!containerRef.current) return
const startIndex = Math.floor(scrollTop / rowHeight)
const endIndex = Math.min(
rows.length,
Math.ceil((scrollTop + containerRef.current.clientHeight) / rowHeight)
)
setVisibleRows(rows.slice(startIndex, endIndex))
}, [rows, scrollTop, rowHeight])
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
setScrollTop(event.currentTarget.scrollTop)
}, [])
return (
<div
ref={containerRef}
className="max-h-[500px] overflow-auto"
onScroll={handleScroll}
>
<div style={{ height: rows.length * rowHeight }}>
<div
style={{
transform: `translateY(${Math.floor(scrollTop / rowHeight) * rowHeight}px)`
}}
>
{visibleRows.map(row => (
<TableRow key={row.id} {...row} />
))}
</div>
</div>
</div>
)
})
HTML Structure¶
<div className="w-full overflow-auto">
<table className="w-full caption-bottom text-sm">
<thead className={cn(
"bg-muted/50 sticky top-0",
"[&_tr]:border-b"
)}>
<tr className="border-b transition-colors hover:bg-muted/50">
{showSelection && (
<th className="h-12 w-12 px-4">
<Checkbox
checked={tableState.selectedRows.length === rows.length}
onCheckedChange={handleBulkSelect}
/>
</th>
)}
{columns.map(column => (
<th
key={column.key}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground",
column.sortable && "cursor-pointer select-none",
column.className
)}
onClick={() => column.sortable && handleSort(column.key)}
>
<div className="flex items-center gap-2">
{column.header}
{column.sortable && tableState.sortColumn === column.key && (
<ChevronUpDown className="h-4 w-4" />
)}
</div>
</th>
))}
</tr>
</thead>
<tbody>
{tableState.loading ? (
<tr>
<td
colSpan={columns.length + (showSelection ? 1 : 0)}
className="h-24 text-center"
>
<Spinner className="h-6 w-6" />
</td>
</tr>
) : rows.length === 0 ? (
<tr>
<td
colSpan={columns.length + (showSelection ? 1 : 0)}
className="h-24 text-center"
>
No results found
</td>
</tr>
) : (
<VirtualTable rows={rows} />
)}
</tbody>
</table>
{showPagination && (
<div className="flex items-center justify-between px-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{tableState.selectedRows.length} of {rows.length} row(s) selected
</div>
<div className="flex items-center gap-6 lg:gap-8">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
value={String(tableState.pageSize)}
onValueChange={value => setTableState(prev => ({
...prev,
pageSize: Number(value)
}))}
>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
</Select>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(tableState.page - 1)}
disabled={tableState.page === 1}
>
Previous
</Button>
<div className="text-sm font-medium">
Page {tableState.page} of {Math.ceil(rows.length / tableState.pageSize)}
</div>
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(tableState.page + 1)}
disabled={tableState.page === Math.ceil(rows.length / tableState.pageSize)}
>
Next
</Button>
</div>
</div>
</div>
)}
</div>
API Integration¶
No direct API routes used - this is a UI component for displaying and managing tabular data.
Components Used¶
- Button
- Checkbox
- Select
- SelectItem
- Spinner
Notes¶
- Implement efficient virtual scrolling
- Handle sorting and filtering properly
- Maintain responsive layout
- Support keyboard navigation
- Clean up event listeners on unmount