让我们创建数据表。第 5 部分:细胞分选和定位

这是关于使用 **React**、**TanStack Table 8**、**Tailwind CSS** 和 **Headless UI** 创建高级数据表组件的系列文章之一。

在上一篇文章中,我们为用户提供了将表格列固定在视口左侧或右侧的机会。

**单元格排序** 是保持表格数据井然有序的另一种方法。此功能允许用户根据特定列按升序或降序重新组织数据。这使用户能够更有效地探索和分析数据。

在本练习中,我们将使用 TanStack 表排序 API 实现单元格排序。我们将介绍以下内容:

  • 对表数据应用排序:我们将配置 TanStack 表以启用基本排序功能。
  • 创建自定义排序函数:我们将探讨如何为特定数据类型或排序需求定义自定义排序函数。
  • 访问和操作排序状态:我们将学习如何检索和修改高级用例的排序状态。
  • 以下是表格排序用户体验的演示。

    使用 TanStack 列排序 API

    TanStack 为我们提供了方便的排序 API,我们将使用它来实现行排序逻辑。

    应用 Row 模型

    首先,我们需要从 TanStack 添加 Sorted Row Model。

    import { FC } from 'react';
    import {
      useReactTable,
      getCoreRowModel,
      getSortedRowModel,
    } from '@tanstack/react-table';
    
    export const DataTable: FC = () => {
      const table = useReactTable({
        columns,
        data: tableData,
        getCoreRowModel: getCoreRowModel(),
        // apply Sorted Row Model from TanStack
        getSortedRowModel: getSortedRowModel(),
      });
    
      // ...

    添加列操作

    下一步是使用表排序 API 扩展“src/DataTable/features/useColumnActions.tsx”,添加两个新的排序操作。

    /**
     * React hook which returns an array 
     * of table column actions config objects
     */
    export const useColumnActions = (
      context: HeaderContext,
    ): ColumnAction[] => {
      // Get column sorting state using TanStack table API
      const isSorted = context.column.getIsSorted();
    
      return useMemo(
        () => [
          //...
          {
            // Use ternary expression to decide which label, text
            // or icon to render, according to the sorting state
            label: isSorted !== 'asc' ? 'Sort ascending' : 'Clear ascending',
            icon:
              isSorted !== 'asc' ? (
                
              ) : (
                
              ),
            onClick: () => {
              // Conditionally set or unset column sorting state 
              // using TanStack table API
              if (isSorted !== 'asc') {
                context.table.setSorting([{ desc: false, id: context.column.id }]);
              } else {
                context.column.clearSorting();
              }
            },
          },
          {
            label: isSorted !== 'desc' ? 'Sort descending' : 'Clear descending',
            icon:
              isSorted !== 'desc' ? (
                
              ) : (
                
              ),
            onClick: () => {
              if (isSorted !== 'desc') {
                context.table.setSorting([{ desc: true, id: context.column.id }]);
              } else {
                context.column.clearSorting();
              }
            },
          },
        ],
        [context.column, context.table, isSorted],
      );
    };

    应用突出显示

    我们还必须突出显示当前已排序的列的单元格。以下是此操作的代码:

    
      {/*...*/}
    

    添加自定义排序功能

    目前,我们能够正确地对“字符串”、“数字”和“日期”单元格进行排序。但在尝试对列进行排序时,我们遇到了问题。

    在上一章中,我们制作了“Country”单元格,以接受“value”属性作为两个字母的 ISO 3166 区域代码,并呈现本地化的国家名称。应用于区域代码的字符串排序与国家名称的预期排序不匹配。

    为了解决这个问题,我们必须为该特定列提供自定义排序功能。

    我们将向 TanStack 表内置方法(例如 `auto`、`alphanumeric`、`alphanumericCaseSensitive`、`text`、`textCaseSensitive`、`datetime`、`basic`)添加自定义排序函数。我们将使用 `countryCodesToNames` 对其进行扩展。此外,我们需要创建 `src/DataTable/declarations.d.ts` 并在此处注册自定义搜索函数。

    import '@tanstack/react-table';
    import { Row } from './types.ts';
    
    declare module '@tanstack/react-table' {
      interface SortingFns {
        countryCodesToNames: SortingFn
      }
    }

    下一步,我们将创建 `src/DataTable/features/useSortingFns.ts` 钩子。

    我们将以与国家单元格相同的方式从提供的国家代码中获取国家名称显示值。

    const leftName = new Intl.DisplayNames(
      LOCALE, 
      { type: 'region' })
    .of(
      left.getValue(id)
    );

    然后我们使用 Intl.Collat​​or 对象,它可以启用语言敏感的字符串比较,以确保国家名称按正确的顺序设置。

    以下是完整的钩子代码:

    import { Row as TableRow, SortingFn } from '@tanstack/react-table';
    import { useCallback } from 'react';
    import { Row } from './../types.ts';
    
    export const useSortingFns = (locale?: string) => {
      const countryCodesToNames: SortingFn = useCallback(
        (left: TableRow, right: TableRow, id: string) => {
          const leftName = new Intl.DisplayNames(locale, { type: 'region' }).of(
            left.getValue(id),
          );
          const rightName = new Intl.DisplayNames(locale, { type: 'region' }).of(
            right.getValue(id),
          );
          return typeof leftName === 'string' && typeof rightName === 'string'
            ? new Intl.Collator(locale).compare(leftName, rightName)
            : 0;
        },
        [locale],
      );
      return { countryCodesToNames };
    };

    这是我们将对“src/DataTable/columnsConfig.tsx”文件提供的更改。

    export const columns = [
      columnHelper.accessor('address.country', {
        sortingFn: 'countryCodesToNames',
        //...
      })
      //...
    ];

    本地化重构

    您可能注意到,我们在许多组件中都依赖“locale”设置。每次都手动设置是一种反模式,我们现在要对其进行重构。

    我们将把我们选择的“区域设置”记录到 TanStack 表元数据中。

    首先,我们将使用“locale”元属性定义扩展“src/DataTable/declarations.d.ts”。

    import '@tanstack/react-table';
    import { Row } from './types.ts';
    
    declare module '@tanstack/react-table' {
      interface TableMeta {
        locale: string;
      }
      //...
    }

    最后,我们将这些更改应用到“src/DataTable/DataTable.tsx”。

    //...
    import { useSortingFns } from './features/useSortingFns.ts';
    
    type Props = {
      //...
      locale?: string;
    };
    
    export const DataTable: FC = ({ tableData, locale = 'en-US' }) => {
    
      // create a custom sorting function
      const { countryCodesToNames } = useSortingFns(locale);
    
      const table = useReactTable({
        meta: {
          // record locale to the table meta
          locale,
        },
        sortingFns: {
          // set the custom sorting function we created for the table
          countryCodesToNames,
        },
        //...
      });
      //...
    }

    我们还必须将相同的 prop 应用于使用依赖于语言环境的转换的 `src/DataTable/cells` 组件。

    export type Props = {
      //...
      locale?: string;
    };
    
    export const CountryCell: FC = ({ value, locale }) => {
      const formattedValue =
        value !== undefined
          ? new Intl.DisplayNames(locale, { type: 'region' }).of(value)
          : '';
    
      //...
    };

    现在我们可以轻松更改所有表数据的区域设置。

    Localized data demo

    演示

    这是此练习的工作演示。

    [附加功能] 可控制排序状态

    TanStack API 允许开发人员使用受控排序设计。因此,每次用户对列进行排序时,都会调用 `onSortingChange` 回调,并且我们可以选择禁用内部排序,转而使用我们在外部提供的排序。这可能对服务器端计算有用。

    创建 `src/DataTable/features/useSorting.ts` 钩子。

    import { useState, useEffect } from "react";
    import type { SortingState } from "@tanstack/react-table";
    
    export type Props = {
        sortingProp: SortingState;
        onSortingChange: (sortingState: SortingState) => void;
    };
    
    export const useSorting = ({ sortingProp, onSortingChange }: Props) => {
        const [sorting, setSorting] = useState(sortingProp);
    
        useEffect(() => {
            setSorting(sortingProp);
        }, [sortingProp]);
    
        useEffect(() => {
            onSortingChange(sorting);
        }, [onSortingChange, sorting]);
    
        return { sorting, setSorting };
    };

    然后我们必须更新`src/DataTable/DataTable.tsx`

    import {
      useReactTable,
      SortingState,
    } from '@tanstack/react-table';
    
    type Props = {
      //...
      /**
        * Control table data sorting externally
        * @see SortingState
        */
      sorting?: SortingState;
      /**
       * Provide a callback to capture table data sorting changes
       * @see SortingState
       */
      onSortingChange?: (sortingState: SortingState) => void;
    };
    
    export const DataTable: FC = ({
      tableData,
      locale = 'en-US',
      onSortingChange,
    }) => {
      //...
      const {sorting, setSorting} = useSorting({sortingProp, onSortingChange});
    
      const table = useReactTable({
        //..
        state: {
          sorting,
        },
        // Set this to true for full external control over sorting state
        manualSorting: false,
        onSortingChange: setSorting,
      })
    }

    下一步:列过滤