Skip to Content
247 Components v1.0 is released 🎉
Components
Data Table

Data Table

Shadcn Docs

Installation

yarn add @tanstack/react-table

Usage

Tên nhân viênSố điện thoạiVị tríCông tyĐộiTrạng thái
ER
+1 (555) 123-4567Product ManagerTech Corp
Development
active
ER
+1 (555) 345-6789Software EngineerTech Corp
Development
active
ER
+1 (555) 456-7890Sales ManagerTech Corp
Sales
inactive
ER
+1 (555) 567-8901Product ManagerTech Corp
Finance
active
ER
+1 (555) 678-9012Software EngineerTech Corp
Development
active
ER
+1 (555) 789-0123DesignerTech Corp
Design
inactive
ER
+1 (555) 890-1234Sales ManagerTech Corp
Sales
active
ER
+1 (555) 901-2345Software EngineerTech Corp
Development
active
ER
+1 (555) 012-3456Product ManagerTech Corp
Marketing
Development
inactive

Folder Structure

employees/ data-table ├── columns.tsx └── index.tsx

Basic 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 &quot;Thêm mới&quot; để 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

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 &quot;Thêm mới&quot; để thêm </span> </div> </TableCell> </TableRow> )} </TableBody> </Table> <DataTablePagination table={table} /> </div> );
Last updated on