جدول داده (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 | ۹۳۰٬۰۰۰ تومان |
مشاهده کد
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) |
|---|---|---|---|
columns | ColumnDef<TData, TValue>[] | - | آرایهای از تعریف ستونها که ساختار جدول را مشخص میکند |
data | TData[] | - | آرایهای از دادهها که در جدول نمایش داده میشوند |
ColumnDef
تعریف هر ستون جدول. از @tanstack/react-table استفاده میشود.
| پراپ (Prop) | نوع (Type) | پیشفرض (Default) | توضیحات (Description) |
|---|---|---|---|
accessorKey | string | - | کلید دسترسی به داده در هر ردیف |
header | string | ((props) => ReactNode) | - | محتوای سرستون — میتواند یک رشته یا تابعی برای رندر سفارشی باشد |
cell | ((props) => ReactNode) | - | تابع رندر سفارشی برای محتوای هر سلول |
enableSorting | boolean | true | فعال یا غیرفعال کردن مرتبسازی برای این ستون |
enableHiding | boolean | true | فعال یا غیرفعال کردن امکان مخفی کردن این ستون |
Table Options
گزینههای useReactTable برای فعالسازی قابلیتهای مختلف جدول.
| پراپ (Prop) | نوع (Type) | پیشفرض (Default) | توضیحات (Description) |
|---|---|---|---|
getCoreRowModel | () => RowModel<TData> | - | مدل اصلی ردیفها — همیشه الزامی است |
getPaginationRowModel | () => RowModel<TData> | - | فعالسازی صفحهبندی خودکار |
getSortedRowModel | () => RowModel<TData> | - | فعالسازی مرتبسازی خودکار |
getFilteredRowModel | () => RowModel<TData> | - | فعالسازی فیلتر خودکار |
state | Partial<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}