Bảng dữ liệu
Các bảng có thể sắp xếp, lọc và phân trang dành cho bảng điều khiển quản trị, bảng điều khiển tổng quan và danh sách đơn hàng — với phong cách thiết kế “neobrutalist” kết hợp đường viền và bóng đổ.
Data table là một hướng dẫn, không phải một thành phần đóng gói sẵn: bạn tự dựng bảng của mình từ thành phần <Table /> và TanStack Table, một engine headless lo phần sắp xếp, lọc, phân trang và chọn dòng — còn phần hình thức do công thức neobrutalist với viền dày, bóng đổ đậm và kiểu chữ đậm đảm nhiệm.
Nên dùng khi:
- Trang quản trị — đơn hàng, người dùng và hóa đơn với cột sắp xếp được, bộ lọc và menu hành động trên từng dòng.
- Dashboard — bảng thanh toán hoặc hoạt động với ô đã định dạng, cột trạng thái và dữ liệu API phân trang.
- Luồng xử lý hàng loạt — chọn dòng bằng checkbox để xuất, lưu trữ hoặc xóa nhiều bản ghi cùng lúc.
Giới thiệu#
Mỗi bảng dữ liệu hoặc datagrid mà tôi tạo ra đều độc đáo. Tất cả chúng đều hoạt động khác nhau, có các yêu cầu sắp xếp và lọc cụ thể, và làm việc với các nguồn dữ liệu khác nhau.
Việc kết hợp tất cả các biến thể này vào một thành phần duy nhất là không hợp lý. Nếu làm vậy, chúng ta sẽ mất đi sự linh hoạt mà headless UI mang lại.
Vì vậy, thay vì một thành phần data-table, tôi nghĩ sẽ hữu ích hơn nếu cung cấp một hướng dẫn về cách xây dựng thành phần của riêng bạn.
Chúng ta sẽ bắt đầu với thành phần <Table /> cơ bản và xây dựng một bảng dữ liệu phức tạp từ đầu.
Mẹo: Nếu bạn thấy mình đang dùng cùng một bảng ở nhiều nơi trong ứng dụng, bạn luôn có thể tách nó thành một thành phần có thể tái sử dụng.
Mục lục#
Hướng dẫn này sẽ chỉ cho bạn cách dùng TanStack Table và thành phần <Table /> để xây dựng bảng dữ liệu tùy chỉnh của riêng bạn. Chúng ta sẽ đề cập đến các chủ đề sau:
- Bảng cơ bản
- Hành động hàng
- Phân trang
- Sắp xếp
- Lọc
- Hiển thị
- Chọn hàng
- Các thành phần có thể tái sử dụng
Cài đặt#
- Thêm thành phần
<Table />vào dự án của bạn:
pnpm dlx shadcn@latest add tablenpx shadcn@latest add tableyarn dlx shadcn@latest add tablebunx --bun shadcn@latest add table
- Thêm phụ thuộc
tanstack/react-table:
pnpm add @tanstack/react-tablenpm install @tanstack/react-tableyarn add @tanstack/react-tablebun add @tanstack/react-table
Điều kiện tiên quyết#
Chúng ta sẽ xây dựng một bảng để hiển thị các khoản thanh toán gần đây. Dữ liệu của chúng ta trông như sau:
type Payment = {
id: string
amount: number
status: "pending" | "processing" | "success" | "failed"
email: string
}
export const payments: Payment[] = [
{
id: "728ed52f",
amount: 100,
status: "pending",
email: "[email protected]",
},
{
id: "489e1d42",
amount: 125,
status: "processing",
email: "[email protected]",
},
// ...
]Cấu trúc dự án#
Bắt đầu bằng cách tạo cấu trúc tệp sau:
app
└── payments
├── columns.tsx
├── data-table.tsx
└── page.tsxỞ đây tôi đang dùng một ví dụ Next.js nhưng cách này hoạt động với bất kỳ framework React nào khác.
columns.tsx(thành phần client) sẽ chứa các định nghĩa cột của chúng ta.data-table.tsx(thành phần client) sẽ chứa thành phần<DataTable />của chúng ta.page.tsx(thành phần server) là nơi chúng ta sẽ lấy dữ liệu và render bảng của mình.
Bảng cơ bản#
Hãy bắt đầu bằng cách xây dựng một bảng cơ bản.
Định nghĩa cột#
Trước tiên, chúng ta sẽ định nghĩa các cột của mình.
"use client"
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 Payment = {
id: string
amount: number
status: "pending" | "processing" | "success" | "failed"
email: string
}
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: "status",
header: "Status",
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "amount",
header: "Amount",
},
]Lưu ý: Các cột là nơi bạn định nghĩa cốt lõi về hình thức của bảng. Chúng xác định dữ liệu sẽ được hiển thị, cách nó được định dạng, sắp xếp và lọc.
Thành phần <DataTable />#
Tiếp theo, chúng ta sẽ tạo một thành phần <DataTable /> để render bảng của mình.
"use client"
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
}
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<div className="overflow-hidden rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{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) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}Mẹo: Nếu bạn thấy mình đang dùng <DataTable /> ở nhiều nơi, đây là thành phần bạn có thể làm cho có thể tái sử dụng bằng cách tách nó ra components/ui/data-table.tsx.
<DataTable columns={columns} data={data} />
Render bảng#
Cuối cùng, chúng ta sẽ render bảng của mình trong thành phần trang.
import { columns, Payment } from "./columns"
import { DataTable } from "./data-table"
async function getData(): Promise<Payment[]> {
// Fetch data from your API here.
return [
{
id: "728ed52f",
amount: 100,
status: "pending",
email: "[email protected]",
},
// ...
]
}
export default async function DemoPage() {
const data = await getData()
return (
<div className="container mx-auto py-10">
<DataTable columns={columns} data={data} />
</div>
)
}Định dạng ô#
Hãy định dạng ô số tiền để hiển thị số tiền bằng đô la. Chúng ta cũng sẽ căn ô sang phải.
Cập nhật định nghĩa cột#
Cập nhật các định nghĩa header và cell cho số tiền như sau:
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: "amount",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"))
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount)
return <div className="text-right font-medium">{formatted}</div>
},
},
]Bạn có thể dùng cùng cách tiếp cận để định dạng các ô và tiêu đề khác.
Hành động hàng#
Hãy thêm các hành động hàng vào bảng của chúng ta. Chúng ta sẽ dùng một thành phần <DropdownMenu /> cho việc này.
Cập nhật định nghĩa cột#
Cập nhật định nghĩa cột của chúng ta để thêm một cột actions mới. Ô actions trả về một thành phần <DropdownMenu />.
"use client"
import { ColumnDef } from "@tanstack/react-table"
import { MoreHorizontal } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export const columns: ColumnDef<Payment>[] = [
// ...
{
id: "actions",
cell: ({ row }) => {
const payment = row.original
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(payment.id)}
>
Copy payment ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
},
},
// ...
]Bạn có thể truy cập dữ liệu hàng bằng row.original trong hàm cell. Dùng cái này để xử lý các hành động cho hàng của bạn, ví dụ dùng id để thực hiện một lệnh gọi DELETE đến API của bạn.
Phân trang#
Tiếp theo, chúng ta sẽ thêm phân trang vào bảng của mình.
Cập nhật <DataTable>#
import {
ColumnDef,
flexRender,
getCoreRowModel,
getPaginationRowModel,
useReactTable,
} from "@tanstack/react-table"
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
// ...
}Điều này sẽ tự động phân trang các hàng của bạn thành các trang gồm 10 hàng. Xem tài liệu phân trang để biết thêm thông tin về việc tùy chỉnh kích thước trang và triển khai phân trang thủ công.
Thêm các điều khiển phân trang#
Chúng ta có thể thêm các điều khiển phân trang vào bảng của mình bằng thành phần <Button /> và các phương thức API table.previousPage(), table.nextPage().
import { Button } from "@/components/ui/button"
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
return (
<div>
<div className="overflow-hidden rounded-md border">
<Table>
{ // .... }
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
)
}Xem phần Các thành phần có thể tái sử dụng để biết một thành phần phân trang nâng cao hơn.
Sắp xếp#
Hãy làm cho cột email có thể sắp xếp.
Cập nhật <DataTable>#
"use client"
import * as React from "react"
import {
ColumnDef,
SortingState,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([])
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
state: {
sorting,
},
})
return (
<div>
<div className="overflow-hidden rounded-md border">
<Table>{ ... }</Table>
</div>
</div>
)
}Làm cho ô tiêu đề có thể sắp xếp#
Bây giờ chúng ta có thể cập nhật ô tiêu đề email để thêm các điều khiển sắp xếp.
"use client"
import { ColumnDef } from "@tanstack/react-table"
import { ArrowUpDown } from "lucide-react"
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: "email",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Email
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
},
]Điều này sẽ tự động sắp xếp bảng (tăng dần và giảm dần) khi người dùng nhấp vào ô tiêu đề.
Lọc#
Hãy thêm một ô nhập tìm kiếm để lọc email trong bảng của chúng ta.
Cập nhật <DataTable>#
"use client"
import * as React from "react"
import {
ColumnDef,
ColumnFiltersState,
SortingState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
)
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
state: {
sorting,
columnFilters,
},
})
return (
<div>
<div className="flex items-center py-4">
<Input
placeholder="Filter emails..."
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("email")?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
</div>
<div className="overflow-hidden rounded-md border">
<Table>{ ... }</Table>
</div>
</div>
)
}Việc lọc giờ đây đã được bật cho cột email. Bạn cũng có thể thêm bộ lọc vào các cột khác. Xem tài liệu lọc để biết thêm thông tin về việc tùy chỉnh bộ lọc.
Hiển thị#
Việc thêm khả năng hiển thị cột khá đơn giản khi dùng API hiển thị của @tanstack/react-table.
Cập nhật <DataTable>#
"use client"
import * as React from "react"
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
)
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({})
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
state: {
sorting,
columnFilters,
columnVisibility,
},
})
return (
<div>
<div className="flex items-center py-4">
<Input
placeholder="Filter emails..."
value={table.getColumn("email")?.getFilterValue() as string}
onChange={(event) =>
table.getColumn("email")?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter(
(column) => column.getCanHide()
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="overflow-hidden rounded-md border">
<Table>{ ... }</Table>
</div>
</div>
)
}Điều này thêm một menu thả xuống mà bạn có thể dùng để bật/tắt khả năng hiển thị cột.
Chọn hàng#
Tiếp theo, chúng ta sẽ thêm việc chọn hàng vào bảng của mình.
Cập nhật các định nghĩa cột#
"use client"
import { ColumnDef } from "@tanstack/react-table"
import { Badge } from "@/components/ui/badge"
import { Checkbox } from "@/components/ui/checkbox"
export const columns: ColumnDef<Payment>[] = [
{
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,
},
]Cập nhật <DataTable>#
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
)
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({})
const [rowSelection, setRowSelection] = React.useState({})
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
})
return (
<div>
<div className="overflow-hidden rounded-md border">
<Table />
</div>
</div>
)
}Điều này thêm một checkbox vào mỗi hàng và một checkbox ở tiêu đề để chọn tất cả các hàng.
Hiển thị các hàng đã chọn#
Bạn có thể hiển thị số hàng đã chọn bằng API table.getFilteredSelectedRowModel().
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>Các thành phần có thể tái sử dụng#
Dưới đây là một số thành phần bạn có thể dùng để xây dựng các bảng dữ liệu của mình. Chúng đến từ bản demo Tasks.
Tiêu đề cột#
Làm cho bất kỳ tiêu đề cột nào có thể sắp xếp và ẩn được.
import { type Column } from "@tanstack/react-table"
import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/registry/base/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/registry/base/ui/dropdown-menu"
interface DataTableColumnHeaderProps<
TData,
TValue,
> extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>
title: string
}
export function DataTableColumnHeader<TData, TValue>({
column,
title,
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>
}
return (
<div className={cn("flex items-center gap-2", className)}>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8 data-popup-open:bg-accent"
/>
}
>
<span>{title}</span>
{column.getIsSorted() === "desc" ? (
<ArrowDown />
) : column.getIsSorted() === "asc" ? (
<ArrowUp />
) : (
<ChevronsUpDown />
)}
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUp />
Asc
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDown />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeOff />
Hide
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
export const columns = [
{
accessorKey: "email",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Email" />
),
},
]Phân trang#
Thêm các điều khiển phân trang vào bảng của bạn bao gồm kích thước trang và số lượng lựa chọn.
import { type Table } from "@tanstack/react-table"
import {
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
} from "lucide-react"
import { Button } from "@/registry/base/ui/button"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/base/ui/select"
const pageSizeItems = [10, 20, 25, 30, 40, 50].map((pageSize) => ({
label: `${pageSize}`,
value: `${pageSize}`,
}))
interface DataTablePaginationProps<TData> {
table: Table<TData>
}
export function DataTablePagination<TData>({
table,
}: DataTablePaginationProps<TData>) {
return (
<div className="flex items-center justify-between px-2">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex items-center space-x-6 lg:space-x-8">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
items={pageSizeItems}
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
if (value !== null) {
table.setPageSize(Number(value))
}
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue />
</SelectTrigger>
<SelectContent side="top">
{pageSizeItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="icon"
className="hidden size-8 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<ChevronsLeft />
</Button>
<Button
variant="outline"
size="icon"
className="size-8"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeft />
</Button>
<Button
variant="outline"
size="icon"
className="size-8"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRight />
</Button>
<Button
variant="outline"
size="icon"
className="hidden size-8 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<ChevronsRight />
</Button>
</div>
</div>
</div>
)
}
<DataTablePagination table={table} />Bật/tắt cột#
Một thành phần để bật/tắt khả năng hiển thị cột.
"use client"
import { type Table } from "@tanstack/react-table"
import { Settings2 } from "lucide-react"
import { Button } from "@/registry/base/ui/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/registry/base/ui/dropdown-menu"
export function DataTableViewOptions<TData>({
table,
}: {
table: Table<TData>
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="outline"
size="sm"
className="ml-auto hidden h-8 lg:flex"
/>
}
>
<Settings2 />
View
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[150px]">
{/* GroupLabel (Base UI) needs a Group ancestor or it throws error #31. */}
<DropdownMenuGroup>
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
{table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" && column.getCanHide()
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
)
}
<DataTableViewOptions table={table} />RTL#
Để bật hỗ trợ RTL trong Neobrutalism, hãy xem hướng dẫn cấu hình RTL.
Khả năng truy cập#
TanStack Table là headless — nó không render markup nào, nên khả năng truy cập đến từ chính những phần tử trong hướng dẫn này. Thành phần <Table /> xuất ra các phần tử <table>, <th> và <td> native, giúp trình đọc màn hình nắm được ngữ cảnh dòng và cột theo mẫu Table của WAI-ARIA. Hai điều cần giữ khi bạn dựng bảng: đặt aria-sort trên header đang được sắp xếp, và giữ aria-label trên các checkbox chọn dòng cùng phần chữ sr-only trên các trigger chỉ có icon (những đoạn mã ở trên đã làm cả hai việc đó).
Mọi điều khiển trong hướng dẫn này đều là nút, checkbox hoặc input native, nên hành vi bàn phím chính là mặc định của trình duyệt:
| Phím | Hành động |
|---|---|
Tab / Shift + Tab | Di chuyển focus qua các điều khiển của bảng — ô lọc, header sắp xếp, menu dòng, phân trang |
Space / Enter | Kích hoạt nút sắp xếp hoặc nút phân trang đang được focus |
Space | Đổi trạng thái checkbox của dòng đang được focus hoặc checkbox chọn tất cả |