| Status | Amount | |||
|---|---|---|---|---|
success | ken99@yahoo.com | $316.00 | ||
success | Abe45@gmail.com | $242.00 | ||
processing | Monserrat44@gmail.com | $837.00 | ||
success | Silas22@gmail.com | $874.00 | ||
failed | carmella@hotmail.com | $721.00 |
<script setup lang="ts">
import type {
ColumnDef,
ColumnFiltersState,
ExpandedState,
SortingState,
VisibilityState,
} from '@tanstack/vue-table'
import {
FlexRender,
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { createReusableTemplate } from '@vueuse/core'
import { ArrowUpDown, ChevronDown, MoreHorizontal } from 'lucide-vue-next'
import { h, ref } from 'vue'
import { valueUpdater } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Input } from '@/components/ui/input'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
export interface Payment {
id: string
amount: number
status: 'pending' | 'processing' | 'success' | 'failed'
email: string
}
const data: Payment[] = [
{
id: 'm5gr84i9',
amount: 316,
status: 'success',
email: 'ken99@yahoo.com',
},
{
id: '3u1reuv4',
amount: 242,
status: 'success',
email: 'Abe45@gmail.com',
},
{
id: 'derv1ws0',
amount: 837,
status: 'processing',
email: 'Monserrat44@gmail.com',
},
{
id: '5kma53ae',
amount: 874,
status: 'success',
email: 'Silas22@gmail.com',
},
{
id: 'bhqecj4p',
amount: 721,
status: 'failed',
email: 'carmella@hotmail.com',
},
]
const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{
payment: {
id: string
}
onExpand: () => void
}>()
const columns: ColumnDef<Payment>[] = [
{
id: 'select',
header: ({ table }) => h(Checkbox, {
'modelValue': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
'onUpdate:modelValue': value => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
}),
cell: ({ row }) => h(Checkbox, {
'modelValue': row.getIsSelected(),
'onUpdate:modelValue': value => row.toggleSelected(!!value),
'ariaLabel': 'Select row',
}),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => h('div', { class: 'capitalize' }, row.getValue('status')),
},
{
accessorKey: 'email',
header: ({ column }) => {
return h(Button, {
variant: 'ghost',
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
}, () => ['Email', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })])
},
cell: ({ row }) => h('div', { class: 'lowercase' }, row.getValue('email')),
},
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
// Format the amount as a dollar amount
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
},
},
{
id: 'actions',
enableHiding: false,
cell: ({ row }) => {
const payment = row.original
return h(ReuseTemplate, {
payment,
onExpand: row.toggleExpanded,
})
},
},
]
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({})
const expanded = ref<ExpandedState>({})
const table = useVueTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value },
get expanded() { return expanded.value },
},
})
function copy(id: string) {
navigator.clipboard.writeText(id)
}
</script>
<template>
<DefineTemplate v-slot="{ payment }">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" class="h-8 w-8 p-0">
<span class="sr-only">Open menu</span>
<MoreHorizontal class="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem @click="copy(payment.id)">
Copy payment ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</DefineTemplate>
<div class="w-full">
<div class="flex items-center py-4">
<Input
class="max-w-sm"
placeholder="Filter emails..."
:model-value="table.getColumn('email')?.getFilterValue() as string"
@update:model-value=" table.getColumn('email')?.setFilterValue($event)"
/>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto">
Columns <ChevronDown class="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuCheckboxItem
v-for="column in table.getAllColumns().filter((column) => column.getCanHide())"
:key="column.id"
class="capitalize"
:model-value="column.getIsVisible()"
@update:model-value="(value) => {
column.toggleVisibility(!!value)
}"
>
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div class="rounded-md border">
<Table>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="table.getRowModel().rows?.length">
<template v-for="row in table.getRowModel().rows" :key="row.id">
<TableRow :data-state="row.getIsSelected() && 'selected'">
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
<TableRow v-if="row.getIsExpanded()">
<TableCell :colspan="row.getAllCells().length">
{{ JSON.stringify(row.original) }}
</TableCell>
</TableRow>
</template>
</template>
<TableRow v-else>
<TableCell
:colspan="columns.length"
class="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
{{ table.getFilteredSelectedRowModel().rows.length }} of
{{ table.getFilteredRowModel().rows.length }} row(s) selected.
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
>
Previous
</Button>
<Button
variant="outline"
size="sm"
:disabled="!table.getCanNextPage()"
@click="table.nextPage()"
>
Next
</Button>
</div>
</div>
</div>
</template>简介
🌐 Introduction
我创建的每个数据表格或数据网格都是独一无二的。它们的行为各不相同,具有特定的排序和筛选要求,并与不同的数据源配合使用。
🌐 Every data table or datagrid I've created has been unique. They all behave differently, have specific sorting and filtering requirements, and work with different data sources.
将所有这些变体组合成一个组件是没有意义的。如果我们这样做,就会失去 headless UI 提供的灵活性。
🌐 It doesn't make sense to combine all of these variations into a single component. If we do that, we'll lose the flexibility that headless UI provides.
因此,我认为提供如何构建自己的组件的指南会更有帮助,而不是数据表格组件。
🌐 So instead of a data-table component, I thought it would be more helpful to provide a guide on how to build your own.
我们将从基本的 <Table /> 组件开始,并从零构建一个复杂的数据表格。
🌐 We'll start with the basic <Table /> component and build a complex data table from scratch.
提示: 如果你发现自己在应用中的多个地方使用同一个表格,你可以将其提取为可重用的组件。
目录
🌐 Table of Contents
本指南将向你展示如何使用 TanStack Table 和 <Table /> 组件来构建你自己的自定义数据表格。我们将涵盖以下主题:
🌐 This guide will show you how to use TanStack Table and the <Table /> component to build your own custom data table. We'll cover the following topics:
安装
🌐 Installation
- 将
<Table />组件添加到你的项目中:
pnpm dlx shadcn-vue@latest add table
- 添加
tanstack/vue-table依赖:
pnpm add @tanstack/vue-table
先决条件
🌐 Prerequisites
我们将建立一个表格来显示最近的付款。我们的数据如下所示:
🌐 We are going to build a table to show recent payments. Here's what our data looks like:
interface Payment {
id: string
amount: number
status: 'pending' | 'processing' | 'success' | 'failed'
email: string
}
export const payments: Payment[] = [
{
id: '728ed52f',
amount: 100,
status: 'pending',
email: 'm@example.com',
},
{
id: '489e1d42',
amount: 125,
status: 'processing',
email: 'example@gmail.com',
},
// ...
]项目结构
🌐 Project Structure
首先创建以下文件结构:
🌐 Start by creating the following file structure:
components
└── payments
├── columns.ts
├── data-table.vue
├── data-table-dropdown.vue
└── app.vue我这里使用的是 Nuxt 示例,但这适用于任何其他 Vue 框架。
🌐 I'm using a Nuxt example here but this works for any other Vue framework.
columns.ts它将包含我们的列定义。data-table.vue它将包含我们的<DataTable />组件。data-table-dropdown.vue它将包含我们的<DropdownAction />组件。app.vue这是我们获取数据并渲染表格的地方。
基本表格
🌐 Basic Table
让我们从构建一个基本表格开始。
🌐 Let's start by building a basic table.
列定义
🌐 Column Definitions
首先,我们将在 columns.ts 文件中定义我们的列。
🌐 First, we'll define our columns in the columns.ts file.
import { h } from 'vue'
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
},
}
]注意: 列是你定义表格核心内容的地方。它们定义了要显示的数据,以及数据的格式、排序和筛选方式。
<DataTable /> 组件
🌐 <DataTable /> component
接下来,我们将创建一个 <DataTable /> 组件来渲染我们的表格。
🌐 Next, we'll create a <DataTable /> component to render our table.
<script setup lang="ts" generic="TData, TValue">
import type { ColumnDef } from '@tanstack/vue-table'
import {
FlexRender,
getCoreRowModel,
useVueTable,
} from '@tanstack/vue-table'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
}>()
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
})
</script>
<template>
<div class="border rounded-md">
<Table>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id">
<FlexRender
v-if="!header.isPlaceholder" :render="header.column.columnDef.header"
:props="header.getContext()"
/>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="table.getRowModel().rows?.length">
<TableRow
v-for="row in table.getRowModel().rows" :key="row.id"
:data-state="row.getIsSelected() ? 'selected' : undefined"
>
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
</template>
<template v-else>
<TableRow>
<TableCell :colspan="columns.length" class="h-24 text-center">
No results.
</TableCell>
</TableRow>
</template>
</TableBody>
</Table>
</div>
</template>提示:如果你发现自己在多个地方使用 <DataTable />,这是你可以通过将其提取到 components/ui/data-table.vue 来使其可重用的组件。
<DataTable :columns="columns" :data="data" />
渲染表格
🌐 Render the table
最后,我们将在我们的索引组件中渲染表格。
🌐 Finally, we'll render our table in our index component.
<script setup lang="ts">
import type { Payment } from './components/columns'
import { onMounted, ref } from 'vue'
import { columns } from './components/columns'
import DataTable from './components/DataTable.vue'
const data = ref<Payment[]>([])
async function getData(): Promise<Payment[]> {
// Fetch data from your API here.
return [
{
id: '728ed52f',
amount: 100,
status: 'pending',
email: 'm@example.com',
},
// ...
]
}
onMounted(async () => {
data.value = await getData()
})
</script>
<template>
<div class="container py-10 mx-auto">
<DataTable :columns="columns" :data="data" />
</div>
</template>单元格格式
🌐 Cell Formatting
让我们将金额单元格格式化以显示美元金额。我们还将把单元格对齐到右侧。
🌐 Let's format the amount cell to display the dollar amount. We'll also align the cell to the right.
更新列定义
🌐 Update columns definition
将 header 和 cell 的金额定义更新如下:
🌐 Update the header and cell definitions for amount as follows:
import { h } from 'vue'
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: 'amount',
header: () => h('div', { class: 'text-right' }, 'Amount'),
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue('amount'))
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount)
return h('div', { class: 'text-right font-medium' }, formatted)
},
}
]You can use the same approach to format other cells and headers.
行操作
🌐 Row Actions
让我们在表格中添加行操作。我们将使用一个 <Dropdown /> 组件来实现这一点。
🌐 Let's add row actions to our table. We'll use a <Dropdown /> component for this.
将以下内容添加到你的 DataTableDropDown.vue 组件中
🌐 Add the following into your DataTableDropDown.vue component
<script setup lang="ts">
import { MoreHorizontal } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
defineProps<{
payment: {
id: string
}
}>()
function copy(id: string) {
navigator.clipboard.writeText(id)
}
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" class="w-8 h-8 p-0">
<span class="sr-only">Open menu</span>
<MoreHorizontal class="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem @click="copy(payment.id)">
Copy payment ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>更新列定义
🌐 Update columns definition
更新我们的列定义以添加一个新的 actions 列。actions 单元格返回一个 <Dropdown /> 组件。
🌐 Update our columns definition to add a new actions column. The actions cell returns a <Dropdown /> component.
import { ColumnDef } from '@tanstack/vue-table'
import DropdownAction from '@/components/DataTableDropDown.vue'
export const columns: ColumnDef<Payment>[] = [
// ...
{
id: 'actions',
enableHiding: false,
cell: ({ row }) => {
const payment = row.original
return h('div', { class: 'relative' }, h(DropdownAction, {
payment,
}))
},
},
]你可以在 cell 函数中使用 row.original 访问行数据。使用此方法处理行的操作,例如使用 id 对你的 API 进行 DELETE 调用。
🌐 You can access the row data using row.original in the cell function. Use this to handle actions for your row eg. use the id to make a DELETE call to your API.
分页
🌐 Pagination
接下来,我们将在表格中添加分页。
🌐 Next, we'll add pagination to our table.
更新 <DataTable>
🌐 Update <DataTable>
import {
FlexRender,
getCoreRowModel,
getPaginationRowModel,
useVueTable,
} from "@tanstack/vue-table"
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})这将自动将你的行分页,每页 10 行。有关自定义页面大小和实现手动分页的更多信息,请参阅 分页文档。
🌐 This will automatically paginate your rows into pages of 10. See the pagination docs for more information on customizing page size and implementing manual pagination.
添加分页控件
🌐 Add pagination controls
我们可以使用 <Button /> 组件和 table.previousPage()、table.nextPage() API 方法为我们的表格添加分页控件。
🌐 We can add pagination controls to our table using the <Button /> component and the table.previousPage(), table.nextPage() API methods.
<script lang="ts" generic="TData, TValue">
import { Button } from '@/components/ui/button'
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
</script>
<template>
<div>
<div class="border rounded-md">
<Table>
{ // .... }
</Table>
</div>
<div class="flex items-center justify-end py-4 space-x-2">
<Button
variant="outline"
size="sm"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
>
Previous
</Button>
<Button
variant="outline"
size="sm"
:disabled="!table.getCanNextPage()"
@click="table.nextPage()"
>
Next
</Button>
</div>
</div>
</template>请参阅 可重用组件 部分,以获取更高级的分页组件。
🌐 See Reusable Components section for a more advanced pagination component.
排序
🌐 Sorting
让我们使电子邮件列可排序。
🌐 Let's make the email column sortable.
将以下内容添加到你的 utils 文件中
🌐 Add the following into your utils file
import type { Updater } from '@tanstack/vue-table'
import type { ClassValue } from 'clsx'
import type { Ref } from 'vue'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function valueUpdater<T extends Updater<any>>(updaterOrValue: T, ref: Ref) {
ref.value = typeof updaterOrValue === 'function'
? updaterOrValue(ref.value)
: updaterOrValue
}valueUpdater 函数用于更新 Vue ref 对象的值。它既处理直接赋值,也可以通过函数进行转换。如果 updaterOrValue 是一个函数,它会以当前的 ref 值为参数调用,并将结果赋给 ref.value。如果它不是函数,则直接赋给 ref.value。该工具增强了更新 ref 值的灵活性。虽然 Vue ref 可以直接管理响应式状态,valueUpdater 简化了值的更新,当新状态可以是直接值或基于当前状态生成的函数时,提高了代码的可读性和可维护性。
🌐 The valueUpdater function updates a Vue ref object's value. It handles both direct assignments and transformations using a function. If updaterOrValue is a function, it's called with the current ref value, and the result is assigned to ref.value. If it's not a function, it's directly assigned to ref.value. This utility enhances flexibility in updating ref values. While Vue ref can manage reactive state directly, valueUpdater simplifies value updates, improving code readability and maintainability when the new state can be a direct value or a function generating it based on the current one.
更新 <DataTable>
🌐 Update <DataTable>
<script setup lang="ts" generic="TData, TValue">
import type {
ColumnDef,
SortingState,
} from '@tanstack/vue-table'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { h, ref } from 'vue'
import {
FlexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { valueUpdater } from '@/lib/utils'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
}>()
const sorting = ref<SortingState>([])
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
state: {
get sorting() { return sorting.value },
},
})
</script>
<template>
<div>
<div class="border rounded-md">
<Table>{ ... }</Table>
</div>
</div>
</template>使标题单元格可排序
🌐 Make header cell sortable
我们现在可以更新 email 表头单元格以添加排序控件。
🌐 We can now update the email header cell to add sorting controls.
// components/payments/columns.ts
import type {
ColumnDef,
} from '@tanstack/vue-table'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: 'email',
header: ({ column }) => {
return h(Button, {
variant: 'ghost',
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc'),
}, () => ['Email', h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })])
},
cell: ({ row }) => h('div', { class: 'lowercase' }, row.getValue('email')),
},
]当用户切换标题单元格时,这将自动对表格进行排序(升序和降序)。
🌐 This will automatically sort the table (asc and desc) when the user toggles on the header cell.
过滤
🌐 Filtering
让我们添加一个搜索输入来过滤表格中的电子邮件。
🌐 Let's add a search input to filter emails in our table.
更新 <DataTable>
🌐 Update <DataTable>
<script setup lang="ts" generic="TData, TValue">
import type {
ColumnDef,
ColumnFiltersState,
SortingState,
} from '@tanstack/vue-table'
import { valueUpdater } from '@/lib/utils'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { h, ref } from 'vue'
import {
FlexRender,
getCoreRowModel,
getPaginationRowModel,
getFilteredRowModel,
getSortedRowModel,
useVueTable,
} from "@tanstack/vue-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
}>()
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
getFilteredRowModel: getFilteredRowModel(),
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
},
})
</script>
<template>
<div>
<div class="flex items-center py-4">
<Input class="max-w-sm" placeholder="Filter emails..."
:model-value="table.getColumn('email')?.getFilterValue() as string"
@update:model-value=" table.getColumn('email')?.setFilterValue($event)" />
</div>
<div class="border rounded-md">
<Table>{ ... }</Table>
</div>
</div>
</template>“email”列的筛选功能现已启用。你也可以为其他列添加筛选器。有关自定义筛选器的更多信息,请参阅筛选文档。
🌐 Filtering is now enabled for the email column. You can add filters to other columns as well. See the filtering docs for more information on customizing filters.
可见性
🌐 Visibility
使用 @tanstack/vue-table 可见性 API 添加列可见性相当简单。
🌐 Adding column visibility is fairly simple using @tanstack/vue-table visibility API.
更新 <DataTable>
🌐 Update <DataTable>
<script setup lang="ts" generic="TData, TValue">
import type {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
} from '@tanstack/vue-table'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { valueUpdater } from '@/lib/utils'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { h, ref } from 'vue'
import {
FlexRender,
getCoreRowModel,
getPaginationRowModel,
getFilteredRowModel,
getSortedRowModel,
useVueTable,
} from "@tanstack/vue-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
}>()
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
},
})
</script>
<template>
<div>
<div class="flex items-center py-4">
<Input class="max-w-sm" placeholder="Filter emails..."
:model-value="table.getColumn('email')?.getFilterValue() as string"
@update:model-value=" table.getColumn('email')?.setFilterValue($event)" />
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto">
Columns
<ChevronDown class="w-4 h-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuCheckboxItem
v-for="column in table.getAllColumns().filter((column) => column.getCanHide())" :key="column.id"
class="capitalize" :modelValue="column.getIsVisible()" @update:modelValue="(value) => {
column.toggleVisibility(!!value)
}">
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div class="border rounded-md">
<Table>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header"
:props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="table.getRowModel().rows?.length">
<TableRow v-for="row in table.getRowModel().rows" :key="row.id"
:data-state="row.getIsSelected() ? 'selected' : undefined">
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
</template>
<template v-else>
<TableRow>
<TableCell :colSpan="columns.length" class="h-24 text-center">
No results.
</TableCell>
</TableRow>
</template>
</TableBody>
</Table>
</div>
</div>
</template>这会添加一个下拉菜单,你可以使用它来切换列可见性。
🌐 This adds a dropdown menu that you can use to toggle column visibility.
行选择
🌐 Row Selection
接下来,我们将在表格中添加行选择。
🌐 Next, we're going to add row selection to our table.
更新列定义
🌐 Update column definitions
import type { ColumnDef } from '@tanstack/vue-table'
import { Checkbox } from '@/components/ui/checkbox'
export const columns: ColumnDef<Payment>[] = [
{
id: 'select',
header: ({ table }) => h(Checkbox, {
'modelValue': table.getIsAllPageRowsSelected(),
'onUpdate:modelValue': (value: boolean) => table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all',
}),
cell: ({ row }) => h(Checkbox, {
'modelValue': row.getIsSelected(),
'onUpdate:modelValue': (value: boolean) => row.toggleSelected(!!value),
'ariaLabel': 'Select row',
}),
enableSorting: false,
enableHiding: false,
},
]更新 <DataTable>
🌐 Update <DataTable>
<script setup lang="ts" generic="TData, TValue">
const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
}>()
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({})
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value },
},
})
</script>
<template>
<div>
<div class="border rounded-md">
<Table />
</div>
</div>
</template>这会在每一行中添加一个复选框,并在标题中添加一个复选框以选择所有行。
🌐 This adds a checkbox to each row and a checkbox in the header to select all rows.
显示选定的行
🌐 Show selected rows
你可以使用 table.getFilteredSelectedRowModel() API 显示所选行的数量。
🌐 You can show the number of selected rows using the table.getFilteredSelectedRowModel() API.
<template>
<div>
<div class="border rounded-md">
<Table />
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
{{ table.getFilteredSelectedRowModel().rows.length }} of
{{ table.getFilteredRowModel().rows.length }} row(s) selected.
</div>
<div class="space-x-2">
<PaginationButtons />
</div>
</div>
</div>
</template>扩展
🌐 Expanding
让行可以展开。
🌐 Let's make rows expandable.
更新 <DataTable>
🌐 Update <DataTable>
<script setup lang="ts" generic="TData, TValue">
import type {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
ExpandedState,
} from '@tanstack/vue-table'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { valueUpdater } from '@/lib/utils'
import { ArrowUpDown, ChevronDown } from 'lucide-vue-next'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { h, ref } from 'vue'
import {
FlexRender,
getCoreRowModel,
getPaginationRowModel,
getFilteredRowModel,
getSortedRowModel,
getExpandedRowModel,
useVueTable,
} from "@tanstack/vue-table"
const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
}>()
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({})
const expanded = ref<ExpandedState>({})
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getExpandedRowModel: getExpandedRowModel(),
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value },
get expanded() { return expanded.value },
},
})
</script>
<template>
<div>
<div class="flex items-center py-4">
<Input class="max-w-sm" placeholder="Filter emails..."
:model-value="table.getColumn('email')?.getFilterValue() as string"
@update:model-value=" table.getColumn('email')?.setFilterValue($event)" />
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto">
Columns
<ChevronDown class="w-4 h-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuCheckboxItem
v-for="column in table.getAllColumns().filter((column) => column.getCanHide())" :key="column.id"
class="capitalize" :modelValue="column.getIsVisible()" @update:modelValue="(value) => {
column.toggleVisibility(!!value)
}">
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div class="border rounded-md">
<Table>
<TableHeader>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header"
:props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="table.getRowModel().rows?.length">
<template v-for="row in table.getRowModel().rows" :key="row.id">
<TableRow :data-state="row.getIsSelected() ? 'selected' : undefined">
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
<TableRow v-if="row.getIsExpanded()">
<TableCell :colspan="row.getAllCells().length">
{{ JSON.stringify(row.original) }}
</TableCell>
</TableRow>
</template>
</template>
<template v-else>
<TableRow>
<TableCell :colSpan="columns.length" class="h-24 text-center">
No results.
</TableCell>
</TableRow>
</template>
</TableBody>
</Table>
</div>
</div>
</template>将展开操作添加到 DataTableDropDown.vue 组件
🌐 Add the expand action to the DataTableDropDown.vue component
<script setup lang="ts">
import { MoreHorizontal } from 'lucide-vue-next'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Button } from '@/components/ui/button'
defineProps<{
payment: {
id: string
}
}>()
defineEmits<{
(e: 'expand'): void
}>()
function copy(id: string) {
navigator.clipboard.writeText(id)
}
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" class="w-8 h-8 p-0">
<span class="sr-only">Open menu</span>
<MoreHorizontal class="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem @click="copy(payment.id)">
Copy payment ID
</DropdownMenuItem>
<DropdownMenuItem @click="$emit('expand')">
Expand
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>使行可展开
🌐 Make rows expandable
现在我们可以更新操作单元格以添加展开控件。
🌐 Now we can update the action cell to add the expand control.
<script setup lang="ts">
export const columns: ColumnDef<Payment>[] = [
{
id: 'actions',
enableHiding: false,
cell: ({ row }) => {
const payment = row.original
return h('div', { class: 'relative' }, h(DropdownAction, {
payment,
onExpand: row.toggleExpanded,
}))
},
},
]
</script>可重复使用的组件
🌐 Reusable Components
这里有一些组件,你可以用来构建你的数据表格。这来自任务演示。
🌐 Here are some components you can use to build your data tables. This is from the Tasks demo.
列标题
🌐 Column header
使任何列标题可排序和隐藏。
🌐 Make any column header sortable and hideable.
<script setup lang="ts">
import type { Column } from '@tanstack/vue-table'
import { type Task } from '../data/schema'
import ArrowDownIcon from '~icons/radix-icons/arrow-down'
import ArrowUpIcon from '~icons/radix-icons/arrow-up'
import CaretSortIcon from '~icons/radix-icons/caret-sort'
import EyeNoneIcon from '~icons/radix-icons/eye-none'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
interface DataTableColumnHeaderProps {
column: Column<Task, any>
title: string
}
defineProps<DataTableColumnHeaderProps>()
</script>
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<template>
<div v-if="column.getCanSort()" :class="cn('flex items-center space-x-2', $attrs.class ?? '')">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="ghost"
size="sm"
class="-ml-3 h-8 data-[state=open]:bg-accent"
>
<span>{{ title }}</span>
<ArrowDownIcon v-if="column.getIsSorted() === 'desc'" class="w-4 h-4 ml-2" />
<ArrowUpIcon v-else-if=" column.getIsSorted() === 'asc'" class="w-4 h-4 ml-2" />
<CaretSortIcon v-else class="w-4 h-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem @click="column.toggleSorting(false)">
<ArrowUpIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Asc
</DropdownMenuItem>
<DropdownMenuItem @click="column.toggleSorting(true)">
<ArrowDownIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem @click="column.toggleVisibility(false)">
<EyeNoneIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Hide
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div v-else :class="$attrs.class">
{{ title }}
</div>
</template>export const columns = [
{
accessorKey: "email",
header: ({ column }) => (
h(DataTableColumnHeader, {
column: column,
title: 'Email'
})
),
},
]分页
🌐 Pagination
向你的表格添加分页控件,包括页面大小和选择计数。
🌐 Add pagination controls to your table including page size and selection count.
<script setup lang="ts">
import { type Table } from '@tanstack/vue-table'
import { type Task } from '../data/schema'
import ChevronLeftIcon from '~icons/radix-icons/chevron-left'
import ChevronRightIcon from '~icons/radix-icons/chevron-right'
import DoubleArrowLeftIcon from '~icons/radix-icons/double-arrow-left'
import DoubleArrowRightIcon from '~icons/radix-icons/double-arrow-right'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
interface DataTablePaginationProps {
table: Table<Task>
}
defineProps<DataTablePaginationProps>()
</script>
<template>
<div class="flex items-center justify-between px-2">
<div class="flex-1 text-sm text-muted-foreground">
{{ table.getFilteredSelectedRowModel().rows.length }} of
{{ table.getFilteredRowModel().rows.length }} row(s) selected.
</div>
<div class="flex items-center space-x-6 lg:space-x-8">
<div class="flex items-center space-x-2">
<p class="text-sm font-medium">
Rows per page
</p>
<Select
:model-value="`${table.getState().pagination.pageSize}`"
@update:model-value="table.setPageSize"
>
<SelectTrigger class="h-8 w-[70px]">
<SelectValue :placeholder="`${table.getState().pagination.pageSize}`" />
</SelectTrigger>
<SelectContent side="top">
<SelectItem v-for="pageSize in [10, 20, 30, 40, 50]" :key="pageSize" :value="`${pageSize}`">
{{ pageSize }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex w-[100px] items-center justify-center text-sm font-medium">
Page {{ table.getState().pagination.pageIndex + 1 }} of
{{ table.getPageCount() }}
</div>
<div class="flex items-center space-x-2">
<Button
variant="outline"
class="hidden w-8 h-8 p-0 lg:flex"
:disabled="!table.getCanPreviousPage()"
@click="table.setPageIndex(0)"
>
<span class="sr-only">Go to first page</span>
<DoubleArrowLeftIcon class="w-4 h-4" />
</Button>
<Button
variant="outline"
class="w-8 h-8 p-0"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
>
<span class="sr-only">Go to previous page</span>
<ChevronLeftIcon class="w-4 h-4" />
</Button>
<Button
variant="outline"
class="w-8 h-8 p-0"
:disabled="!table.getCanNextPage()"
@click="table.nextPage()"
>
<span class="sr-only">Go to next page</span>
<ChevronRightIcon class="w-4 h-4" />
</Button>
<Button
variant="outline"
class="hidden w-8 h-8 p-0 lg:flex"
:disabled="!table.getCanNextPage()"
@click="table.setPageIndex(table.getPageCount() - 1)"
>
<span class="sr-only">Go to last page</span>
<DoubleArrowRightIcon class="w-4 h-4" />
</Button>
</div>
</div>
</div>
</template><DataTablePagination :table="table" />列切换
🌐 Column toggle
用于切换列可见性的组件。
🌐 A component to toggle column visibility.
<script setup lang="ts">
import type { Table } from '@tanstack/vue-table'
import { computed } from 'vue'
import { type Task } from '../data/schema'
import MixerHorizontalIcon from '~icons/radix-icons/mixer-horizontal'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
interface DataTableViewOptionsProps {
table: Table<Task>
}
const props = defineProps<DataTableViewOptionsProps>()
const columns = computed(() => props.table.getAllColumns()
.filter(
column =>
typeof column.accessorFn !== 'undefined' && column.getCanHide(),
))
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="outline"
size="sm"
class="hidden h-8 ml-auto lg:flex"
>
<MixerHorizontalIcon class="w-4 h-4 mr-2" />
View
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-[150px]">
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
v-for="column in columns"
:key="column.id"
class="capitalize"
:modelValue="column.getIsVisible()"
@update:modelValue="(value) => column.toggleVisibility(!!value)"
>
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</template>