جدول داده (Data Table)

جدول داده‌های قدرتمند با استفاده از TanStack Table و کامپوننت <Table /> — شامل مرتب‌سازی، فیلتر، صفحه‌بندی، انتخاب ردیف و مخفی‌سازی ستون‌ها.

نصب (Installation)

ابتدا کامپوننت Table را نصب کنید:

1npx @quark-lab/rad-ui add table

سپس وابستگی @tanstack/react-table را اضافه کنید:

1npm install @tanstack/react-table

جدول پایه (Basic Table)

ساده‌ترین حالت استفاده از Data Table با تعریف ستون‌ها و نمایش داده‌ها.

وضعیتایمیل
مبلغ
موفقken99@example.com
۳۱۶٬۰۰۰ تومان
موفقabe45@example.com
۲۴۲٬۰۰۰ تومان
در حال پردازشmonserrat44@example.com
۸۳۷٬۰۰۰ تومان
موفقsilas22@example.com
۸۷۴٬۰۰۰ تومان
ناموفقcarmella@example.com
۷۲۱٬۰۰۰ تومان
مشاهده کد
1"use client";
2
3import {
4  ColumnDef,
5  flexRender,
6  getCoreRowModel,
7  useReactTable,
8} from "@tanstack/react-table";
9import {
10  Table,
11  TableBody,
12  TableCell,
13  TableHead,
14  TableHeader,
15  TableRow,
16} from "@/components/ui/table";
17
18interface Payment {
19  id: string;
20  amount: number;
21  status: "موفق" | "در انتظار" | "ناموفق" | "در حال پردازش";
22  email: string;
23}
24
25const columns: ColumnDef<Payment>[] = [
26  {
27    accessorKey: "status",
28    header: "وضعیت",
29  },
30  {
31    accessorKey: "email",
32    header: "ایمیل",
33  },
34  {
35    accessorKey: "amount",
36    header: () => <div className="text-end">مبلغ</div>,
37    cell: ({ row }) => {
38      const amount = parseFloat(row.getValue("amount"));
39      const formatted = new Intl.NumberFormat("fa-IR").format(amount);
40      return <div className="text-end font-medium">{formatted} تومان</div>;
41    },
42  },
43];
44
45interface DataTableProps<TData, TValue> {
46  columns: ColumnDef<TData, TValue>[];
47  data: TData[];
48}
49
50function DataTable<TData, TValue>({
51  columns,
52  data,
53}: DataTableProps<TData, TValue>) {
54  const table = useReactTable({
55    data,
56    columns,
57    getCoreRowModel: getCoreRowModel(),
58  });
59
60  return (
61    <div className="overflow-hidden rounded-md border">
62      <Table>
63        <TableHeader>
64          {table.getHeaderGroups().map((headerGroup) => (
65            <TableRow key={headerGroup.id}>
66              {headerGroup.headers.map((header) => (
67                <TableHead key={header.id}>
68                  {header.isPlaceholder
69                    ? null
70                    : flexRender(
71                        header.column.columnDef.header,
72                        header.getContext()
73                      )}
74                </TableHead>
75              ))}
76            </TableRow>
77          ))}
78        </TableHeader>
79        <TableBody>
80          {table.getRowModel().rows?.length ? (
81            table.getRowModel().rows.map((row) => (
82              <TableRow key={row.id}>
83                {row.getVisibleCells().map((cell) => (
84                  <TableCell key={cell.id}>
85                    {flexRender(
86                      cell.column.columnDef.cell,
87                      cell.getContext()
88                    )}
89                  </TableCell>
90                ))}
91              </TableRow>
92            ))
93          ) : (
94            <TableRow>
95              <TableCell
96                colSpan={columns.length}
97                className="h-24 text-center"
98              >
99                نتیجه‌ای یافت نشد.
100              </TableCell>
101            </TableRow>
102          )}
103        </TableBody>
104      </Table>
105    </div>
106  );
107}

جدول کامل (Full-Featured Table)

جدول با مرتب‌سازی، فیلتر ایمیل، صفحه‌بندی، نمایش/مخفی‌سازی ستون‌ها، انتخاب ردیف و عملیات ردیف.

وضعیت
مبلغ
موفق
ken99@example.com
۳۱۶٬۰۰۰ تومان
موفق
abe45@example.com
۲۴۲٬۰۰۰ تومان
در حال پردازش
monserrat44@example.com
۸۳۷٬۰۰۰ تومان
موفق
silas22@example.com
۸۷۴٬۰۰۰ تومان
ناموفق
carmella@example.com
۷۲۱٬۰۰۰ تومان
در انتظار
ali.r@example.com
۱۵۰٬۰۰۰ تومان
موفق
sara.m@example.com
۴۹۰٬۰۰۰ تومان
در حال پردازش
reza@example.com
۶۲۰٬۰۰۰ تومان
ناموفق
maryam@example.com
۱۸۵٬۰۰۰ تومان
موفق
hossein@example.com
۹۳۰٬۰۰۰ تومان
0 از 12 ردیف انتخاب شده.
صفحه ۱ از ۲
مشاهده کد
1"use client";
2
3import * as React from "react";
4import {
5  ColumnDef,
6  ColumnFiltersState,
7  SortingState,
8  VisibilityState,
9  flexRender,
10  getCoreRowModel,
11  getFilteredRowModel,
12  getPaginationRowModel,
13  getSortedRowModel,
14  useReactTable,
15} from "@tanstack/react-table";
16import { ArrowUpDown, MoreHorizontal } from "lucide-react";
17import { Badge } from "@/components/ui/badge";
18import { Button } from "@/components/ui/button";
19import { Checkbox } from "@/components/ui/checkbox";
20import {
21  DropdownMenu,
22  DropdownMenuCheckboxItem,
23  DropdownMenuContent,
24  DropdownMenuItem,
25  DropdownMenuLabel,
26  DropdownMenuSeparator,
27  DropdownMenuTrigger,
28} from "@/components/ui/dropdown-menu";
29import { Input } from "@/components/ui/input";
30import {
31  Table,
32  TableBody,
33  TableCell,
34  TableHead,
35  TableHeader,
36  TableRow,
37} from "@/components/ui/table";
38
39// ستون‌ها و داده‌ها مشابه مثال پایه، با اضافه شدن
40// انتخاب ردیف، مرتب‌سازی، فیلتر و عملیات ردیف
41
42export default function FullExample() {
43  const [sorting, setSorting] = React.useState<SortingState>([]);
44  const [columnFilters, setColumnFilters] =
45    React.useState<ColumnFiltersState>([]);
46  const [columnVisibility, setColumnVisibility] =
47    React.useState<VisibilityState>({});
48  const [rowSelection, setRowSelection] = React.useState({});
49
50  const table = useReactTable({
51    data,
52    columns,
53    onSortingChange: setSorting,
54    onColumnFiltersChange: setColumnFilters,
55    getCoreRowModel: getCoreRowModel(),
56    getPaginationRowModel: getPaginationRowModel(),
57    getSortedRowModel: getSortedRowModel(),
58    getFilteredRowModel: getFilteredRowModel(),
59    onColumnVisibilityChange: setColumnVisibility,
60    onRowSelectionChange: setRowSelection,
61    state: { sorting, columnFilters, columnVisibility, rowSelection },
62  });
63
64  return (
65    <div>
66      {/* فیلتر و نمایش ستون‌ها */}
67      <div className="flex items-center gap-4 py-4">
68        <Input
69          placeholder="فیلتر ایمیل..."
70          value={
71            (table.getColumn("email")?.getFilterValue() as string) ?? ""
72          }
73          onChange={(event) =>
74            table.getColumn("email")?.setFilterValue(event.target.value)
75          }
76          className="max-w-sm"
77        />
78        <DropdownMenu>
79          <DropdownMenuTrigger asChild>
80            <Button variant="outline" className="ms-auto">
81              ستون‌ها
82            </Button>
83          </DropdownMenuTrigger>
84          <DropdownMenuContent align="end">
85            {table
86              .getAllColumns()
87              .filter((column) => column.getCanHide())
88              .map((column) => (
89                <DropdownMenuCheckboxItem
90                  key={column.id}
91                  checked={column.getIsVisible()}
92                  onCheckedChange={(value) =>
93                    column.toggleVisibility(!!value)
94                  }
95                >
96                  {column.id}
97                </DropdownMenuCheckboxItem>
98              ))}
99          </DropdownMenuContent>
100        </DropdownMenu>
101      </div>
102
103      {/* جدول */}
104      <div className="overflow-hidden rounded-md border">
105        <Table>...</Table>
106      </div>
107
108      {/* صفحه‌بندی */}
109      <div className="flex items-center justify-between py-4">
110        <div className="text-sm text-muted-foreground">
111          {table.getFilteredSelectedRowModel().rows.length} از{" "}
112          {table.getFilteredRowModel().rows.length} ردیف انتخاب شده.
113        </div>
114        <div className="flex items-center gap-2">
115          <Button
116            variant="outline"
117            size="sm"
118            onClick={() => table.previousPage()}
119            disabled={!table.getCanPreviousPage()}
120          >
121            قبلی
122          </Button>
123          <Button
124            variant="outline"
125            size="sm"
126            onClick={() => table.nextPage()}
127            disabled={!table.getCanNextPage()}
128          >
129            بعدی
130          </Button>
131        </div>
132      </div>
133    </div>
134  );
135}

مرجع API (API Reference)

DataTable

پراپ‌های کامپوننت DataTable. این یک کامپوننت جنریک است که نوع داده و ستون‌ها را دریافت می‌کند.

پراپ (Prop)نوع (Type)پیش‌فرض (Default)توضیحات (Description)
columnsColumnDef<TData, TValue>[]-آرایه‌ای از تعریف ستون‌ها که ساختار جدول را مشخص می‌کند
dataTData[]-آرایه‌ای از داده‌ها که در جدول نمایش داده می‌شوند

ColumnDef

تعریف هر ستون جدول. از @tanstack/react-table استفاده می‌شود.

پراپ (Prop)نوع (Type)پیش‌فرض (Default)توضیحات (Description)
accessorKeystring-کلید دسترسی به داده در هر ردیف
headerstring | ((props) => ReactNode)-محتوای سرستون — می‌تواند یک رشته یا تابعی برای رندر سفارشی باشد
cell((props) => ReactNode)-تابع رندر سفارشی برای محتوای هر سلول
enableSortingbooleantrueفعال یا غیرفعال کردن مرتب‌سازی برای این ستون
enableHidingbooleantrueفعال یا غیرفعال کردن امکان مخفی کردن این ستون

Table Options

گزینه‌های useReactTable برای فعال‌سازی قابلیت‌های مختلف جدول.

پراپ (Prop)نوع (Type)پیش‌فرض (Default)توضیحات (Description)
getCoreRowModel() => RowModel<TData>-مدل اصلی ردیف‌ها — همیشه الزامی است
getPaginationRowModel() => RowModel<TData>-فعال‌سازی صفحه‌بندی خودکار
getSortedRowModel() => RowModel<TData>-فعال‌سازی مرتب‌سازی خودکار
getFilteredRowModel() => RowModel<TData>-فعال‌سازی فیلتر خودکار
statePartial<TableState>-وضعیت کنترل‌شده جدول شامل مرتب‌سازی، فیلتر، صفحه‌بندی و غیره

دسترسی‌پذیری (Accessibility)

ساختار معنایی (Semantic Structure)

از المان‌های معنایی HTML مانند <table>، <thead> و <tbody> استفاده می‌شود که به صفحه‌خوان‌ها کمک می‌کند ساختار جدول را درک کنند.

ناوبری کیبورد (Keyboard Navigation)

  • Tab — حرکت بین عناصر قابل فوکوس (چک‌باکس‌ها، دکمه‌های مرتب‌سازی، منوی عملیات)
  • Space / Enter — فعال‌سازی چک‌باکس انتخاب ردیف
  • Arrow Keys — ناوبری در منوی کشویی عملیات

برچسب‌های ARIA

چک‌باکس‌های انتخاب ردیف دارای aria-label هستند. دکمه‌های صفحه‌بندی با sr-only توضیحات مناسبی دارند. ردیف‌های انتخاب‌شده با data-state="selected" مشخص می‌شوند.

بهترین شیوه‌ها (Best Practices)

جداسازی ستون‌ها و جدول (Separate Columns and Table)

تعریف ستون‌ها را در فایل جداگانه‌ای مانند columns.tsx قرار دهید و کامپوننت DataTable را در فایل data-table.tsx بنویسید. صفحه سرور فقط داده‌ها را دریافت و به DataTable پاس می‌دهد.

استفاده از فرمت فارسی برای اعداد (Persian Number Formatting)

برای نمایش مبالغ و اعداد از Intl.NumberFormat("fa-IR") استفاده کنید تا اعداد با فرمت فارسی نمایش داده شوند.

تراز متن در RTL (Text Alignment in RTL)

برای تراز متن از کلاس‌های منطقی مانند text-start و text-end استفاده کنید. برای فاصله‌گذاری از ms- و me- به جای ml- و mr- استفاده کنید.

قابلیت استفاده مجدد (Reusability)

اگر جدول مشابهی را در چند صفحه استفاده می‌کنید، DataTable را به یک کامپوننت مشترک در components/ui/data-table.tsx انتقال دهید.

ایمیل‌ها به صورت LTR (LTR Emails)

محتوای ایمیل و آدرس‌های وب را با dir="ltr" نمایش دهید تا ترتیب کاراکترها صحیح باشد.

نحوه استفاده (Usage)

ساختار پروژه پیشنهادی برای استفاده از Data Table. فایل ستون‌ها و کامپوننت DataTable در فایل‌های جداگانه قرار می‌گیرند و صفحه سرور فقط داده‌ها را دریافت و پاس می‌دهد.

1// ساختار پروژه پیشنهادی
2// app
3// └── payments
4//     ├── columns.tsx
5//     ├── data-table.tsx
6//     └── page.tsx
7
8// columns.tsx — تعریف ستون‌ها (کامپوننت کلاینت)
9"use client";
10
11import { ColumnDef } from "@tanstack/react-table";
12
13export interface Payment {
14  id: string;
15  amount: number;
16  status: "موفق" | "در انتظار" | "ناموفق" | "در حال پردازش";
17  email: string;
18}
19
20export const columns: ColumnDef<Payment>[] = [
21  {
22    accessorKey: "status",
23    header: "وضعیت",
24  },
25  {
26    accessorKey: "email",
27    header: "ایمیل",
28  },
29  {
30    accessorKey: "amount",
31    header: () => <div className="text-end">مبلغ</div>,
32    cell: ({ row }) => {
33      const amount = parseFloat(row.getValue("amount"));
34      const formatted = new Intl.NumberFormat("fa-IR").format(amount);
35      return <div className="text-end font-medium">{formatted} تومان</div>;
36    },
37  },
38];
39
40// data-table.tsx — کامپوننت DataTable (کامپوننت کلاینت)
41"use client";
42
43import {
44  ColumnDef,
45  flexRender,
46  getCoreRowModel,
47  useReactTable,
48} from "@tanstack/react-table";
49import {
50  Table,
51  TableBody,
52  TableCell,
53  TableHead,
54  TableHeader,
55  TableRow,
56} from "@/components/ui/table";
57
58interface DataTableProps<TData, TValue> {
59  columns: ColumnDef<TData, TValue>[];
60  data: TData[];
61}
62
63export function DataTable<TData, TValue>({
64  columns,
65  data,
66}: DataTableProps<TData, TValue>) {
67  const table = useReactTable({
68    data,
69    columns,
70    getCoreRowModel: getCoreRowModel(),
71  });
72
73  return (
74    <div className="overflow-hidden rounded-md border">
75      <Table>
76        <TableHeader>
77          {table.getHeaderGroups().map((headerGroup) => (
78            <TableRow key={headerGroup.id}>
79              {headerGroup.headers.map((header) => (
80                <TableHead key={header.id}>
81                  {header.isPlaceholder
82                    ? null
83                    : flexRender(
84                        header.column.columnDef.header,
85                        header.getContext()
86                      )}
87                </TableHead>
88              ))}
89            </TableRow>
90          ))}
91        </TableHeader>
92        <TableBody>
93          {table.getRowModel().rows?.length ? (
94            table.getRowModel().rows.map((row) => (
95              <TableRow
96                key={row.id}
97                data-state={row.getIsSelected() && "selected"}
98              >
99                {row.getVisibleCells().map((cell) => (
100                  <TableCell key={cell.id}>
101                    {flexRender(
102                      cell.column.columnDef.cell,
103                      cell.getContext()
104                    )}
105                  </TableCell>
106                ))}
107              </TableRow>
108            ))
109          ) : (
110            <TableRow>
111              <TableCell
112                colSpan={columns.length}
113                className="h-24 text-center"
114              >
115                نتیجه‌ای یافت نشد.
116              </TableCell>
117            </TableRow>
118          )}
119        </TableBody>
120      </Table>
121    </div>
122  );
123}
124
125// page.tsx — صفحه سرور
126import { columns, Payment } from "./columns";
127import { DataTable } from "./data-table";
128
129async function getData(): Promise<Payment[]> {
130  // دریافت داده از API
131  return [
132    {
133      id: "INV-001",
134      amount: 316000,
135      status: "موفق",
136      email: "ken99@example.com",
137    },
138    // ...
139  ];
140}
141
142export default async function PaymentsPage() {
143  const data = await getData();
144
145  return (
146    <div className="container mx-auto py-10">
147      <DataTable columns={columns} data={data} />
148    </div>
149  );
150}

مثال‌های پیشرفته (Advanced Examples)

قالب‌بندی سلول سفارشی (Custom Cell Formatting)

می‌توانید با تابع cell در تعریف ستون، محتوای هر سلول را سفارشی کنید.

وضعیتمبلغ
موفق۳۱۶,۰۰۰ تومان
ناموفق۷۲۱,۰۰۰ تومان
مشاهده کد
1const columns: ColumnDef<Payment>[] = [
2  {
3    accessorKey: "amount",
4    header: () => <div className="text-end">مبلغ</div>,
5    cell: ({ row }) => {
6      const amount = parseFloat(row.getValue("amount"));
7      const formatted = new Intl.NumberFormat("fa-IR").format(amount);
8      return (
9        <div className="text-end font-medium">{formatted} تومان</div>
10      );
11    },
12  },
13  {
14    accessorKey: "status",
15    header: "وضعیت",
16    cell: ({ row }) => {
17      const status = row.getValue("status") as string;
18      const variantMap = {
19        "موفق": "default",
20        "در انتظار": "secondary",
21        "ناموفق": "destructive",
22        "در حال پردازش": "outline",
23      };
24      return <Badge variant={variantMap[status]}>{status}</Badge>;
25    },
26  },
27];

عملیات ردیف (Row Actions)

با استفاده از DropdownMenu می‌توانید منوی عملیات برای هر ردیف ایجاد کنید.

مثال عملی را در جدول کامل بالا مشاهده کنید — ستون آخر هر ردیف شامل منوی عملیات است.

مشاهده کد
1{
2  id: "actions",
3  enableHiding: false,
4  cell: ({ row }) => {
5    const payment = row.original;
6    return (
7      <DropdownMenu>
8        <DropdownMenuTrigger asChild>
9          <Button variant="ghost" className="h-8 w-8 p-0">
10            <span className="sr-only">باز کردن منو</span>
11            <MoreHorizontal className="h-4 w-4" />
12          </Button>
13        </DropdownMenuTrigger>
14        <DropdownMenuContent align="end">
15          <DropdownMenuLabel>عملیات</DropdownMenuLabel>
16          <DropdownMenuItem
17            onClick={() =>
18              navigator.clipboard.writeText(payment.id)
19            }
20          >
21            کپی شناسه پرداخت
22          </DropdownMenuItem>
23          <DropdownMenuSeparator />
24          <DropdownMenuItem>مشاهده مشتری</DropdownMenuItem>
25          <DropdownMenuItem>جزئیات پرداخت</DropdownMenuItem>
26        </DropdownMenuContent>
27      </DropdownMenu>
28    );
29  },
30}