ข้ามไปเนื้อหา
WhereUI

ค้นหาในเอกสาร

ค้นหาชื่อ component หรือหน้าเอกสาร

DataTable

WhereUI

ตารางข้อมูลแบบสมบูรณ์บน TanStack Table v9 ครบในตัวเดียว: เรียงคอลัมน์ ค้นหารวม แบ่งหน้า เลือกแถวด้วย checkbox สถานะ loading และ empty พร้อม toolbar สำหรับปุ่มเพิ่มเติม

สมชาย ใจดีผู้จัดการฝ่ายบุคคลฝ่ายบุคคล65,000 บาท4 มี.ค. 2562
สมหญิง รักงานนักบัญชีฝ่ายการเงิน42,000 บาท15 มิ.ย. 2563
ธนกฤต ศรีสุขนักพัฒนาซอฟต์แวร์ฝ่ายไอที55,000 บาท11 ม.ค. 2564
ปวีณา ทองดีผู้จัดการฝ่ายการขายฝ่ายขาย68,000 บาท1 พ.ย. 2561
ณัฐพล วงศ์ดีนักวิเคราะห์ระบบฝ่ายไอที50,000 บาท14 ก.พ. 2565
กมลชนก แสงทองเจ้าหน้าที่ฝ่ายสนับสนุนลูกค้าฝ่ายขาย28,000 บาท22 พ.ค. 2566
พิชญะ ตั้งมั่นนักการตลาดดิจิทัลฝ่ายการตลาด38,000 บาท1 ส.ค. 2565
อรทัย พูนสุขผู้จัดการฝ่ายการเงินฝ่ายการเงิน72,000 บาท3 เม.ย. 2560
ศิริชัย มั่นคงวิศวกร DevOpsฝ่ายไอที60,000 บาท6 ก.ย. 2564
จิราภรณ์ สุขเจริญเจ้าหน้าที่ฝ่ายบุคคลฝ่ายบุคคล30,000 บาท16 ม.ค. 2566

แถวที่ 1–10 จาก 25

หน้า 1 จาก 3

ติดตั้ง

Terminal
pnpm dlx shadcn@latest add @whereui/data-table

ต้องผูก registry และติดตั้ง @whereui/theme นำหน้าเสมอเมื่อติดตั้งครั้งแรก

การใช้งาน

import { DataTable, type DataTableFeatures } from "@/components/ui/data-table"import type { ColumnDef } from "@tanstack/react-table"const columns: ColumnDef<DataTableFeatures, Employee, any>[] = [  { accessorKey: "name", header: "ชื่อ-นามสกุล" },  { accessorKey: "department", header: "แผนก" },]<DataTable columns={columns} data={employees} />

ตัวอย่างเพิ่มเติม

เลือกแถวด้วย checkbox

เลือกแล้ว 0 รายการ

สมชาย ใจดีผู้จัดการฝ่ายบุคคลฝ่ายบุคคล65,000 บาท4 มี.ค. 2562
สมหญิง รักงานนักบัญชีฝ่ายการเงิน42,000 บาท15 มิ.ย. 2563
ธนกฤต ศรีสุขนักพัฒนาซอฟต์แวร์ฝ่ายไอที55,000 บาท11 ม.ค. 2564
ปวีณา ทองดีผู้จัดการฝ่ายการขายฝ่ายขาย68,000 บาท1 พ.ย. 2561
ณัฐพล วงศ์ดีนักวิเคราะห์ระบบฝ่ายไอที50,000 บาท14 ก.พ. 2565
กมลชนก แสงทองเจ้าหน้าที่ฝ่ายสนับสนุนลูกค้าฝ่ายขาย28,000 บาท22 พ.ค. 2566
พิชญะ ตั้งมั่นนักการตลาดดิจิทัลฝ่ายการตลาด38,000 บาท1 ส.ค. 2565
อรทัย พูนสุขผู้จัดการฝ่ายการเงินฝ่ายการเงิน72,000 บาท3 เม.ย. 2560
ศิริชัย มั่นคงวิศวกร DevOpsฝ่ายไอที60,000 บาท6 ก.ย. 2564
จิราภรณ์ สุขเจริญเจ้าหน้าที่ฝ่ายบุคคลฝ่ายบุคคล30,000 บาท16 ม.ค. 2566

แถวที่ 1–10 จาก 25

หน้า 1 จาก 3
สถานะ loading (skeleton)

กำลังโหลดข้อมูล…

หน้า 1 จาก 1

Props

proptypeค่าเริ่มต้นคำอธิบาย
columnsColumnDef<DataTableFeatures, TData, any>[]—นิยามคอลัมน์ตาม API ของ TanStack Table v9 เช่น accessorKey, header, cell, enableSorting
dataReadonlyArray<TData>—ข้อมูลแถวของตาราง (generic ตามชนิดที่คอลัมน์อ้างถึง)
loadingbooleanfalseแสดง skeleton แทนแถวข้อมูลระหว่างโหลด
enableSelectionbooleanfalseเพิ่มคอลัมน์ checkbox สำหรับเลือกแถว (หัวคอลัมน์เลือกทั้งหน้าได้ มีสถานะ indeterminate)
toolbarReact.ReactNode—ช่องวางปุ่มเพิ่มเติมถัดจากช่องค้นหา เช่น ปุ่มเพิ่มรายการ หรือส่งออกไฟล์
pageSizeOptionsnumber[][10, 25, 50]ตัวเลือกขนาดหน้าใน dropdown ท้ายตาราง (ค่าแรกใช้เป็นขนาดหน้าเริ่มต้น)
getRowId(row: TData, index: number) => string—กำหนด id ของแถว แนะนำให้ใส่เมื่อเปิด enableSelection เพื่อให้การเลือกติดกับข้อมูลถูกต้องแม้เรียงใหม่
onSelectionChange(selectedRows: TData[]) => void—เรียกเมื่อการเลือกแถวเปลี่ยน ได้อาร์เรย์ข้อมูลดิบของแถวที่เลือกทั้งหมด
classNamestring—class เพิ่มเติมของ element ราก (ส่งต่อ props อื่นของ div ได้ด้วย)

ข้อควรรู้

  • จอแคบ: ตารางเลื่อนแนวนอนได้ในกรอบของตัวเอง ไม่ดันหน้าเว็บให้กว้าง
  • เข้าถึงได้: หัวคอลัมน์ที่เรียงได้เป็นปุ่มจริงใช้คีย์บอร์ดได้ มี aria-sort และ checkbox มี aria-label ภาษาไทย
  • กด Shift ค้างพร้อมคลิกหัวคอลัมน์เพื่อเรียงหลายคอลัมน์พร้อมกัน
  • ข้อความสถานะ (ค้นหา, ว่าง, แบ่งหน้า) เป็นภาษาไทยสำเร็จรูปในตัว component

ซอร์สโค้ด

components/ui/data-table.tsx
"use client"import * as React from "react"import {  columnFilteringFeature,  createFilteredRowModel,  createPaginatedRowModel,  createSortedRowModel,  filterFn_includesString,  globalFilteringFeature,  rowPaginationFeature,  rowSelectionFeature,  rowSortingFeature,  tableFeatures,  useTable,  FlexRender,  type ColumnDef,  type RowData,} from "@tanstack/react-table"import {  ArrowDownIcon,  ArrowUpDownIcon,  ArrowUpIcon,  ChevronLeftIcon,  ChevronRightIcon,  ChevronsLeftIcon,  ChevronsRightIcon,  SearchIcon,  SearchXIcon,} from "lucide-react"import { cn } from "cn"import { Button } from "@/components/ui/button"import { Checkbox } from "@/components/ui/checkbox"import {  Empty,  EmptyDescription,  EmptyHeader,  EmptyMedia,  EmptyTitle,} from "@/components/ui/empty"import { Input } from "@/components/ui/input"import {  Select,  SelectContent,  SelectItem,  SelectTrigger,  SelectValue,} from "@/components/ui/select"import { Skeleton } from "@/components/ui/skeleton"import {  Table,  TableBody,  TableCell,  TableHead,  TableHeader,  TableRow,} from "@/components/ui/table"// ชุด features ที่ DataTable ใช้ (TanStack Table v9 ต้องประกาศ features + row model เอง)// ประกาศนอก component ตามคำแนะนำของห้องสมุดconst dataTableFeatures = tableFeatures({  columnFilteringFeature,  globalFilteringFeature,  rowPaginationFeature,  rowSelectionFeature,  rowSortingFeature,  filteredRowModel: createFilteredRowModel(),  paginatedRowModel: createPaginatedRowModel(),  sortedRowModel: createSortedRowModel(),})export type DataTableFeatures = typeof dataTableFeaturesexport interface DataTableProps<TData extends RowData>  extends Omit<React.ComponentProps<"div">, "children"> {  /** นิยามคอลัมน์ตาม API ของ TanStack Table v9 */  columns: Array<ColumnDef<DataTableFeatures, TData, any>>  /** ข้อมูลแถวของตาราง */  data: ReadonlyArray<TData>  /** แสดง skeleton แทนแถวข้อมูลระหว่างโหลด */  loading?: boolean  /** เพิ่มคอลัมน์ checkbox เลือกแถว (เลือกทั้งหน้าได้) */  enableSelection?: boolean  /** ช่องวางปุ่มเพิ่มเติมถัดจากช่องค้นหา */  toolbar?: React.ReactNode  /** ตัวเลือกขนาดหน้าใน dropdown */  pageSizeOptions?: number[]  /** กำหนด id ของแถว (แนะนำให้ใส่เมื่อเปิด enableSelection เพื่อให้การเลือกติดตามข้อมูลถูกต้อง) */  getRowId?: (row: TData, index: number) => string  /** เรียกเมื่อการเลือกแถวเปลี่ยน ได้ข้อมูลดิบของแถวที่เลือกทั้งหมด */  onSelectionChange?: (selectedRows: TData[]) => void}function DataTable<TData extends RowData>({  columns,  data,  loading = false,  enableSelection = false,  toolbar,  pageSizeOptions = [10, 25, 50],  getRowId,  onSelectionChange,  className,  ...props}: DataTableProps<TData>) {  // คอลัมน์เลือกแถว (แสดงเฉพาะเมื่อ enableSelection)  const selectColumn = React.useMemo<    ColumnDef<DataTableFeatures, TData, any>  >(    () => ({      id: "select",      enableSorting: false,      enableGlobalFilter: false,      header: ({ table: t }) => (        <Checkbox          aria-label="เลือกทุกแถวในหน้านี้"          checked={t.getIsAllPageRowsSelected()}          indeterminate={            t.getIsSomePageRowsSelected() && !t.getIsAllPageRowsSelected()          }          onCheckedChange={(checked) =>            t.toggleAllPageRowsSelected(Boolean(checked))          }        />      ),      cell: ({ row }) => (        <Checkbox          aria-label="เลือกแถวนี้"          checked={row.getIsSelected()}          onCheckedChange={(checked) => row.toggleSelected(Boolean(checked))}        />      ),    }),    []  )  const allColumns = React.useMemo(    () => (enableSelection ? [selectColumn, ...columns] : columns),    [enableSelection, selectColumn, columns]  )  const table = useTable({    features: dataTableFeatures,    columns: allColumns,    data,    getRowId,    // ค้นหารวมแบบไม่สนตัวพิมพ์เล็ก-ใหญ่    globalFilterFn: filterFn_includesString,    initialState: {      pagination: { pageIndex: 0, pageSize: pageSizeOptions[0] },    },  })  const { globalFilter, pagination, rowSelection } = table.state  const rows = table.getRowModel().rows  const columnCount = table.getAllLeafColumns().length  const total = table.getRowCount()  const pageCount = table.getPageCount()  const start = total === 0 ? 0 : pagination.pageIndex * pagination.pageSize + 1  const end = Math.min((pagination.pageIndex + 1) * pagination.pageSize, total)  // แจ้งแถวที่เลือกเฉพาะเมื่อสถานะการเลือกเปลี่ยน  React.useEffect(() => {    onSelectionChange?.(      table.getSelectedRowModel().rows.map((row) => row.original)    )    // eslint-disable-next-line react-hooks/exhaustive-deps  }, [rowSelection])  return (    <div      data-slot="data-table"      className={cn("flex w-full min-w-0 flex-col gap-3", className)}      {...props}    >      <div        data-slot="data-table-toolbar"        className="flex flex-wrap items-center gap-2"      >        <div className="relative w-full sm:max-w-64">          <SearchIcon className="pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground" />          <Input            aria-label="ค้นหาในตาราง"            placeholder="ค้นหา…"            className="pl-8"            value={globalFilter ?? ""}            onChange={(event) => table.setGlobalFilter(event.target.value)}          />        </div>        {toolbar ? (          <div className="ms-auto flex flex-wrap items-center gap-2">            {toolbar}          </div>        ) : null}      </div>      <div className="overflow-hidden rounded-xl border border-border bg-surface shadow-card">        <Table>          <TableHeader>            {table.getHeaderGroups().map((headerGroup) => (              <TableRow                key={headerGroup.id}                className="bg-muted/50 hover:bg-muted/50"              >                {headerGroup.headers.map((header) => {                  const sorted = header.column.getIsSorted()                  return (                    <TableHead                      key={header.id}                      colSpan={header.colSpan}                      aria-sort={                        sorted === "asc"                          ? "ascending"                          : sorted === "desc"                            ? "descending"                            : undefined                      }                    >                      {header.isPlaceholder ? null : header.column.getCanSort() ? (                        <Button                          variant="ghost"                          size="sm"                          className="-ml-2 gap-1 font-medium"                          onClick={header.column.getToggleSortingHandler()}                        >                          <FlexRender header={header} />                          {sorted === "asc" ? (                            <ArrowUpIcon />                          ) : sorted === "desc" ? (                            <ArrowDownIcon />                          ) : (                            <ArrowUpDownIcon className="size-3.5 opacity-60" />                          )}                        </Button>                      ) : (                        <FlexRender header={header} />                      )}                    </TableHead>                  )                })}              </TableRow>            ))}          </TableHeader>          <TableBody>            {loading ? (              // กำลังโหลด: แสดง skeleton แทนแถวข้อมูล              Array.from({ length: 6 }, (_, rowIdx) => (                <TableRow key={`loading-${rowIdx}`}>                  {table.getAllLeafColumns().map((column) => (                    <TableCell key={column.id}>                      <Skeleton className="h-4 w-full max-w-24" />                    </TableCell>                  ))}                </TableRow>              ))            ) : rows.length === 0 ? (              <TableRow>                <TableCell colSpan={columnCount} className="p-0">                  <Empty className="min-h-48">                    <EmptyHeader>                      <EmptyMedia variant="icon">                        <SearchXIcon />                      </EmptyMedia>                      <EmptyTitle>ไม่พบข้อมูล</EmptyTitle>                      <EmptyDescription>                        ลองเปลี่ยนคำค้นหา หรือเพิ่มข้อมูลใหม่แล้วลองอีกครั้ง                      </EmptyDescription>                    </EmptyHeader>                  </Empty>                </TableCell>              </TableRow>            ) : (              rows.map((row) => (                <TableRow                  key={row.id}                  data-state={row.getIsSelected() ? "selected" : undefined}                >                  {row.getAllCells().map((cell) => (                    <TableCell key={cell.id}>                      <FlexRender cell={cell} />                    </TableCell>                  ))}                </TableRow>              ))            )}          </TableBody>        </Table>      </div>      <div        data-slot="data-table-pagination"        className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"      >        <p className="text-sm whitespace-nowrap text-muted-foreground">          {loading            ? "กำลังโหลดข้อมูล…"            : total === 0              ? "ไม่พบข้อมูล"              : `แถวที่ ${start}–${end} จาก ${total}`}        </p>        <div className="flex flex-wrap items-center gap-2">          <Select            value={String(pagination.pageSize)}            onValueChange={(value) =>              table.setPagination({                pageIndex: 0,                pageSize: Number(value),              })            }          >            <SelectTrigger size="sm" aria-label="เลือกขนาดหน้า">              <SelectValue>{(value) => `${value} แถว/หน้า`}</SelectValue>            </SelectTrigger>            <SelectContent>              {pageSizeOptions.map((size) => (                <SelectItem key={size} value={String(size)}>                  {size} แถว/หน้า                </SelectItem>              ))}            </SelectContent>          </Select>          <div className="flex items-center gap-1">            <Button              variant="ghost"              size="icon-sm"              aria-label="หน้าแรก"              disabled={!table.getCanPreviousPage()}              onClick={table.firstPage}            >              <ChevronsLeftIcon />            </Button>            <Button              variant="ghost"              size="icon-sm"              aria-label="หน้าก่อนหน้า"              disabled={!table.getCanPreviousPage()}              onClick={table.previousPage}            >              <ChevronLeftIcon />            </Button>            <Button              variant="ghost"              size="icon-sm"              aria-label="หน้าถัดไป"              disabled={!table.getCanNextPage()}              onClick={table.nextPage}            >              <ChevronRightIcon />            </Button>            <Button              variant="ghost"              size="icon-sm"              aria-label="หน้าสุดท้าย"              disabled={!table.getCanNextPage()}              onClick={table.lastPage}            >              <ChevronsRightIcon />            </Button>          </div>          <span className="text-sm whitespace-nowrap text-muted-foreground">            หน้า {pagination.pageIndex + 1} จาก {Math.max(pageCount, 1)}          </span>        </div>      </div>    </div>  )}export { DataTable }