文档
数据表格

数据表格

使用 TanStack Table 构建的强大表格和数据网格。

介绍

我创建的每个数据表格或数据网格都是独一无二的。 它们的行为都不同,具有特定的排序和过滤要求,并且可以与不同的数据源一起使用。

将所有这些变体组合到一个组件中是没有意义的。 如果我们这样做,我们将失去 headless UI 提供的灵活性。

因此,我认为提供一个关于如何构建自己的数据表格的指南会更有帮助,而不是提供一个数据表格组件。

我们将从基本的 <Table /> 组件开始,从头开始构建一个复杂的数据表格。

提示: 如果您发现自己在应用程序的多个位置使用同一个表格,您可以随时将其提取到可重用的组件中。

目录

本指南将向您展示如何使用 TanStack Table<Table /> 组件来构建您自己的自定义数据表格。 我们将涵盖以下主题:

安装

  1. <Table /> 组件添加到您的项目
npx shadcn-solid@latest add table button dropdown-menu textfield checkbox
  1. 添加 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 是我们将获取数据并渲染表格的位置。

基本表格

让我们从构建一个基本表格开始。

列定义

首先,我们将定义我们的列。

src/routes/_components/columns.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 /> 组件来渲染我们的表格。

src/routes/_components/data-table.tsx
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>
  );
};

渲染表格

最后,我们将在我们的索引页面中渲染我们的表格。

src/routes/index.tsx
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 /> 组件。

src/routes/_components/columns.tsx
//...
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.originalcell 函数中访问行数据。 使用此功能来处理您的行的操作,例如,使用 id 向您的 API 发出 DELETE 调用。

分页

接下来,我们将为我们的表格添加分页。

更新 <DataTable>

src/routes/_components/data-table.tsx
//...
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 方法将分页控件添加到我们的表格。

src/routes/_components/data-table.tsx
//...
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>

src/routes/_components/data-table.tsx
//...
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 标题单元格以添加排序控件。

src/routes/_components/columns.tsx
//...
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>

src/routes/_components/data-table.tsx
//...
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>

src/routes/_components/data-table.tsx
//...
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>;
};

行选择

接下来,我们将为我们的表格添加行选择。

更新列定义

src/routes/_components/columns.tsx
//...
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>

src/routes/_components/data-table.tsx
//...
 
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>