资讯详情

资讯详情

用 shadcn-svelte 与 TanStack Table v9 从零构建可复用数据表(Data Table)

用 shadcn-svelte 与 TanStack Table v9 从零构建可复用数据表Data Table【免费下载链接】shadcn-svelteshadcn/ui, but for Svelte. ✨项目地址: https://gitcode.com/GitHub_Trending/sh/shadcn-svelte数据表格是后台应用最复杂、最难以组件化的 UI 形态之一排序、过滤、分页、列显隐、行选择等功能几乎每个数据集都不相同任何一刀切的封装都会在真实需求面前失灵。因此 shadcn-svelte 没有试图提供一个全能的数据表组件而是给出了一套从基础Table /出发、按需叠加 TanStack Table 能力的构建指南。本篇以 docs/content/components/data-table.md 为核心脉络结合仓库内的真实源码ui/table 组件、data-table-demo 示例、dashboard-01 数据表 Block完整演示如何在 shadcn-svelte 项目中搭建一个具备分页、排序、过滤、列显隐、行选择与行操作的全功能数据表并讲清 TanStack Table v9 的 feature-based 架构与 Svelte 5 runes 响应式原理。读完你将能独立落地一套可复制、可抽取、可复用的DataTable /。思路为什么是指南而非组件Data tables are difficult to componentize because of the wide variety of features they support, and the uniqueness of every data set.数据表功能面太宽、每份数据集又独一无二所以正确的做法不是追求一个万能组件而是先掌握用 TanStack Table 驱动 shadcn-svelteTable /的方法再按需把通用部分抽成自己的组件。文档特别提示如果你发现同一张表在多个页面重复出现完全可以把它提取成可复用组件。安装构建数据表需要两部分基础样式组件 表格状态引擎。添加Table /组件npx shadcn-sveltelatest add table这会向项目注入 9 个基础子组件Root、Body、Caption、Cell、Footer、Head、Header、Row它们统一通过 ui/table/index.ts 以import * as Table from $lib/components/ui/table/index.js的方式导出。以 table.svelte 为例每个子组件都是纯展示层接收class、ref与其余原生属性通过cn()合并cn-table-*样式类不做任何状态管理——这正是它与 TanStack Table 分工的前提。添加依赖本指南使用TanStack Table v9npm install tanstack/svelte-table若同时使用 pnpm / bun将命令替换为对应包管理器即可。准备数据模型我们以最近支付记录为例数据类型如下type Payment { id: string; amount: number; status: pending | processing | success | failed; email: string; }; export const data: Payment[] [ { id: 728ed52f, amount: 100, status: pending, email: mexample.com, }, { id: 489e1d42, amount: 125, status: processing, email: examplegmail.com, }, // ... ];项目文件结构在路由目录下示例中为payments创建如下文件各司其职routes └── payments ├── columns.ts # 列定义 ├──>import { columnFilteringFeature, columnVisibilityFeature, createFilteredRowModel, createPaginatedRowModel, createSortedRowModel, filterFn_includesString, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFn_alphanumeric, sortFn_text, tableFeatures, } from tanstack/svelte-table; // v9 新特性声明这张表用到的能力——未注册的会被 tree-shaken 掉 export const features tableFeatures({ columnFilteringFeature, columnVisibilityFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, filteredRowModel: createFilteredRowModel(), paginatedRowModel: createPaginatedRowModel(), sortedRowModel: createSortedRowModel(), filterFns: { includesString: filterFn_includesString }, sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text }, }); // 作为第一个泛型参数传给 ColumnDef / Column / Table / Row // 让每个类型都知道当前可用哪些特性 API。 export type DataTableFeatures typeof features;注意核心 row model 始终内置无需注册可选特性的 row model 用create*RowModel()创建并注册到 features 上v9 中不再存在get*RowModel表选项。仓库中的>import { createColumnHelper } from tanstack/svelte-table; import type { DataTableFeatures } from ./data-table-features.js; export type Payment { id: string; amount: number; status: pending | processing | success | failed; email: string; }; // 数据列用 accessor无数据来源的列用 display const columnHelper createColumnHelperDataTableFeatures, Payment(); export const columns columnHelper.columns([ columnHelper.accessor(status, { header: Status }), columnHelper.accessor(email, { header: Email }), columnHelper.accessor(amount, { header: Amount }), ]);列定义决定了表格的形态展示什么数据、如何格式化、如何排序与过滤。DataTable /组件script langts genericsTData extends RowData import { type ColumnDef, type RowData, createTable, FlexRender, } from tanstack/svelte-table; import * as Table from $lib/components/ui/table/index.js; import { features, type DataTableFeatures } from ./data-table-features.js; type DataTablePropsTData extends RowData { columns: ColumnDefDataTableFeatures, TData[]; data: TData[]; }; let { data, columns }: DataTablePropsTData $props(); const table createTable({ features, get data() { return data; }, columns, }); /script div classrounded-md border Table.Root Table.Header {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} Table.Row {#each headerGroup.headers as header (header.id)} Table.Head colspan{header.colSpan} {#if !header.isPlaceholder} FlexRender {header} / {/if} /Table.Head {/each} /Table.Row {/each} /Table.Header Table.Body {#each table.getRowModel().rows as row (row.id)} Table.Row>export async function load() { // logic to fetch payments data here const payments await getPayments(); return { payments, }; }script langts import DataTable from ./data-table.svelte; import { columns } from ./columns.js; let { data } $props(); /script DataTable data{data.payments} {columns} /单元格格式化把 amount 列格式化为美元金额并右对齐需要自定义header与cellimport { createColumnHelper, renderSnippet } from tanstack/svelte-table; import { createRawSnippet } from svelte; import type { DataTableFeatures } from ./data-table-features.js; const columnHelper createColumnHelperDataTableFeatures, Payment(); export const columns columnHelper.columns([ columnHelper.accessor(amount, { header: () { const amountHeaderSnippet createRawSnippet(() ({ render: () div classtext-endAmount/div, })); return renderSnippet(amountHeaderSnippet); }, cell: ({ row }) { const formatter new Intl.NumberFormat(en-US, { style: currency, currency: USD, }); const amountCellSnippet createRawSnippet[{ amount: number }]( (getAmount) { const { amount } getAmount(); const formatted formatter.format(amount); return { render: () div classtext-end font-medium${formatted}/div, }; } ); return renderSnippet(amountCellSnippet, { amount: row.original.amount, }); }, }), ]);这里的两个核心辅助函数均直接来自tanstack/svelte-tablecreateRawSnippetSvelte 内置创建无需完整生命周期/状态的轻量 HTML Snippet适合纯展示元素带参数时通过(getter) ({ render: () ... })形式接收。renderSnippet把 Snippet 渲染进表格需要组件级能力时则改用renderComponent。同一思路可用于任意单元格与表头的格式化。参考>npx shadcn-sveltelatest add button dropdown-menu创建操作组件script langts import EllipsisIcon from lucide/svelte/icons/ellipsis; import { Button } from $lib/components/ui/button/index.js; import * as DropdownMenu from $lib/components/ui/dropdown-menu/index.js; let { id }: { id: string } $props(); /script DropdownMenu.Root DropdownMenu.Trigger {#snippet child({ props })} Button {...props} variantghost sizeicon classrelative size-8 p-0 span classsr-onlyOpen menu/span EllipsisIcon / /Button {/snippet} /DropdownMenu.Trigger DropdownMenu.Content DropdownMenu.Group DropdownMenu.LabelActions/DropdownMenu.Label DropdownMenu.Item onclick{() navigator.clipboard.writeText(id)} Copy payment ID /DropdownMenu.Item /DropdownMenu.Group DropdownMenu.Separator / DropdownMenu.ItemView customer/DropdownMenu.Item DropdownMenu.ItemView payment details/DropdownMenu.Item /DropdownMenu.Content /DropdownMenu.Root加入 actions 列该列不读取数据字段故用columnHelper.display定义并通过renderComponent把当前行数据传入组件import { createColumnHelper, renderComponent } from tanstack/svelte-table; import DataTableActions from ./data-table-actions.svelte; export const columns columnHelper.columns([ // ... columnHelper.display({ id: actions, cell: ({ row }) { // 需要什么就从 row.original 里取什么 return renderComponent(DataTableActions, { id: row.original.id }); }, }), ]);cell函数内可通过row.original访问整行数据例如用id发起 DELETE 请求实现删除操作。仓库示例>script langts genericsTData extends RowData import { Button } from $lib/components/ui/button/index.js; // ...其余脚本保持不变 /script div div classrounded-md border Table.Root !--- ... 表格实现 -- /Table.Root /div div classflex items-center justify-end space-x-2 py-4 Button variantoutline sizesm onclick{() table.previousPage()} disabled{!table.getCanPreviousPage()} Previous /Button Button variantoutline sizesm onclick{() table.nextPage()} disabled{!table.getCanNextPage()} Next /Button /div /div修改页大小与读取分页状态table.setPageSize()修改页大小table.atoms.pagination.get()是 rune-reactive 读取用$derived派生即可在翻页时自动更新script langts // rune-reactive 读取 —— 用户翻页时自动更新 const pagination $derived(table.atoms.pagination.get()); /script Button variantoutline sizesm onclick{() table.setPageSize(20)} Show 20 rows /Button div classtext-muted-foreground text-sm Page {pagination.pageIndex 1} of {table.getPageCount()} /div排序SortingrowSortingFeature、sorted row model 以及alphanumeric/text排序函数字符串列经默认auto设置解析都已在 features 中注册因此DataTable /无需改动只需添加 UI。定义可排序表头按钮script langts import type { ComponentProps } from svelte; import ArrowUpDownIcon from lucide/svelte/icons/arrow-up-down; import { Button } from $lib/components/ui/button/index.js; let { variant ghost, ...restProps }: ComponentPropstypeof Button $props(); /script Button {variant} {...restProps} Email ArrowUpDownIcon classms-2 size-4 / /Button让表头可排序把column.getToggleSortingHandler()绑定到按钮的点击事件import { createColumnHelper, renderComponent } from tanstack/svelte-table; import DataTableEmailButton from ./data-table-email-button.svelte; export const columns columnHelper.columns([ // ... columnHelper.accessor(email, { header: ({ column }) renderComponent(DataTableEmailButton, { onclick: column.getToggleSortingHandler(), }), }), ]);用户点击表头即自动在升序/降序间切换排序状态由表格内部持有无需任何额外接线。过滤FilteringcolumnFilteringFeature、filtered row model 与includesString过滤函数均已注册唯一要做的是渲染一个输入框script langts genericsTData extends RowData import { Input } from $lib/components/ui/input/index.js; // ...其余脚本保持不变 /script div div classflex items-center py-4 Input placeholderFilter emails... value{(table.getColumn(email)?.getFilterValue() as string) ?? } onchange{(e) { table.getColumn(email)?.setFilterValue(e.currentTarget.value); }} oninput{(e) { table.getColumn(email)?.setFilterValue(e.currentTarget.value); }} classmax-w-sm / /div div classrounded-md border Table.Root!-- ... --/Table.Root /div /divgetColumn(email)?.getFilterValue()负责读取当前过滤值受控输入的前提setFilterValue写入新值。重要限制字符串过滤引用只会解析你注册过的函数若其他列需要不同的内置过滤器务必先把它加进data-table-features.ts的filterFns。列可见性VisibilitycolumnVisibilityFeature已在 features 中注册只需叠加一个下拉菜单script langts genericsTData extends RowData import * as DropdownMenu from $lib/components/ui/dropdown-menu/index.js; // ...其余脚本保持不变 /script div div classflex items-center py-4 Input placeholderFilter emails... value{(table.getColumn(email)?.getFilterValue() as string) ?? } onchange{(e) table.getColumn(email)?.setFilterValue(e.currentTarget.value)} oninput{(e) table.getColumn(email)?.setFilterValue(e.currentTarget.value)} classmax-w-sm / DropdownMenu.Root DropdownMenu.Trigger {#snippet child({ props })} Button {...props} variantoutline classms-autoColumns/Button {/snippet} /DropdownMenu.Trigger DropdownMenu.Content alignend {#each table .getAllColumns() .filter((col) col.getCanHide()) as column (column.id)} DropdownMenu.CheckboxItem classcapitalize bind:checked{ () column.getIsVisible(), (v) column.toggleVisibility(!!v) } {column.id} /DropdownMenu.CheckboxItem {/each} /DropdownMenu.Content /DropdownMenu.Root /div div classrounded-md border Table.Root!--...--/Table.Root /div /div要点getAllColumns()取全部列.filter((col) col.getCanHide())排除不可隐藏列例如行选择列可设置enableHiding: falsebind:checked的双向绑定在 Svelte 5 中写作 getter/setter 对——读column.getIsVisible()写column.toggleVisibility(!!v)。行选择Row SelectionrowSelectionFeature已注册表格自动跟踪选中状态我们只需渲染复选框。定义复选框组件script langts import type { ComponentProps } from svelte; import { Checkbox } from $lib/components/ui/checkbox/index.js; let { checked false, onCheckedChange (v) (checked v), ...restProps }: ComponentPropstypeof Checkbox $props(); /script Checkbox bind:checked{() checked, onCheckedChange} {...restProps} /添加 select 列import { createColumnHelper, renderComponent } from tanstack/svelte-table; import DataTableCheckbox from ./data-table-checkbox.svelte; export const columns columnHelper.columns([ columnHelper.display({ id: select, header: ({ table }) renderComponent(DataTableCheckbox, { checked: table.getIsAllPageRowsSelected(), indeterminate: table.getIsSomePageRowsSelected() !table.getIsAllPageRowsSelected(), onCheckedChange: (value: boolean) table.toggleAllPageRowsSelected(!!value), aria-label: Select all, }), cell: ({ row }) renderComponent(DataTableCheckbox, { checked: row.getIsSelected(), onCheckedChange: (value: boolean) row.toggleSelected(!!value), aria-label: Select row, }), enableSorting: false, enableHiding: false, }), // ... ]);v9 细节table.getIsSomePageRowsSelected()只要有一个页面行被选中就返回true即使全选亦然所以表头复选框的indeterminate状态必须用!table.getIsAllPageRowsSelected()门控全选时才能正确清除半选态。外部接管选择状态默认表格内部自持状态若页面需要读取或驱动选择比如把选择同步到 URL可用createTableState只接管这一个切片其余保持内部管理// 把行选择放到表格外部便于应用其他部分读写 const [rowSelection, setRowSelection] createTableStateRowSelectionState( {} ); const table createTable({ features, get data() { return data; }, columns, state: { get rowSelection() { return rowSelection(); }, }, onRowSelectionChange: setRowSelection, });rowSelection()是 rune 背书的 getter可在组件任意位置读取setRowSelection接受值或 updater 函数。展示选中行数div classtext-muted-foreground flex-1 text-sm {table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s) selected. /div抽取可复用组件文档建议同一张表出现在多处时应把DataTable /抽取为components/ui/data-table.svelte对外只暴露columns与data两个 props。仓库中有两处可直接参考的集大成实现data-table-demo.svelte完整复刻本指南全部能力选择列、格式化单元格、可排序 email 头、过滤输入、列显隐下拉、分页控件、选中计数、行操作菜单是基础 → 全功能的零成本对照样板其子组件位于 examples/data-table/。dashboard-01 数据表 Block在同等模式上演示了进阶用法——getRowId自定义行 ID、enableRowSelection、autoResetPageIndex: false保持翻页位置以及基于 dnd-kit 的行拖拽排序DragDropProvideruseSortableRestrictToVerticalAxis。这意味着一旦你掌握了本文的列定义、features 注册与渲染循环往任意方向扩展拖拽、行内编辑、多级表头都只是加列、加 features、加 UI的增量工作。小结shadcn-svelte 的数据表哲学可以概括为三句话Table /只负责结构与样式状态与行为全部交给 TanStack Table v9 的tableFeatures()按需声明未注册即被 tree-shakenv9 内部持有状态、读取 rune-reactive配合 Svelte 5 runes 几乎不需要手写状态镜像createTableState只在需要外部接管时出场列定义是唯一的事实来源accessor/display两种列型配合renderSnippet/renderComponent即可在表格里渲染任意内容与交互。按基础表格 → 单元格格式化 → 行操作 → 分页 → 排序 → 过滤 → 列显隐 → 行选择 → 抽取复用这条主线走完你就拥有了自主构建任意复杂数据表的能力。【免费下载链接】shadcn-svelteshadcn/ui, but for Svelte. ✨项目地址: https://gitcode.com/GitHub_Trending/sh/shadcn-svelte创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →