جدول داده (DataTable)
جدول داده ترکیبی با مرتبسازی، صفحهبندی، انتخاب ردیف، و حالتهای بارگذاری و خالی
معرفی
DataTable یک کامپوننت ترکیبی است که جدول، مرتبسازی، صفحهبندی، انتخاب ردیف، و مدیریت حالتهای خالی و بارگذاری را در یک API ساده ارائه میدهد. این کامپوننت رایجترین الگوی نمایش داده در اپلیکیشنهای SaaS را پوشش میدهد.
جدول سطح مستقلِ روشنتر، حاشیهٔ ظریف و گوشههای بومی دارد؛ همان محفظهٔ اسکرول این قاب را میسازد. آن را داخل Card دیگری نگذارید. رنگِ ردیفهای عادی و سلولهای ثابت از سطح خود جدول میآید؛ انتخاب و اشارهگر حالتهای خود را حفظ میکنند.
متن عادی و عددها از تراکم جدول و وزن regular (400) پیروی میکنند؛ شناسهٔ اصلی میتواند medium (500) باشد. وضعیت، هویت، اعتماد و دیگر مقدارهای تصمیمگیری اندازهٔ بدنه را نگه میدارند، نه caption؛ وزن semibold، bold یا black برای وضعیت مجاز نیست. اطلاعات فرعی 12px و regular است؛ نشان بومی نقش پیشفرض 12px / 500 خود را حفظ میکند. فقط جمعبندی واقعی در footer میتواند medium باشد. سرستون default همان 13px / 500 و compact همان 12px / 500 است. برای ستون وضعیت از StatusCell استفاده کنید: نقطهٔ معنایی، برچسب متنی با وزن 400 و caption.
چه زمانی استفاده کنیم:
- لیست دادهها با بیش از 5 آیتم که نیاز به مرتبسازی یا صفحهبندی دارند
- جداول با قابلیت انتخاب برای عملیات دستهای
- جدول هر صفحهٔ فهرست در اپلیکیشن (اینفلوئنسرها، کمپینها، منشنها)، بهعنوان فرزند قالب
ListPage
چه زمانی استفاده نکنیم:
- نمایش ساده 3-5 آیتم بدون تعامل — از
Tableمستقیم استفاده کنید - جداول با ساختار بسیار سفارشی — از کامپوننتهای
Tableبه صورت ترکیبی استفاده کنید - نمایش کارتی — از
Cardاستفاده کنید
| رضا کریمی | 234,000 | 2.8٪ |
| سارا احمدی | 87,300 | 3.1٪ |
| امیر رضایی | 45,800 | 3.5٪ |
| علی محمدی | 12,500 | 4.2٪ |
| مریم حسینی | 5,200 | 6.7٪ |
import { DataTable } from '@partodata/ui'در صفحهٔ فهرست: فرزند ListPage
در یک صفحهٔ اپلیکیشن، جدولِ فهرست فرزند ListPage است و هیچیک از size،
pagination، emptyState و isLoading خودش را ندارد: تراکم را قالب تعیین میکند، صفحهبندی pagination قالب است و
بارگذاری، خطا و حالت خالی state={pageState(…)} قالباند که جای کل جدول مینشینند. جستوجو، فیلترها و اقدامهای بالای
جدول هم propهای همان قالباند (search، filters، secondaryActions، primaryAction). نمونههای صفحهبندی و ترکیب
کامل پایین این صفحه، جدولی را نشان میدهند که صفحه نیست (مثلاً فهرستی داخل DashboardChart یا یک Sheet).
زمین بازی
با تغییر تنظیمات زیر، پیشنمایش زنده را مشاهده کنید.
| احساس | |||
|---|---|---|---|
| پست الف | اینستاگرام | 12,345 | |
| پست ب | توییتر | 8,900 | |
| پست ج | تلگرام | 5,400 | |
| پست د | اینستاگرام | 4,200 | |
| پست ه | توییتر | 3,100 |
استفاده پایه
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 را
ندهید، بازهی «1–20 از 1,240» رندر نمیشود.
با انتخاب ردیف
در صفحهٔ فهرست: selection قالب ListPage
جدول فقط چکباکسها را میکشد. در صفحهٔ فهرست همان وضعیت انتخاب به selection قالب
ListPage هم میرود، با bulkActions: قالب شمار،
اقدامهای گروهی و «لغو انتخاب» را تا ردیفی انتخاب شده بهجای ردیف نوارابزار نشان میدهد و با بارگذاری دوبارهٔ فهرست
یا نمایش صفحهٔ دیگر انتخاب را پاک میکند. selection جدول همیشه getRowKey: (row) => row.id دارد: بی آن کلید هر
ردیف جایگاهش در صفحه است و اقدام گروهی جایگاهها را بهجای شناسهها میگیرد. نوار اقدام گروهی را خودتان نسازید و
اقدام گروهی را در secondaryActions نگذارید.
'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,
}}
/>
)
}انتخاب بازهای با Shift-click
بدون هیچ پیکربندی اضافه: یک ردیف را کلیک کنید (لنگر میشود)، بعد با Shift ردیف دیگری را کلیک کنید — همهی
ردیفهای بینشان همان حالتِ جدیدِ ردیفِ کلیکشده را میگیرند. لنگر تا کلیکِ سادهی بعدی ثابت میماند، پس چند
Shift-click پشتِ سرِ هم همه از یک نقطه بازه میسازند. ردیفهای غیرقابلانتخاب (isRowSelectable که false
برگرداند) از بازه کنار گذاشته میشوند — دقیقاً مثل رفتارشان در «انتخاب همه».
بازه در محدودهی همین صفحه است
جدول فقط رکوردهای data همین رندر را میشناسد؛ برای جدولی که سرور-صفحهبندی است، یک بازهی Shift-click از صفحهای
عبور نمیکند — دقیقاً چون خودِ لنگر و ردیفِ کلیکشده هم باید همین حالا روی صفحه دیده شده باشند تا اصلاً بشود رویشان
کلیک کرد.
تراکم
مثل Table: default (سرِ جدول 40، ردیف حداقل 44، متن 14) برای همهٔ جدولهای فهرست، و size="compact" (36 / 36 / 13)
برای جدولهای پرتراکم منشن. دلیل عددها در صفحهٔ Table آمده است. درون یک ListPage، size
نمیدهید: قالب تراکم همهٔ صفحههای فهرست را یکی میکند.
- محتوای ستونها را زیر سقف تراکم نگه دارید تا همهٔ ردیفها یک ارتفاع داشته باشند. در
defaultکنترلهایsm(30 پیکسل، پیشفرض Button) و آواتار تا 32 پیکسل (Avatar size="md") جا میشوند، و درcompactکنترلهایxs(26) و نشانها و آواتار تا 24 پیکسل (Avatar size="sm"). - تراکم مال خود جدول است.
DataTableفشرده داخلPageContainerفشرده میماند، و جدولی که در ردیف بازشدهٔ (expansion) آن قرار میگیرد تراکم خودش را دارد. - با
virtualize، مقدارrowHeightرا برابر ارتفاع ردیف بدهید: 44 درdefaultو 36 درcompact(ردیفهای 3٫x حدود 31 پیکسل بودند). فاصلهگذاری و اسکرول از همین عدد حساب میشود. - اسکلت بارگذاری بیرون از جدول (
TableSkeleton) همsize="compact"میگیرد، تا جایگزین شدنش با جدول پرش نداشته باشد.
حالتهای بارگذاری، خطا و خالی
جدول حالتهایش را خودش نمیکشد: قالب صفحه آنها را جای کل جدول میگذارد. در صفحهٔ فهرست با state قالب
ListPage، و برای جدولی که جدا از صفحه بارگذاری میشود (بخشی از صفحهٔ جزئیات) با state همان DetailSection یا یک
PageState:
import { DataTable, useAsync } from '@partodata/ui'
import { ListPage, pageState } from '@partodata/ui/templates'
const { data, isLoading, error, run } = useAsync<Paged<Influencer>>()
;<ListPage
title="اینفلوئنسرها"
state={pageState({
data: data?.items,
isLoading,
error,
onRetry: load,
emptyCopy: { title: 'هنوز اینفلوئنسری ثبت نشده است' },
})}
>
<DataTable columns={columns} data={data?.items ?? []} />
</ListPage>- بارگذاری اول: اسکلتی به شکل جدول جای جدول؛ هنگام بارگذاری صفحهٔ بعد، جستوجو یا فیلتر ردیفها کمرنگ سر جایشان میمانند.
- خطا:
ErrorStateبا «تلاش مجدد» (کهonRetryرا صدا میزند) جای جدول. - خالی: وقتی جستوجو یا فیلتری فعال است «نتیجهای یافت نشد» با «پاک کردن فیلترها»؛ وگرنه عنوان
emptyCopyکه نام آنچه نیست را میبرد. سرستونهای جدول نمیمانند.
propهای isLoading، loadingRows و emptyState خود جدول فقط برای سازگاری با کدهای پیشین ماندهاند؛ در صفحه آنها را
به کار نبرید.
ترکیب کامل
'use client'
import { useState } from 'react'
import { DataTable, type DataTableColumn, type SortDirection, Avatar, Badge, formatNumber } from '@partodata/ui'
import { PlatformMark } from '@partodata/ui/social'
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) => <PlatformMark source={row.platform} variant="badge" showLabel />,
},
{
id: 'followers',
header: 'دنبالکنندهها',
sortable: true,
align: 'end',
cell: (row) => formatNumber(row.followers),
},
{
id: 'status',
header: 'وضعیت',
cell: (row) => <Badge variant={row.active ? 'success' : 'default'}>{row.active ? 'فعال' : 'غیرفعال'}</Badge>,
},
]
return (
<DataTable
columns={columns}
data={influencers}
size="compact"
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="142 اینفلوئنسر"
/>
)
}کلیک روی ردیف
با 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'
type Platform = 'instagram' | 'telegram' | 'x'
const PLATFORMS: { value: Platform; label: string }[] = [
{ value: 'instagram', label: 'اینستاگرام' },
{ value: 'telegram', label: 'تلگرام' },
{ value: 'x', label: 'ایکس' },
]
const [platforms, setPlatforms] = useState<Platform[]>([])
<DataTableFacetedFilter
title="پلتفرم"
options={PLATFORMS}
selected={platforms}
onSelectedChange={setPlatforms}
facets={{ instagram: 1240, telegram: 87, x: 12 }}
/>فیلتر با نوع مقدار گزینههایش تایپ میشود: وقتی گزینهها و وضعیت با union برنامه تایپ شوند
(useState<Platform[]>)، selected و onSelectedChange هم Platform[] هستند و انتخاب بیتبدیل نوع (بدون
as Platform[]) به APIای میرسد که با همان union تایپ شده است. گزینهای بیرون از union خطای نوع است. با مقدارهای
string مثل قبل کار میکند. در صفحهٔ فهرست، هر تغییر فیلتر صفحه را هم به 1 برمیگرداند (filterBy در
ListPage).
شمارشها را سرور میدهد
facets را مصرفکننده پاس میدهد، چون DataTable دقیقاً همان dataای را رندر میکند که گرفته (سرور-صفحهبندی).
شمارشی که از صفحهی جاری حساب شود دروغ است. اگر facets ندهید، گزینهها بدون عدد رندر میشوند.
جدول در صفحهٔ فهرست (جستوجو + فیلترها + خروجی)
در صفحهٔ فهرست، جدول فرزند قالب ListPage است و جستوجو، فیلترها و اقدامهای بالای آن
propهای همان قالباند: قالب نوارابزار را میسازد، همهٔ کنترلها را همقد میکند، به هر کدام عرض محتوایش را میدهد،
جستوجو را با عرض نامدار میگذارد و فاصلهٔ 16 پیکسلی تا جدول را خودش تعیین میکند. پس این ردیف را با PageToolbar،
FilterBar یا div و flex نسازید و به کنترلهای داخلش size یا کلاس عرض ندهید. جدول خودش size، pagination،
emptyState و isLoading ندارد.
Prop در ListPage | چه چیزی در آن میآید |
|---|---|
search | یک SearchInput بدون عرض؛ درخواست مقدارش را با useDebounce(q, 300) میگیرد |
filters | یک DataTableFacetedFilter برای هر ستون شمارشی، و DateRangePicker با placeholder برای بازهٔ زمانی |
filtered | آیا جستوجو یا فیلتری فعال است؛ هر تغییر جستوجو یا فیلتر صفحه را به 1 برمیگرداند |
onClearFilters | همه را پاک میکند و به صفحهٔ 1 برمیگردد (دکمهٔ «پاک کردن فیلترها» را قالب میگذارد) |
secondaryActions | DataTableExportButton بدون variant (آنجا default رندر میشود) |
toolbarEnd | DataTableColumnVisibilityToggle، فقط وقتی محصول انتخاب ستونها را میخواهد |
primaryAction | اقدام اصلی صفحه اگر دارد: یک Button بدون variant که کاری میکند (یا پیوندی با asChild) |
state، pagination | حالتها با pageState(…) و هر پنج فیلد صفحهبندی |
| فرزند | خود DataTable |
نمونهٔ کامل و کامپایلشده در ListPage آمده است.
چرا کامپوننتی بهنام DataTableToolbar نداریم
شادسیان یک DataTableToolbar دارد که فقط چیدمان ردیف بالای جدول است. در پرتو این ردیف را قالب ListPage از propهایش
میسازد (روی جزء سطح پایین PageToolbar، که مستقیم فقط درون یک CustomPage به کار میرود). FilterBar جزء سطح
پایینتری است برای ردیف فیلتری که نوارابزار صفحه نیست، مثل ردیف فیلتر داخل یک پنل کناری.
جدول ویژگیها
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برای لیست دادهها با بیش از 5 آیتم که نیاز به مرتبسازی یا صفحهبندی دارند استفاده کنید - در صفحهٔ فهرست جدول را فرزندListPageکنید و حالت خالی را باemptyCopy: {title}درpageStateقالب بدهید، نه در خود جدول - عملیات هر ردیف را آخرین ستون کنید: یکDropdownMenuپشتButton variant="ghost"باIcons.moreHorizontalوaria-labelبه نام ردیف - برای جداول با انتخاب ردیف،getRowKeyیکتا تعریف کنید
نکنید
- برای نمایش ساده 3-5 آیتم بدون تعامل از
DataTableاستفاده نکنید — ازTableاستفاده کنید - ستونهای غیرضروری اضافه نکنید — اطلاعات باید قابل اسکن باشند - برای نمایش کارتی از جدول استفاده نکنید — ازCardدر grid layout استفاده کنید
دسترسیپذیری
- ستونهای قابل مرتبسازی از
aria-sortاستفاده میکنند - همه نامهای قابلدسترس از prop به نام
localeپیروی میکنند (faپیشفرض،ar،en) — چکباکس انتخاب، دکمه باز/بستن ردیف و دستگیره تغییر اندازه ستون - نشان اولویت مرتبسازی چندستونه معنای خود را با یک متن
sr-onlyاعلام میکند (روی یک<span>بدون role،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 میکند.
کامپوننتهای مرتبط
- ListPage — قالب صفحهٔ فهرست: جستوجو، فیلترها، اقدامها، حالتها و صفحهبندی جدول؛ جدول فرزند آن است
- Table — اگر دادههای شما ساده هستند و نیاز به مرتبسازی یا صفحهبندی ندارند، از Table استفاده کنید
- Card — اگر نمایش کارتی مناسبتر از جدول است، از Card در grid layout استفاده کنید
- DataTableCells — renderer های مخصوص cell جدول (sparkline، trend، status، …)