جدول داده (DataTable)
جدول داده ترکیبی با مرتبسازی، صفحهبندی، انتخاب ردیف، و حالتهای بارگذاری و خالی
معرفی
DataTable یک کامپوننت ترکیبی است که جدول، مرتبسازی، صفحهبندی، انتخاب ردیف، و مدیریت حالتهای خالی و بارگذاری را در یک API ساده ارائه میدهد. این کامپوننت رایجترین الگوی نمایش داده در اپلیکیشنهای SaaS را پوشش میدهد.
چه زمانی استفاده کنیم:
- لیست دادهها با بیش از ۵ آیتم که نیاز به مرتبسازی یا صفحهبندی دارند
- جداول با قابلیت انتخاب برای عملیات دستهای
- هر صفحه لیست در اپلیکیشن (اینفلوئنسرها، کمپینها، گزارشها)
چه زمانی استفاده نکنیم:
- نمایش ساده ۳-۵ آیتم بدون تعامل — از
Tableمستقیم استفاده کنید - جداول با ساختار بسیار سفارشی — از کامپوننتهای
Tableبه صورت ترکیبی استفاده کنید - نمایش کارتی — از
Cardاستفاده کنید
| رضا کریمی | ۲۳۴٬۰۰۰ | ۲٫۸٪ |
| سارا احمدی | ۸۷٬۳۰۰ | ۳٫۱٪ |
| امیر رضایی | ۴۵٬۸۰۰ | ۳٫۵٪ |
| علی محمدی | ۱۲٬۵۰۰ | ۴٫۲٪ |
| مریم حسینی | ۵٬۲۰۰ | ۶٫۷٪ |
import { DataTable } from '@partodata/ui'زمین بازی
با تغییر تنظیمات زیر، پیشنمایش زنده را مشاهده کنید.
| احساس | |||
|---|---|---|---|
| پست الف | اینستاگرام | ۱۲٬۳۴۵ | |
| پست ب | توییتر | ۸٬۹۰۰ | |
| پست ج | تلگرام | ۵٬۴۰۰ | |
| پست د | اینستاگرام | ۴٬۲۰۰ | |
| پست ه | توییتر | ۳٬۱۰۰ |
استفاده پایه
import { DataTable, type DataTableColumn } from '@partodata/ui'
interface Influencer {
id: number
name: string
followers: number
engagementRate: number
}
const columns: DataTableColumn<Influencer>[] = [
{ id: 'name', header: 'نام', cell: (row) => row.name },
{
id: 'followers',
header: 'دنبالکنندهها',
cell: (row) => row.followers.toLocaleString('en-US'),
align: 'end',
},
{
id: 'engagementRate',
header: 'نرخ تعامل',
cell: (row) => `${row.engagementRate}٪`,
align: 'end',
},
]
const data: Influencer[] = [
{ id: 1, name: 'محمد رضایی', followers: 125000, engagementRate: 4.2 },
{ id: 2, name: 'سارا احمدی', followers: 89000, engagementRate: 6.8 },
{ id: 3, name: 'علی محمدی', followers: 350000, engagementRate: 2.1 },
]
<DataTable columns={columns} data={data} />با مرتبسازی
'use client'
import { useState } from 'react'
import { DataTable, type DataTableColumn, type SortDirection } from '@partodata/ui'
function SortableExample() {
const [sortColumn, setSortColumn] = useState<string | null>(null)
const [sortDirection, setSortDirection] = useState<SortDirection | null>(null)
return (
<DataTable
columns={columns}
data={data}
sort={{
column: sortColumn,
direction: sortDirection,
onSort: (col, dir) => {
setSortColumn(col)
setSortDirection(dir)
},
}}
/>
)
}با صفحهبندی
<DataTable
columns={columns}
data={pageData}
pagination={{
currentPage: 2,
totalPages: 10,
onPageChange: (page) => setCurrentPage(page),
}}
/>نوار صفحهبندی کامل
با افزودن pageSize/onPageSizeChange و totalRows، نوار پایین علاوه بر شمارهی صفحهها، انتخاب تعداد ردیف در هر صفحه، بازهی نتایج و شمار ردیفهای انتخابشده را هم نشان میدهد. هر سه اختیاریاند؛ بدون آنها همان نوار شمارهایِ وسطچین قبلی رندر میشود.
<DataTable
columns={columns}
data={pageData}
selection={{ selectedRows, onSelectionChange: setSelectedRows, getRowKey: (row) => row.id }}
pagination={{
currentPage,
totalPages,
onPageChange: setCurrentPage,
pageSize,
onPageSizeChange: setPageSize,
pageSizeOptions: [10, 20, 50, 100],
totalRows: 1240,
}}
/>totalRows از سرور میآید
این جدول سرور-صفحهبندی است و فقط یک صفحه ردیف در دست دارد؛ پس نمیتواند تعداد کل را حدس بزند. اگر totalRows را
ندهید، بازهی «۱–۲۰ از ۱٬۲۴۰» رندر نمیشود.
با انتخاب ردیف
'use client'
import { useState } from 'react'
function SelectableExample() {
const [selected, setSelected] = useState<Set<number>>(new Set())
return (
<DataTable
columns={columns}
data={data}
selection={{
selectedRows: selected,
onSelectionChange: setSelected,
getRowKey: (row) => row.id,
}}
/>
)
}حالت بارگذاری
<DataTable columns={columns} data={[]} isLoading loadingRows={5} />حالت خالی
import { Empty, EmptyIcon, EmptyTitle, EmptyDescription, Button } from '@partodata/ui'
import { SearchX } from 'lucide-react'
;<DataTable
columns={columns}
data={[]}
emptyState={
<Empty className="border-0">
<EmptyIcon>
<SearchX className="size-6" />
</EmptyIcon>
<EmptyTitle>اینفلوئنسری یافت نشد</EmptyTitle>
<EmptyDescription>فیلترهای خود را تغییر دهید یا اینفلوئنسر جدید اضافه کنید</EmptyDescription>
</Empty>
}
/>ترکیب کامل
'use client'
import { useState } from 'react'
import {
DataTable,
type DataTableColumn,
type SortDirection,
Avatar,
Badge,
SocialPlatformBadge,
} from '@partodata/ui'
function FullExample() {
const [page, setPage] = useState(1)
const [sortCol, setSortCol] = useState<string | null>(null)
const [sortDir, setSortDir] = useState<SortDirection | null>(null)
const [selected, setSelected] = useState<Set<number>>(new Set())
const columns: DataTableColumn<Influencer>[] = [
{
id: 'name',
header: 'نام',
sortable: true,
cell: (row) => (
<div className="flex items-center gap-2">
<Avatar src={row.avatar} fallback={row.name[0]} size="sm" />
<span className="font-medium">{row.name}</span>
</div>
),
},
{
id: 'platform',
header: 'پلتفرم',
cell: (row) => <SocialPlatformBadge platform={row.platform} size="sm" />,
},
{
id: 'followers',
header: 'دنبالکنندهها',
sortable: true,
align: 'end',
cell: (row) => row.followers.toLocaleString('en-US'),
},
{
id: 'status',
header: 'وضعیت',
cell: (row) => (
<Badge variant={row.active ? 'success' : 'default'} size="sm">
{row.active ? 'فعال' : 'غیرفعال'}
</Badge>
),
},
]
return (
<DataTable
columns={columns}
data={influencers}
size="sm"
striped
sort={{
column: sortCol,
direction: sortDir,
onSort: (col, dir) => {
setSortCol(col)
setSortDir(dir)
},
}}
selection={{
selectedRows: selected,
onSelectionChange: setSelected,
getRowKey: (row) => row.id,
}}
pagination={{
currentPage: page,
totalPages: 12,
onPageChange: setPage,
}}
resultCount="۱۴۲ اینفلوئنسر"
/>
)
}کلیک روی ردیف
با onRowClick، ردیفهای بدنه تعاملی میشوند: فوکوسپذیر با کیبورد (Tab) و قابل فعالسازی با Enter/Space، و کلیک/فعالسازی این callback را با (row, rowIndex) صدا میزند. برای «کلیک ردیف → صفحهٔ جزئیات» مناسب است. کنترلهای تعاملی داخل سلول (دکمه، منو) رفتار خودشان را حفظ میکنند.
<DataTable columns={columns} data={campaigns} onRowClick={(row) => router.push(`/campaigns/${row.id}`)} />فیلتر facetedِ ستونمحور
DataTableFacetedFilter یک چندانتخابیِ Popover + Command است برای enumهای محدودِ دامنه — پلتفرم، احساس، موضع، وضعیت، شدت، ردهی تعامل. کنارِ هر گزینه، شمارِ نتایج نمایش داده میشود.
| نام | پلتفرم | پستها |
|---|---|---|
| علی محمدی | اینستاگرام | 128 |
| سارا احمدی | تلگرام | 64 |
| رضا کریمی | اینستاگرام | 91 |
| مریم حسینی | ایکس | 37 |
| امیر رضایی | تلگرام | 12 |
'use client'
import { DataTable, DataTableFacetedFilter } from '@partodata/ui'
const [platforms, setPlatforms] = useState<string[]>([])
<DataTableFacetedFilter
title="پلتفرم"
options={[
{ value: 'instagram', label: 'اینستاگرام' },
{ value: 'telegram', label: 'تلگرام' },
{ value: 'x', label: 'ایکس' },
]}
selected={platforms}
onSelectedChange={setPlatforms}
facets={{ instagram: 1240, telegram: 87, x: 12 }}
/>شمارشها را سرور میدهد
facets را مصرفکننده پاس میدهد، چون DataTable دقیقاً همان dataای را رندر میکند که گرفته (سرور-صفحهبندی).
شمارشی که از صفحهی جاری حساب شود دروغ است. اگر facets ندهید، گزینهها بدون عدد رندر میشوند.
نوار ابزار کامل (جستوجو + فیلترها + خروجی)
پرتو کامپوننتِ جداگانهای بهنامِ DataTableToolbar ندارد و لازم هم ندارد — FilterBar دقیقاً همین کار را میکند. نوار ابزارِ متعارف از قطعاتِ موجود سرِ هم میشود:
| قطعه | نقش |
|---|---|
FilterBar | ظرفِ عمودی (نوار + جدول) |
FilterBarRow | ردیفِ کنترلها (flex-wrap) |
SearchInput + useDebounce | جستوجوی متنیِ debounceشده |
DataTableFacetedFilter | فیلترِ facet برای هر enum |
FilterBarClear | ریست — فقط وقتی فیلتری فعال است |
FilterBarActions | با ms-auto به انتهای خط میراند |
DataTableColumnVisibilityToggle / DataTableExportButton | نمایانیِ ستون و خروجی |
| نام | پلتفرم | احساس | پستها |
|---|---|---|---|
| علی محمدی | اینستاگرام | مثبت | 128 |
| سارا احمدی | تلگرام | خنثی | 64 |
| رضا کریمی | اینستاگرام | منفی | 91 |
| مریم حسینی | ایکس | مثبت | 37 |
| امیر رضایی | تلگرام | مثبت | 12 |
'use client'
import {
DataTable,
DataTableFacetedFilter,
DataTableColumnVisibilityToggle,
DataTableExportButton,
FilterBar,
FilterBarRow,
FilterBarActions,
FilterBarClear,
SearchInput,
useDebounce,
} from '@partodata/ui'
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
const [platforms, setPlatforms] = useState<string[]>([])
const [visible, setVisible] = useState({})
const hasFilters = !!query || platforms.length > 0
// در محصول واقعی، debouncedQuery و platforms را به سرور میفرستید
// و سرور صفحهی نتایج + facetها را برمیگرداند.
;<FilterBar>
<FilterBarRow>
<SearchInput
value={query}
onChange={(e) => setQuery(e.target.value)}
onClear={() => setQuery('')}
placeholder="جستوجوی نام"
wrapperClassName="w-44"
/>
<DataTableFacetedFilter
title="پلتفرم"
options={platformOptions}
selected={platforms}
onSelectedChange={setPlatforms}
facets={facetCounts}
/>
{hasFilters && (
<FilterBarClear
onClear={() => {
setQuery('')
setPlatforms([])
}}
/>
)}
<FilterBarActions>
<DataTableColumnVisibilityToggle columns={columns} visibility={{ visible, onVisibilityChange: setVisible }} />
<DataTableExportButton columns={columns} data={rows} filename="influencers.csv" />
</FilterBarActions>
</FilterBarRow>
<DataTable columns={columns} data={rows} columnVisibility={{ visible, onVisibilityChange: setVisible }} />
</FilterBar>چرا کامپوننت جدید نساختیم
شادسیان یک DataTableToolbar دارد، ولی آن فقط یک wrapperِ چیدمانی است و FilterBar ما همان نقش را با RTL و
i18nِ درست بازی میکند. افزودنِ کامپوننتِ همکار، فقط بدهیِ تکراری میساخت.
جدول ویژگیها
DataTable
DataTableColumn
| ویژگی | نوع | توضیح |
|---|---|---|
id | string | کلید یکتای ستون |
header | ReactNode | عنوان ستون |
cell | (row, index) => ReactNode | تابع رندر سلول |
sortable | boolean | قابلیت مرتبسازی |
align | "start" | "center" | "end" | تراز ستون |
pinned | "start" | "end" | سنجاقکردن ستون (چسبان هنگام اسکرول افقی) |
defaultVisible | boolean | با false تا فعالشدن مخفی است |
hideable | boolean | با false ستونِ ساختاری است و در منوی نمایانیِ ستون نمیآید (پیشفرض true) |
className | string | کلاس اضافی |
DataTablePagination
DataTableFacetedFilter
راهنمای استفاده
بکنید
- از
DataTableبرای لیست دادهها با بیش از ۵ آیتم که نیاز به مرتبسازی یا صفحهبندی دارند استفاده کنید - همیشهemptyStateسفارشی با پیام فارسی مناسب ارائه دهید - برای جداول با انتخاب ردیف،getRowKeyیکتا تعریف کنید
نکنید
- برای نمایش ساده ۳-۵ آیتم بدون تعامل از
DataTableاستفاده نکنید — ازTableاستفاده کنید - ستونهای غیرضروری اضافه نکنید — اطلاعات باید قابل اسکن باشند - برای نمایش کارتی از جدول استفاده نکنید — ازCardدر grid layout استفاده کنید
دسترسیپذیری
- ستونهای قابل مرتبسازی از
aria-sortاستفاده میکنند - چکباکسها دارای
aria-labelفارسی هستند - حالت بارگذاری از اسکلتون بصری استفاده میکند
- ساختار جدول از تگهای معنایی HTML (
table,thead,tbody,th,td) پیروی میکند
تعامل با کیبورد
Tab: حرکت بین عناصر تعاملی (چکباکسها، دکمههای مرتبسازی، صفحهبندی) -Space: انتخاب ردیف (چکباکس) -Enter: فعالسازی مرتبسازی ستون
v2 features (نمایانسازی ستون، expansion، pinning، export)
'use client'
import {
DataTable,
DataTableColumnVisibilityToggle,
DataTableExportButton,
} from '@partodata/ui'
const [visibility, setVisibility] = useState({})
const [expandedRows, setExpandedRows] = useState(new Set<number>())
<div className="flex items-center justify-end gap-2">
<DataTableColumnVisibilityToggle
columns={columns}
visibility={{ visible: visibility, onVisibilityChange: setVisibility }}
/>
<DataTableExportButton columns={columns} data={rows} filename="pages.csv" />
</div>
<DataTable
columns={[
{ id: 'name', header: 'نام', cell: (r) => r.name, pinned: 'start', width: 200 },
{ id: 'engagement', header: 'تعامل', cell: (r) => r.engagement },
{ id: 'notes', header: 'یادداشت', cell: (r) => r.notes, defaultVisible: false },
]}
data={rows}
columnVisibility={{ visible: visibility, onVisibilityChange: setVisibility }}
expansion={{
expandedRows,
onExpandedRowsChange: setExpandedRows,
renderExpandedRow: (r) => <div className="p-4 text-sm">{r.detailsHtml}</div>,
}}
/>columnVisibility(4.2) — show/hide columns via a checkbox dropdown.defaultVisible: falseon a column hides it until toggled on.expansion(4.5) — adds a chevron column; clicking expands a full-width detail row below.pinned: 'start' | 'end'(4.6) — sticky columns که در زمان horizontal scroll قابل دیدن میمانند. RTL-aware via CSS logical inset.width: number— عرض ثابت پیکسل برای layout header (همراه با pinning مفید است).<DataTableExportButton>(4.9) — dropdown با CSV download + TSV-clipboard.exportValueدر ستون برای cellهای React-node serialize را override میکند.
کامپوننتهای مرتبط
- Table — اگر دادههای شما ساده هستند و نیاز به مرتبسازی یا صفحهبندی ندارند، از Table استفاده کنید
- Card — اگر نمایش کارتی مناسبتر از جدول است، از Card در grid layout استفاده کنید
- DataTableCells — renderer های مخصوص cell جدول (sparkline، trend، status، …)