数据表格
使用 TanStack Table 构建的强大表格和数据网格。
import type { DropdownMenuTriggerProps } from "@kobalte/core/dropdown-menu";
import type { SelectTriggerProps } from "@kobalte/core/select";
import { Badge } from "@repo/tailwindcss/ui/badge";
import { Button } from "@repo/tailwindcss/ui/button";
import { Checkbox, CheckboxControl } from "@repo/tailwindcss/ui/checkbox";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuGroupLabel,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@repo/tailwindcss/ui/dropdown-menu";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@repo/tailwindcss/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@repo/tailwindcss/ui/table";
import { TextField, TextFieldRoot } from "@repo/tailwindcss/ui/textfield";
import type {
Column,
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
} from "@tanstack/solid-table";
import {
createSolidTable,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
} from "@tanstack/solid-table";
import type { VoidProps } from "solid-js";
import {
For,
Match,
Show,
Switch,
createMemo,
createSignal,
splitProps,
} from "solid-js";
import { dataTable } from "./data-table-data";
export type Task = {
id: string;
code: string;
title: string;
status: "todo" | "in-progress" | "done" | "cancelled";
label: "bug" | "feature" | "enhancement" | "documentation";
};
const filteredStatusList = () =>
["todo", "in-progress", "done", "cancelled"].map((e) => ({
title: e,
value: e,
}));
const TableColumnHeader = <TData, TValue>(
props: VoidProps<{ column: Column<TData, TValue>; title: string }>,
) => {
const [local] = splitProps(props, ["column", "title"]);
return (
<Show
when={local.column.getCanSort() && local.column.getCanHide()}
fallback={<span class="text-sm font-medium">{local.title}</span>}
>
<div class="flex items-center space-x-2">
<DropdownMenu>
<DropdownMenuTrigger
as={(props: DropdownMenuTriggerProps) => (
<Button
aria-label={
local.column.getIsSorted() === "desc"
? "Sorted descending. Click to sort ascending."
: local.column.getIsSorted() === "asc"
? "Sorted ascending. Click to sort descending."
: "Not sorted. Click to sort ascending."
}
variant="ghost"
class="-ml-4 h-8 data-[expanded]:bg-accent"
{...props}
>
<span>{local.title}</span>
<div class="ml-1">
<Switch
fallback={
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-3.5"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m8 9l4-4l4 4m0 6l-4 4l-4-4"
/>
</svg>
}
>
<Match when={local.column.getIsSorted() === "asc"}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-3.5"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m4-10l-4-4M8 9l4-4"
/>
</svg>
</Match>
<Match when={local.column.getIsSorted() === "desc"}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-3.5"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m4-4l-4 4m-4-4l4 4"
/>
</svg>
</Match>
</Switch>
</div>
</Button>
)}
/>
<DropdownMenuContent>
<Show when={local.column.getCanSort()}>
<DropdownMenuItem
aria-label="Sort ascending"
onClick={() => local.column.toggleSorting(false, true)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 size-4 text-muted-foreground/70"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m4-10l-4-4M8 9l4-4"
/>
</svg>
Asc
</DropdownMenuItem>
<DropdownMenuItem
aria-label="Sort descending"
onClick={() => local.column.toggleSorting(true, true)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 size-4 text-muted-foreground/70"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m4-4l-4 4m-4-4l4 4"
/>
</svg>
Desc
</DropdownMenuItem>
</Show>
<Show when={local.column.getCanSort() && local.column.getCanHide()}>
<DropdownMenuSeparator />
</Show>
<Show when={local.column.getCanHide()}>
<DropdownMenuItem
aria-label="Hide column"
onClick={() => local.column.toggleVisibility(false)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 size-4 text-muted-foreground/70"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 9c-2.4 2.667-5.4 4-9 4c-3.6 0-6.6-1.333-9-4m0 6l2.5-3.8M21 14.976L18.508 11.2M9 17l.5-4m5.5 4l-.5-4"
/>
</svg>
Hide
</DropdownMenuItem>
</Show>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Show>
);
};
const columns: ColumnDef<Task>[] = [
{
id: "selects",
header: (props) => (
<Checkbox
indeterminate={props.table.getIsSomePageRowsSelected()}
checked={props.table.getIsAllPageRowsSelected()}
onChange={(value) => props.table.toggleAllPageRowsSelected(value)}
aria-label="Select all"
class="translate-y-[2px]"
>
<CheckboxControl />
</Checkbox>
),
cell: (props) => (
<Checkbox
checked={props.row.getIsSelected()}
onChange={(value) => props.row.toggleSelected(value)}
aria-label="Select row"
class="translate-y-[2px]"
>
<CheckboxControl />
</Checkbox>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "code",
header: (props) => <TableColumnHeader column={props.column} title="Task" />,
cell: (props) => <div class="w-[70px]">{props.row.getValue("code")}</div>,
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "title",
header: (props) => (
<TableColumnHeader column={props.column} title="Title" />
),
cell: (props) => (
<div class="flex space-x-2">
<Badge variant="outline">{props.row.original.label}</Badge>
<span class="max-w-[250px] truncate font-medium">
{props.row.getValue("title")}
</span>
</div>
),
},
{
accessorKey: "status",
header: (props) => (
<TableColumnHeader column={props.column} title="Status" />
),
cell: (props) => (
<div class="flex w-[100px] items-center">
<Switch>
<Match when={props.row.original.status === "cancelled"}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
class="mr-2 size-4 text-muted-foreground"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 20.777a8.942 8.942 0 0 1-2.48-.969M14 3.223a9.003 9.003 0 0 1 0 17.554m-9.421-3.684a8.961 8.961 0 0 1-1.227-2.592M3.124 10.5c.16-.95.468-1.85.9-2.675l.169-.305m2.714-2.941A8.954 8.954 0 0 1 10 3.223M14 14l-4-4m0 4l4-4"
/>
</svg>
</Match>
<Match when={props.row.original.status === "done"}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
class="mr-2 size-4 text-muted-foreground"
aria-hidden="true"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M10 20.777a8.942 8.942 0 0 1-2.48-.969M14 3.223a9.003 9.003 0 0 1 0 17.554m-9.421-3.684a8.961 8.961 0 0 1-1.227-2.592M3.124 10.5c.16-.95.468-1.85.9-2.675l.169-.305m2.714-2.941A8.954 8.954 0 0 1 10 3.223" />
<path d="m9 12l2 2l4-4" />
</g>
</svg>
</Match>
<Match when={props.row.original.status === "in-progress"}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
class="mr-2 size-4 text-muted-foreground"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 20.777a8.942 8.942 0 0 1-2.48-.969M14 3.223a9.003 9.003 0 0 1 0 17.554m-9.421-3.684a8.961 8.961 0 0 1-1.227-2.592M3.124 10.5c.16-.95.468-1.85.9-2.675l.169-.305m2.714-2.941A8.954 8.954 0 0 1 10 3.223M12 9l-2 3h4l-2 3"
/>
</svg>
</Match>
<Match when={props.row.original.status === "todo"}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
class="mr-2 size-4 text-muted-foreground"
aria-hidden="true"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M12 16v.01M12 13a2 2 0 0 0 .914-3.782a1.98 1.98 0 0 0-2.414.483M10 20.777a8.942 8.942 0 0 1-2.48-.969" />
<path d="M14 3.223a9.003 9.003 0 0 1 0 17.554m-9.421-3.684a8.961 8.961 0 0 1-1.227-2.592M3.124 10.5c.16-.95.468-1.85.9-2.675l.169-.305m2.714-2.941A8.954 8.954 0 0 1 10 3.223" />
</g>
</svg>
</Match>
</Switch>
<span class="capitalize">{props.row.original.status}</span>
</div>
),
filterFn: (row, id, value) => {
return Array.isArray(value) && value.includes(row.getValue(id));
},
},
{
id: "actions",
cell: () => (
<DropdownMenu placement="bottom-end">
<DropdownMenuTrigger class="flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0"
/>
<title>Action</title>
</svg>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];
const DataTableDemo = () => {
const data = createMemo(() => dataTable);
const [rowSelection, setRowSelection] = createSignal({});
const [columnVisibility, setColumnVisibility] = createSignal<VisibilityState>(
{},
);
const [columnFilters, setColumnFilters] = createSignal<ColumnFiltersState>(
[],
);
const [sorting, setSorting] = createSignal<SortingState>([]);
const table = createSolidTable({
get data() {
return data();
},
columns,
state: {
get sorting() {
return sorting();
},
get columnVisibility() {
return columnVisibility();
},
get rowSelection() {
return rowSelection();
},
get columnFilters() {
return columnFilters();
},
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return (
<div class="w-full space-y-2.5">
<div class="flex items-center justify-between gap-2">
<TextFieldRoot>
<TextField
type="text"
placeholder="Filter titles..."
class="h-8"
value={(table.getColumn("title")?.getFilterValue() as string) ?? ""}
onInput={(e) =>
table.getColumn("title")?.setFilterValue(e.currentTarget.value)
}
/>
</TextFieldRoot>
<div class="flex items-center gap-2">
<Select
onChange={(e) => {
table
.getColumn("status")
?.setFilterValue(e.length ? e.map((v) => v.value) : undefined);
}}
placement="bottom-end"
sameWidth={false}
options={filteredStatusList()}
optionValue="value"
optionTextValue="title"
multiple
itemComponent={(props) => (
<SelectItem item={props.item} class="capitalize">
{props.item.rawValue.title}
</SelectItem>
)}
>
<SelectTrigger
as={(props: SelectTriggerProps) => (
<Button
{...props}
aria-label="Filter status"
variant="outline"
class="relative flex h-8 w-full gap-2 [&>svg]:hidden"
>
<div class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 size-4"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m12 20l-3 1v-8.5L4.52 7.572A2 2 0 0 1 4 6.227V4h16v2.172a2 2 0 0 1-.586 1.414L15 12v1.5m2.001 5.5a2 2 0 1 0 4 0a2 2 0 1 0-4 0m2-3.5V17m0 4v1.5m3.031-5.25l-1.299.75m-3.463 2l-1.3.75m0-3.5l1.3.75m3.463 2l1.3.75"
/>
<title>Status</title>
</svg>
Status
</div>
<SelectValue<
ReturnType<typeof filteredStatusList>[0]
> class="flex h-full items-center gap-1">
{(state) => (
<Show
when={state.selectedOptions().length <= 2}
fallback={
<>
<Badge class="absolute -top-2 right-0 block size-4 rounded-full p-0 capitalize md:hidden">
{state.selectedOptions().length}
</Badge>
<Badge class="hidden capitalize md:inline-flex py-0 px-1">
{state.selectedOptions().length} selected
</Badge>
</>
}
>
<For each={state.selectedOptions()}>
{(item) => (
<>
<Badge class="absolute -top-2 right-0 block size-4 rounded-full p-0 capitalize md:hidden">
{state.selectedOptions().length}
</Badge>
<Badge class="hidden capitalize md:inline-flex py-0 px-1">
{item.title}
</Badge>
</>
)}
</For>
</Show>
)}
</SelectValue>
</Button>
)}
/>
<SelectContent />
</Select>
<DropdownMenu placement="bottom-end">
<DropdownMenuTrigger
as={(props: DropdownMenuTriggerProps) => (
<Button
{...props}
aria-label="Toggle columns"
variant="outline"
class="flex h-8"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 size-4"
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0-4 0" />
<path d="M12 18c-3.6 0-6.6-2-9-6c2.4-4 5.4-6 9-6c3.6 0 6.6 2 9 6m-3.999 7a2 2 0 1 0 4 0a2 2 0 1 0-4 0m2-3.5V17m0 4v1.5m3.031-5.25l-1.299.75m-3.463 2l-1.3.75m0-3.5l1.3.75m3.463 2l1.3.75" />
</g>
<title>View</title>
</svg>
View
</Button>
)}
/>
<DropdownMenuContent class="w-40">
<DropdownMenuGroup>
<DropdownMenuGroupLabel>Toggle columns</DropdownMenuGroupLabel>
<DropdownMenuSeparator />
<For
each={table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" &&
column.getCanHide(),
)}
>
{(column) => (
<DropdownMenuCheckboxItem
class="capitalize"
checked={column.getIsVisible()}
onChange={(value) => column.toggleVisibility(value)}
>
<span class="truncate">{column.id}</span>
</DropdownMenuCheckboxItem>
)}
</For>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div class="rounded-md border">
<Table>
<TableHeader>
<For each={table.getHeaderGroups()}>
{(headerGroup) => (
<TableRow>
<For each={headerGroup.headers}>
{(header) => {
return (
<TableHead>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
}}
</For>
</TableRow>
)}
</For>
</TableHeader>
<TableBody>
<Show
when={table.getRowModel().rows?.length}
fallback={
<TableRow>
<TableCell colSpan={columns.length} class="h-24 text-center">
No results.
</TableCell>
</TableRow>
}
>
<For each={table.getRowModel().rows}>
{(row) => (
<TableRow data-state={row.getIsSelected() && "selected"}>
<For each={row.getVisibleCells()}>
{(cell) => (
<TableCell>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
)}
</For>
</TableRow>
)}
</For>
</Show>
</TableBody>
</Table>
</div>
<div class="flex w-full flex-col-reverse items-center justify-between gap-4 overflow-auto px-2 py-1 sm:flex-row">
<div class="flex-1 whitespace-nowrap text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div class="flex flex-col-reverse items-center gap-4 sm:flex-row">
<div class="flex items-center space-x-2">
<p class="whitespace-nowrap text-sm font-medium">Rows per page</p>
<Select
value={table.getState().pagination.pageSize}
onChange={(value) => value && table.setPageSize(value)}
options={[10, 20, 30, 40, 50]}
itemComponent={(props) => (
<SelectItem item={props.item}>{props.item.rawValue}</SelectItem>
)}
>
<SelectTrigger class="h-8 w-[4.5rem]">
<SelectValue<string>>
{(state) => state.selectedOption()}
</SelectValue>
</SelectTrigger>
<SelectContent />
</Select>
</div>
<div class="flex items-center justify-center whitespace-nowrap text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div>
<div class="flex items-center space-x-2">
<Button
aria-label="Go to first page"
variant="outline"
class="flex size-8 p-0"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m11 7l-5 5l5 5m6-10l-5 5l5 5"
/>
</svg>
</Button>
<Button
aria-label="Go to previous page"
variant="outline"
size="icon"
class="size-8"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m15 6l-6 6l6 6"
/>
</svg>
</Button>
<Button
aria-label="Go to next page"
variant="outline"
size="icon"
class="size-8"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m9 6l6 6l-6 6"
/>
</svg>
</Button>
<Button
aria-label="Go to last page"
variant="outline"
size="icon"
class="flex size-8"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m7 7l5 5l-5 5m6-10l5 5l-5 5"
/>
</svg>
</Button>
</div>
</div>
</div>
</div>
);
};
export default DataTableDemo;
介绍
我创建的每个数据表格或数据网格都是独一无二的。 它们的行为都不同,具有特定的排序和过滤要求,并且可以与不同的数据源一起使用。
将所有这些变体组合到一个组件中是没有意义的。 如果我们这样做,我们将失去 headless UI 提供的灵活性。
因此,我认为提供一个关于如何构建自己的数据表格的指南会更有帮助,而不是提供一个数据表格组件。
我们将从基本的 <Table />
组件开始,从头开始构建一个复杂的数据表格。
提示: 如果您发现自己在应用程序的多个位置使用同一个表格,您可以随时将其提取到可重用的组件中。
目录
本指南将向您展示如何使用 TanStack Table 和 <Table />
组件来构建您自己的自定义数据表格。 我们将涵盖以下主题:
安装
- 将
<Table />
组件添加到您的项目
npx shadcn-solid@latest add table button dropdown-menu textfield checkbox
- 添加
tanstack/solid-table
依赖
npm install @tanstack/solid-table
前提条件
我们将构建一个表格来显示任务。 这是我们的数据看起来的样子
type Task = {
id: string;
code: string;
title: string;
status: "todo" | "in-progress" | "done" | "cancelled";
label: "bug" | "feature" | "enhancement" | "documentation";
};
export const tasks: Task[] = [
{
id: "ptL0KpX_yRMI98JFr6B3n",
code: "TASK-33",
title: "We need to bypass the redundant AI interface!",
status: "todo",
label: "bug"
},
{
id: "RsrTg_SmBKPKwbUlr7Ztv",
code: "TASK-59",
title:
"Overriding the capacitor won't do anything, we need to generate the solid state JBOD pixel!",
status: "in-progress",
label: "feature"
}
// ...
];
项目结构
首先创建以下文件结构
src
└── routes
├── _components
│ ├── columns.tsx
│ └── data-table.tsx
└── index.tsx
-
columns.tsx
将包含我们的列定义。 -
data-table.tsx
将包含我们的<DataTable />
组件。 -
index.tsx
是我们将获取数据并渲染表格的位置。
基本表格
让我们从构建一个基本表格开始。
列定义
首先,我们将定义我们的列。
import type { ColumnDef } from "@tanstack/solid-table";
// This type is used to define the shape of our data.
// You can use a Zod or Validbot schema here if you want.
export type Task = {
id: string;
code: string;
title: string;
status: "todo" | "in-progress" | "done" | "cancelled";
label: "bug" | "feature" | "enhancement" | "documentation";
};
export const columns: ColumnDef<Task>[] = [
{
accessorKey: "code",
header: "Task"
},
{
accessorKey: "title",
header: "Title"
},
{
accessorKey: "status",
header: "Status"
}
];
<DataTable />
组件
接下来,我们将创建一个 <DataTable />
组件来渲染我们的表格。
import type { ColumnDef } from "@tanstack/solid-table";
import { flexRender, createSolidTable, getCoreRowModel } from "@tanstack/solid-table";
import { For, Show, splitProps, Accessor } from "solid-js";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table";
type Props<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: Accessor<TData[] | undefined>;
};
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
const [local] = splitProps(props, ["columns", "data"]);
const table = createSolidTable({
get data() {
return local.data() || [];
},
columns: local.columns,
getCoreRowModel: getCoreRowModel()
});
return (
<div class="rounded-md border">
<Table>
<TableHeader>
<For each={table.getHeaderGroups()}>
{headerGroup => (
<TableRow>
<For each={headerGroup.headers}>
{header => {
return (
<TableHead>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
}}
</For>
</TableRow>
)}
</For>
</TableHeader>
<TableBody>
<Show
when={table.getRowModel().rows?.length}
fallback={
<TableRow>
<TableCell colSpan={local.columns.length} class="h-24 text-center">
No results.
</TableCell>
</TableRow>
}
>
<For each={table.getRowModel().rows}>
{row => (
<TableRow data-state={row.getIsSelected() && "selected"}>
<For each={row.getVisibleCells()}>
{cell => (
<TableCell>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
)}
</For>
</TableRow>
)}
</For>
</Show>
</TableBody>
</Table>
</div>
);
};
渲染表格
最后,我们将在我们的索引页面中渲染我们的表格。
import type { Task } from "./_components/columns";
import { columns } from "./_components/columns";
import { DataTable } from "./_components/data-table";
import type { RouteDefinition } from "@solidjs/router";
import { cache, createAsync } from "@solidjs/router";
const getData = cache(async (): Promise<Task[]> => {
// Fetch data from your API here.
return [
{
id: "ptL0KpX_yRMI98JFr6B3n",
code: "TASK-33",
title: "We need to bypass the redundant AI interface!",
status: "todo",
label: "bug"
}
// ...
];
}, "data");
export const route: RouteDefinition = {
load: () => getData()
};
const Home = () => {
const data = createAsync(() => getData());
return (
<div class="w-full space-y-2.5">
<DataTable columns={columns} data={data} />
</div>
);
};
export default Home;
行操作
更新我们的列定义以添加一个新的 actions
列。 actions
单元格返回一个 <Dropdown />
组件。
//...
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
export const columns: ColumnDef<Task>[] = [
// ...
{
id: "actions",
cell: () => (
<DropdownMenu placement="bottom-end">
<DropdownMenuTrigger class="flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0"
/>
</svg>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
// ...
];
您可以使用 row.original
在 cell
函数中访问行数据。 使用此功能来处理您的行的操作,例如,使用 id
向您的 API 发出 DELETE 调用。
分页
接下来,我们将为我们的表格添加分页。
更新 <DataTable>
//...
import {
//...
getPaginationRowModel
} from "@tanstack/solid-table";
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
const table = createSolidTable({
//...
getPaginationRowModel: getPaginationRowModel()
});
// ...
};
这将自动将您的行分页为 10 页。 有关自定义页面大小和实现手动分页的更多信息,请参阅 分页文档。
添加分页控件
我们可以使用 <Button />
组件和 table.previousPage()
, table.nextPage()
API 方法将分页控件添加到我们的表格。
//...
import { Button } from "@/components/ui/button"
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) {
//...
return (
<div>
<div class="rounded-md border">...
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
)
}
排序
更新 <DataTable>
//...
import {
//...
getSortedRowModel
} from "@tanstack/solid-table";
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
const [sorting, setSorting] = createSignal<SortingState>([]);
const table = createSolidTable({
//...
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
state: {
get sorting() {
return sorting();
}
}
});
// ...
};
使标题单元格可排序
更新 status
标题单元格以添加排序控件。
//...
import { Button } from "@/components/ui/button";
export const columns: ColumnDef<Task>[] = [
// ...
{
accessorKey: "status",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Status
<svg
xmlns="http://www.w3.org/2000/svg"
class="ml-2 size-4"
aria-hidden="true"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m4-4l-4 4m-4-4l4 4"
/>
</svg>
</Button>
);
}
}
// ...
];
过滤
向标题添加过滤器。 有关自定义过滤器的更多信息,请参阅 过滤文档。
更新 <DataTable>
//...
import {
//...
getFilteredRowModel
} from "@tanstack/solid-table";
import { TextField, TextFieldInput } from "~/components/ui/textfield";
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
//...
const [columnFilters, setColumnFilters] = createSignal<ColumnFiltersState>([]);
const table = createSolidTable({
//...
getFilteredRowModel: getFilteredRowModel(),
state: {
//...
get columnFilters() {
return columnFilters();
}
}
});
<div>
<div class="flex items-center py-4">
<TextField>
<TextFieldInput
placeholder="Filter title..."
value={(table.getColumn("title")?.getFilterValue() as string) ?? ""}
onInput={event => table.getColumn("title")?.setFilterValue(event.currentTarget.value)}
class="max-w-sm"
/>
</TextField>
</div>
<div class="rounded-md border">...</div>
</div>;
};
可见性
使用可见性 API 添加列可见性。
更新 <DataTable>
//...
import { As } from "@kobalte/core";
import { Button } from "~/components/ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
//...
const [columnVisibility, setColumnVisibility] = createSignal<VisibilityState>({});
const table = createSolidTable({
//...
onColumnVisibilityChange: setColumnVisibility,
state: {
//...
get columnVisibility() {
return columnVisibility();
}
}
});
<div>
<div class="flex items-center py-4">
//...
<DropdownMenu>
<DropdownMenuTrigger asChild>
<As component={Button} variant="outline" class="ml-auto">
Columns
</As>
</DropdownMenuTrigger>
<DropdownMenuContent>
<For each={table.getAllColumns().filter(column => column.getCanHide())}>
{item => (
<DropdownMenuCheckboxItem
class="capitalize"
checked={item.getIsVisible()}
onChange={value => item.toggleVisibility(!!value)}
>
{item.id}
</DropdownMenuCheckboxItem>
)}
</For>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div class="rounded-md border">...</div>
</div>;
};
行选择
接下来,我们将为我们的表格添加行选择。
更新列定义
//...
import { Checkbox, CheckboxControl } from "@/components/ui/checkbox";
export const columns: ColumnDef<Task>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
indeterminate={table.getIsSomePageRowsSelected()}
checked={table.getIsAllPageRowsSelected()}
onChange={value => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
>
<CheckboxControl />
</Checkbox>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onChange={value => row.toggleSelected(!!value)}
aria-label="Select row"
>
<CheckboxControl />
</Checkbox>
),
enableSorting: false,
enableHiding: false
}
// ...
];
更新 <DataTable>
//...
export const DataTable = <TData, TValue>(props: Props<TData, TValue>) => {
//...
const [rowSelection, setRowSelection] = createSignal({});
const table = createSolidTable({
//...
onRowSelectionChange: setRowSelection,
state: {
// ...
get rowSelection() {
return rowSelection();
}
}
});
<div>...</div>;
};
显示所选行
您可以使用 table.getFilteredSelectedRowModel()
API 显示所选行的数量。
<div class="text-muted-foreground flex-1 text-sm">
{table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length}{" "}
row(s) selected.
</div>