پرش به محتوا

جدول داده

جدول‌های قابل مرتب‌سازی، قابل فیلترسازی و صفحه‌بندی‌شده برای پنل‌های مدیریت، داشبوردها و فهرست سفارش‌ها — با جلوه‌ی نئوبروتالیستی مرزها و سایه‌ها.

جدول داده یک راهنماست نه کامپوننت بسته‌بندی‌شده: جدول خودتان را از کامپوننت <Table /> و TanStack Table — موتور headless برای sort، filter، pagination و انتخاب ردیف — می‌سازید، و دستور پخت نئوبروتالیستی حاشیه‌های ضخیم، سایه‌های سخت و تایپوگرافی پررنگ ظاهر را می‌سازد.

برای این موارد از آن استفاده کنید:

  • پنل ادمین — سفارش، کاربر و فاکتور با ستون‌های قابل‌مرتب‌سازی، فیلتر و منوی اقدام per-row.
  • داشبورد — جدول پرداخت یا فعالیت با سلول‌های قالب‌بندی‌شده، ستون وضعیت و دادهٔ API صفحه‌بندی‌شده.
  • گردش‌کار گروهی — انتخاب ردیف با چک‌باکس برای export، بایگانی یا حذف چند رکورد با هم.

مقدمه

هر جدول داده یا datagrid که ساخته‌ام منحصربه‌فرد بوده است. همهٔ آن‌ها به‌گونه‌ای متفاوت رفتار می‌کنند، الزامات مرتب‌سازی و فیلتر کردن خاصی دارند و با منابع دادهٔ مختلف کار می‌کنند.

ترکیب همهٔ این تغییرات در یک کامپوننت واحد منطقی نیست. اگر این کار را انجام دهیم، انعطاف‌پذیری‌ای را که headless UI فراهم می‌کند از دست خواهیم داد.

بنابراین به‌جای یک کامپوننت data-table، فکر کردم مفیدتر باشد که راهنمایی دربارهٔ نحوهٔ ساخت کامپوننت خودتان ارائه دهم.

ما با کامپوننت پایهٔ <Table /> شروع می‌کنیم و یک جدول دادهٔ پیچیده را از صفر می‌سازیم.

فهرست مطالب

این راهنما به شما نشان می‌دهد که چگونه از TanStack Table و کامپوننت <Table /> برای ساخت جدول دادهٔ سفارشی خودتان استفاده کنید. ما موضوعات زیر را پوشش خواهیم داد:

نصب

  1. کامپوننت <Table /> را به پروژهٔ خود اضافه کنید:
pnpm dlx shadcn@latest add table
npx shadcn@latest add table
yarn dlx shadcn@latest add table
bunx --bun shadcn@latest add table
  1. وابستگی tanstack/react-table را اضافه کنید:
pnpm add @tanstack/react-table
npm install @tanstack/react-table
yarn add @tanstack/react-table
bun add @tanstack/react-table

پیش‌نیازها

ما قصد داریم جدولی برای نمایش پرداخت‌های اخیر بسازیم. داده‌های ما به این شکل است:

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]",
  },
  // ...
]

ساختار پروژه

با ایجاد ساختار فایل زیر شروع کنید:

app
└── payments
    ├── columns.tsx
    ├── data-table.tsx
    └── page.tsx

من در اینجا از یک نمونهٔ Next.js استفاده می‌کنم، اما این با هر فریم‌ورک React دیگری کار می‌کند.

  • columns.tsx (کامپوننت کلاینت) شامل تعاریف ستون‌های ما خواهد بود.
  • data-table.tsx (کامپوننت کلاینت) شامل کامپوننت <DataTable /> ما خواهد بود.
  • page.tsx (کامپوننت سرور) جایی است که در آن داده‌ها را دریافت کرده و جدول خود را رندر می‌کنیم.

جدول پایه

بیایید با ساخت یک جدول پایه شروع کنیم.

تعاریف ستون

ابتدا ستون‌های خود را تعریف می‌کنیم.

app/payments/columns.tsx
"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",
  },
]

کامپوننت <DataTable />

سپس یک کامپوننت <DataTable /> برای رندر کردن جدول خود ایجاد می‌کنیم.

app/payments/data-table.tsx
"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>
  )
}

رندر کردن جدول

در نهایت، جدول خود را در کامپوننت صفحهٔ خود رندر می‌کنیم.

app/payments/page.tsx
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>
  )
}

قالب‌بندی سلول

بیایید سلول مبلغ را قالب‌بندی کنیم تا مبلغ دلاری نمایش داده شود. همچنین سلول را به راست تراز می‌کنیم.

به‌روزرسانی تعریف ستون‌ها

تعاریف header و cell را برای مبلغ به این صورت به‌روزرسانی کنید:

app/payments/columns.tsx
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>
    },
  },
]

می‌توانید از همین رویکرد برای قالب‌بندی سلول‌ها و سرصفحه‌های دیگر استفاده کنید.

اقدامات ردیف

بیایید اقدامات ردیف را به جدول خود اضافه کنیم. برای این کار از یک کامپوننت <DropdownMenu /> استفاده می‌کنیم.

به‌روزرسانی تعریف ستون‌ها

تعریف ستون‌های خود را به‌روزرسانی کنید تا یک ستون actions جدید اضافه شود. سلول actions یک کامپوننت <DropdownMenu /> را برمی‌گرداند.

app/payments/columns.tsx
"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>
      )
    },
  },
  // ...
]

می‌توانید با استفاده از row.original در تابع cell به داده‌های ردیف دسترسی داشته باشید. از این برای مدیریت اقدامات ردیف خود استفاده کنید، برای مثال از id برای انجام یک فراخوانی DELETE به API خود استفاده کنید.

صفحه‌بندی

سپس، صفحه‌بندی را به جدول خود اضافه می‌کنیم.

به‌روزرسانی <DataTable>

app/payments/data-table.tsx
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(),
  })
 
  // ...
}

این کار ردیف‌های شما را به‌صورت خودکار به صفحه‌های ۱۰تایی تقسیم می‌کند. برای اطلاعات بیشتر دربارهٔ سفارشی‌سازی اندازهٔ صفحه و پیاده‌سازی صفحه‌بندی دستی، مستندات صفحه‌بندی را ببینید.

افزودن کنترل‌های صفحه‌بندی

می‌توانیم با استفاده از کامپوننت <Button /> و متدهای API table.previousPage()، table.nextPage() کنترل‌های صفحه‌بندی را به جدول خود اضافه کنیم.

app/payments/data-table.tsx
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>
  )
}

برای یک کامپوننت صفحه‌بندی پیشرفته‌تر، به بخش کامپوننت‌های قابل استفاده مجدد نگاه کنید.

مرتب‌سازی

بیایید ستون email را قابل مرتب‌سازی کنیم.

به‌روزرسانی <DataTable>

app/payments/data-table.tsx
"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>
  )
}

قابل مرتب‌سازی کردن سلول سرصفحه

اکنون می‌توانیم سلول سرصفحهٔ email را به‌روزرسانی کنیم تا کنترل‌های مرتب‌سازی اضافه شوند.

app/payments/columns.tsx
"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>
      )
    },
  },
]

این کار جدول را هنگامی که کاربر روی سلول سرصفحه کلیک می‌کند به‌صورت خودکار مرتب می‌کند (صعودی و نزولی).

فیلتر کردن

بیایید یک ورودی جست‌وجو اضافه کنیم تا ایمیل‌ها را در جدول خود فیلتر کنیم.

به‌روزرسانی <DataTable>

app/payments/data-table.tsx
"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>
  )
}

فیلتر کردن اکنون برای ستون email فعال است. می‌توانید فیلترها را به ستون‌های دیگر نیز اضافه کنید. برای اطلاعات بیشتر دربارهٔ سفارشی‌سازی فیلترها، مستندات فیلتر کردن را ببینید.

قابلیت مشاهده

افزودن قابلیت مشاهدهٔ ستون با استفاده از API قابلیت مشاهدهٔ @tanstack/react-table نسبتاً ساده است.

به‌روزرسانی <DataTable>

app/payments/data-table.tsx
"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>
  )
}

این یک منوی کشویی اضافه می‌کند که می‌توانید از آن برای تغییر قابلیت مشاهدهٔ ستون استفاده کنید.

انتخاب ردیف

سپس، انتخاب ردیف را به جدول خود اضافه می‌کنیم.

به‌روزرسانی تعاریف ستون‌ها

app/payments/columns.tsx
"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,
  },
]

به‌روزرسانی <DataTable>

app/payments/data-table.tsx
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>
  )
}

این یک چک‌باکس به هر ردیف و یک چک‌باکس در سرصفحه برای انتخاب همهٔ ردیف‌ها اضافه می‌کند.

نمایش ردیف‌های انتخاب‌شده

می‌توانید تعداد ردیف‌های انتخاب‌شده را با استفاده از 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>

کامپوننت‌های قابل استفاده مجدد

در اینجا چند کامپوننت آمده است که می‌توانید برای ساخت جدول‌های دادهٔ خود از آن‌ها استفاده کنید. این‌ها از دموی Tasks هستند.

سرصفحهٔ ستون

هر سرصفحهٔ ستونی را قابل مرتب‌سازی و قابل پنهان کردن کنید.

components/data-table-column-header.tsx
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" />
    ),
  },
]

صفحه‌بندی

کنترل‌های صفحه‌بندی را به جدول خود اضافه کنید، از جمله اندازهٔ صفحه و تعداد انتخاب.

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} />

تغییر وضعیت ستون

کامپوننتی برای تغییر قابلیت مشاهدهٔ ستون.

"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

برای فعال‌سازی پشتیبانی RTL در Neobrutalism، راهنمای پیکربندی RTL را ببینید.

دسترسی‌پذیری

TanStack Table سرِ headless است — markup رندر نمی‌کند، پس دسترسی‌پذیری از عناصر این راهنما می‌آید. کامپوننت <Table /> عناصر بومی <table>، <th> و <td> خروجی می‌دهد که به صفحه‌خوان زمینهٔ سطر و ستون را مطابق الگوی WAI-ARIA Table می‌دهند. دو نکته هنگام ساخت: aria-sort را روی header مرتب‌شده بگذارید، و aria-label روی چک‌باکس‌های انتخاب و متن sr-only روی تریگرهای فقط‌آیکون را نگه دارید (snippetهای بالا هر دو را دارند).

هر کنترل این راهنما دکمه، چک‌باکس یا input بومی است، پس رفتار صفحه‌کلید پیش‌فرض مرورگر است:

کلیدعمل
Tab / Shift + Tabحرکت فوکوس در کنترل‌های جدول — input فیلتر، header مرتب‌سازی، منوی ردیف، pagination
Space / Enterفعال‌سازی دکمهٔ sort یا pagination فوکوس‌شده
Space روی چک‌باکستغییر انتخاب ردیف یا select-all