Data Table
Shadcn Docs
Installation
yarn add @tanstack/react-tableUsage
| Tên nhân viên | Số điện thoại | Vị trí | Công ty | Đội | Trạng thái | |
|---|---|---|---|---|---|---|
ER | +1 (555) 123-4567 | Product Manager | Tech Corp | Development | active | |
ER | +1 (555) 345-6789 | Software Engineer | Tech Corp | Development | active | |
ER | +1 (555) 456-7890 | Sales Manager | Tech Corp | Sales | inactive | |
ER | +1 (555) 567-8901 | Product Manager | Tech Corp | Finance | active | |
ER | +1 (555) 678-9012 | Software Engineer | Tech Corp | Development | active | |
ER | +1 (555) 789-0123 | Designer | Tech Corp | Design | inactive | |
ER | +1 (555) 890-1234 | Sales Manager | Tech Corp | Sales | active | |
ER | +1 (555) 901-2345 | Software Engineer | Tech Corp | Development | active | |
ER | +1 (555) 012-3456 | Product Manager | Tech Corp | Marketing Development | inactive |
Folder Structure
employees/
data-table
├── columns.tsx
└── index.tsxBasic Table
Column Definitions
'use client';
import {
Avatar,
AvatarFallback,
AvatarImage,
Badge,
Checkbox,
Label,
} from '247-components-ui/v2';
import { ColumnDef } from '@tanstack/react-table';
// This type is used to define the shape of our data.
// You can use a Zod schema here if you want.
export type Employee = {
id: string;
avatar: string;
name: string;
email: string;
phone: string;
role: 'Product Manager' | 'Software Engineer' | 'Designer' | 'Sales Manager';
company: string;
team: ('Design' | 'Development' | 'Marketing' | 'Sales' | 'HR' | 'Finance')[];
status: 'active' | 'inactive';
};
export const columns: ColumnDef<Employee>[] = [
{
id: 'select',
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && 'indeterminate')
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'name',
header: 'Tên nhân viên',
enablePinning: true,
size: 100,
cell: ({ row }) => {
return (
<div className="flex items-center gap-2">
<Avatar>
<AvatarImage src={row.original.avatar} alt="@evilrabbit" />
<AvatarFallback>ER</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<Label className="text-md font-semibold">{row.original.name}</Label>
<Label className="text-muted-foreground text-sm">
{row.original.email}
</Label>
</div>
</div>
);
},
},
{
accessorKey: 'phone',
header: 'Số điện thoại',
size: 150,
},
{
accessorKey: 'role',
header: 'Vị trí',
size: 100,
},
{
accessorKey: 'company',
header: 'Công ty',
size: 100,
},
{
accessorKey: 'team',
header: 'Đội',
size: 150,
cell: ({ row }) => {
return (
<div className="flex flex-nowrap gap-2">
{row.original.team.map((team) => (
<Badge key={team} variant="outline">
{team}
</Badge>
))}
</div>
);
},
},
{
accessorKey: 'status',
header: 'Trạng thái',
size: 150,
cell: ({ row }) => {
if (row.original.status === 'active') {
return (
<Badge className="border-success-700 bg-success-50 hover:bg-success-100 text-success-700 capitalize">
<span
className="bg-success-500 size-1.5 rounded-full"
aria-hidden="true"
/>
{row.original.status}
</Badge>
);
}
return (
<Badge className="border-error-700 bg-error-50 hover:bg-error-100 text-error-700 capitalize">
<span
className="bg-error-500 size-1.5 rounded-full"
aria-hidden="true"
/>
{row.original.status}
</Badge>
);
},
},
{
id: 'actions',
header: 'Thao tác',
size: 100,
cell: ({ row }) => {
return (
<div className="flex items-center gap-2">
<Button size="sm" variant="outline">
Sửa
</Button>
<Button size="sm" variant="destructive">
Xóa
</Button>
</div>
);
},
},
];Pinned Columns
const pinnedRightClasses =
'sticky right-0 bg-white after:absolute after:top-0 after:h-full after:w-2 after:shadow-fixed-right-column';Add property for Column Meta
declare in src root folder, example in data-table.d.ts file
declare module '@tanstack/table-core' {
interface ColumnMeta<TData, TValue> {
className?: string;
}
}DataTable component
'use client';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '247-components-ui/v2';
import { cn } from '@/lib/utils';
import Image from 'next/image';
import { EmptyStateBg } from '@/assets/images/background';
import {
ColumnDef,
flexRender,
getCoreRowModel,
OnChangeFn,
PaginationOptions,
PaginationState,
SortingState,
useReactTable,
} from '@tanstack/react-table';
import { Filters } from '@/types/data-table';
// CSS classes for pinned columns
const pinnedRightClasses =
'sticky right-0 bg-white after:absolute after:top-0 after:h-full after:w-2 after:shadow-fixed-right-column';
type Props<TData, TValue> = {
data: TData[];
columns: ColumnDef<TData, TValue>[];
pagination: PaginationState;
paginationOptions: Pick<PaginationOptions, 'onPaginationChange' | 'rowCount'>;
sorting: SortingState;
onSortingChange: OnChangeFn<SortingState>;
className?: string;
};
export default function DataTable<TData, TValue>({
data,
columns,
pagination,
paginationOptions,
sorting,
onSortingChange,
className,
}: Props<TData, TValue>) {
const table = useReactTable({
data,
columns,
state: {
pagination,
sorting,
columnPinning: {
right: ['actions'], // Pin actions column to the right
},
},
...paginationOptions,
onSortingChange,
manualFiltering: true,
manualSorting: true,
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
});
return (
<Table className="relative overflow-auto border">
<TableHeader className="sticky top-0 z-10">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const isPinned = header.column.getIsPinned?.();
return (
<TableHead
key={header.id}
className={cn(
isPinned === 'right' ? pinnedRightClasses : '',
header.column.columnDef.meta?.className,
)}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => {
const isPinned = cell.column.getIsPinned?.();
return (
<TableCell
key={cell.id}
className={cn(
isPinned === 'right' ? pinnedRightClasses : '',
)}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
);
})}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="relative h-[600px] text-center">
<Image
src={EmptyStateBg.src}
height={480}
width={480}
alt="empty"
className={cn('absolute top-0 left-1/2 -translate-x-1/2')}
/>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center justify-center text-center">
<span className="text-md text-text-primary font-semibold mb-1">
Danh sách {emptyLabel} đang trống
</span>
<span className="text-sm text-text-tertiary font-normal">
Ấn vào "Thêm mới" để thêm {emptyLabel}
</span>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
);
}
Render Table
const data = [
{
id: 1,
name: 'John Doe',
email: 'john.doe@example.com',
phone: '1234567890',
},
...
];
export default async function DemoPage() {
const [paginationState, setPaginationState] = useState<PaginationState>({
pageIndex: 0,
pageSize: 20,
});
const [sorting, setSorting] = useState<SortingState>([]);
return (
<div className="container mx-auto py-10">
<DataTable
data={data}
columns={columns}
pagination={paginationState}
paginationOptions={{
onPaginationChange: (pagination) => {
const newPagination =
typeof pagination === 'function'
? pagination(paginationState)
: pagination;
setPaginationState({
pageIndex: newPagination.pageIndex + 1,
pageSize: newPagination.pageSize,
});
},
rowCount: data.length ?? 0,
}}
sorting={sorting}
onSortingChange={(sorting) => {
setSorting(sorting);
}}
/>
</div>
);
}Pagination
- Example apply Group Button Style of Pagination Component
import { Table } from '@tanstack/react-table';
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationRange,
SelectGroup,
} from '247-components-ui/v2';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '247-components-ui/v2';
const DEFAULT_PAGE_SIZE_OPTIONS = [10, 20, 30, 40, 50];
interface DataTablePaginationProps<TData> {
table: Table<TData>;
}
export function DataTablePagination<TData>({
table,
}: DataTablePaginationProps<TData>) {
return (
<Pagination>
<PaginationContent className="flex-1 justify-end gap-4">
<PaginationItem>
<PaginationRange
size="md"
currentPage={table.getState().pagination.pageIndex + 1}
pageSize={table.getState().pagination.pageSize}
totalPages={table.getPageCount()}
isGroupButton
onPageChange={(page) => {
table.setPageIndex(page - 1);
}}
/>
</PaginationItem>
<PaginationItem>
<Select
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
value={table.getState().pagination.pageSize.toString()}
>
<SelectTrigger className="w-[115px]">
<SelectValue placeholder="Chọn" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{DEFAULT_PAGE_SIZE_OPTIONS.map((option) => (
<SelectItem key={option} value={option.toString()}>
{option}/trang
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</PaginationItem>
</PaginationContent>
</Pagination>
);
}Update DataTable Component
return (
<div className="flex flex-col gap-4">
<Table className="relative overflow-auto border">
<TableHeader className="sticky top-0 z-10">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const isPinned = header.column.getIsPinned?.();
return (
<TableHead
key={header.id}
className={cn(
isPinned === 'right' ? pinnedRightClasses : '',
header.column.columnDef.meta?.className
)}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => {
const isPinned = cell.column.getIsPinned?.();
return (
<TableCell
key={cell.id}
className={cn(
isPinned === 'right' ? pinnedRightClasses : ''
)}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
);
})}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="relative h-[600px] text-center"
>
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
<span className="text-md text-text-primary mb-1 font-semibold">
Danh sách đang trống
</span>
<span className="text-text-tertiary text-sm font-normal">
Ấn vào "Thêm mới" để thêm
</span>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<DataTablePagination table={table} />
</div>
);Last updated on