alibaba / hooks

A high-quality & reliable React Hooks library. https://ahooks.pages.dev/
https://ahooks.js.org/
MIT License
14.06k stars 2.71k forks source link

[Feature] 是否有计划支持 Antd Table rowSelection #2471

Open raotaohub opened 9 months ago

raotaohub commented 9 months ago

请问目前是否有计划支持 Antd Table 组件的 rowSelection属性通过 hook

  1. 单独新建立一个 useTableSelections 模块 (目前已有useSelections)
  2. 或是基于 useAntdTable 通过第2个参数拓展 (是否有开销问题,和适用性不够广的问题,或是说这个能力不应该在该hooks中)

下面是我目前在使用的实现

基于方案1的实现

import React, { useMemo, useState } from 'react';
import { useMemoizedFn } from 'ahooks';

type RowSelectionType = 'checkbox' | 'radio';

type GetRowKey<RecordType> = (record: RecordType, index?: number) => React.Key;

interface TableRowSelection<RecordType> {
  type: RowSelectionType;
  selectedRowKeys: React.Key[];
  defaultSelectedRowKeys: React.Key[];
  getCheckboxProps: (row: RecordType) => { disabled: boolean };
  onChange: (selectedRowKeys: React.Key[], selectedRows: RecordType[]) => void;
  rowKey: GetRowKey<RecordType>;
  disabled: boolean | ((row: RecordType) => boolean);
}

export interface AntdTableSelections<RecordType> {
  state: {
    allSelected: boolean;
    selectedRows: RecordType[];
    selectedRowKeys: React.Key[];
  };
  action: {
    toggle: (item: RecordType) => void;
    toggleAll: () => void;
    selectAll: () => void;
    unSelectAll: () => void;
    setSelected: (keys: React.Key[]) => void;
  };
  rowSelection: Omit<TableRowSelection<RecordType>, 'rowKey' | 'disabled'>;
}

function useTableSelections<RecordType>(
  rows: RecordType[],
  config?: Partial<TableRowSelection<RecordType>>
): AntdTableSelections<RecordType> {
  const {
    type = 'checkbox',
    rowKey = 'key',
    defaultSelectedRowKeys = [],
    disabled = false,
    ...rest
  } = config || {};

  const [selectedRows, setSelectedRows] = useState<RecordType[]>(rows || []);
  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>(defaultSelectedRowKeys);

  // ========================= Keys =========================
  const getRowKey = React.useMemo<GetRowKey<RecordType>>(() => {
    if (typeof rowKey === 'function') {
      return rowKey;
    }

    return (record: RecordType) => (record as any)?.[rowKey];
  }, [rowKey]);

  // ========================= Funcs =========================
  const onRowsChange = useMemoizedFn((rows: RecordType[]) => {
    setSelectedRows(rows);
    setSelectedRowKeys(rows.map((t) => getRowKey(t)));
  });

  const onSelectionChange = useMemoizedFn(
    (selectedRowKeys: React.Key[], selectedRows: RecordType[]) => {
      setSelectedRows(selectedRows);
      setSelectedRowKeys(selectedRowKeys);
    }
  );

  const getCheckboxProps = useMemoizedFn((row: RecordType) => {
    if (typeof disabled === 'boolean') {
      return { disabled };
    }

    if (typeof disabled === 'function') {
      return { disabled: disabled(row) };
    }

    return { disabled: false };
  });

  // ========================= Select Set =========================
  const selectedSet = useMemo(() => new Set(selectedRows), [selectedRows]);

  const isSelected = (item: RecordType) => selectedSet.has(item);

  const unSelect = (item: RecordType) => {
    selectedSet.delete(item);
    onRowsChange(Array.from(selectedSet));
  };

  const selectAll = () => {
    rows.forEach((o) => {
      selectedSet.add(o);
    });

    onRowsChange(Array.from(selectedSet));
  };

  const unSelectAll = () => {
    rows.forEach((o) => {
      selectedSet.delete(o);
    });
    onRowsChange(Array.from(selectedSet));
  };

  const select = (item: RecordType) => {
    selectedSet.add(item);
    onRowsChange(Array.from(selectedSet));
  };

  const toggle = (item: RecordType) => {
    if (isSelected(item)) {
      unSelect(item);
    } else {
      select(item);
    }
  };

  const noneSelected = useMemo(() => rows.every((o) => !selectedSet.has(o)), [rows, selectedSet]);

  const allSelected = useMemo(() => rows.every((o) => selectedSet.has(o)) && !noneSelected, [
    rows,
    selectedSet,
    noneSelected,
  ]);

  const toggleAll = () => (allSelected ? unSelectAll() : selectAll());

  const setSelected = useMemoizedFn((keys: React.Key[]) => setSelectedRowKeys(keys));

  return {
    state: {
      allSelected,
      selectedRows,
      selectedRowKeys,
    },
    action: {
      toggle,
      toggleAll,
      selectAll,
      unSelectAll,
      setSelected,
    },
    rowSelection: {
      ...rest,
      type,
      onChange: onSelectionChange,
      getCheckboxProps,
      selectedRowKeys,
      defaultSelectedRowKeys,
    },
  };
}

export default useTableSelections;

基于以上方案1的使用

配合 useAntdTable 获得 tableProps.dataSource

  const { loading, tableProps, search } = useAntdTable(getTableData, {
    form,
  });

  const { state, action, rowSelection } = useTableSelections<IProject>(tableProps.dataSource, {
    rowKey: (row) => row.guid,
    disabled: (row) => row.diabled || row.name === 'foo',
  });

  return (
        <Table
        {...tableProps}
        rowSelection={rowSelection}
        columns={columns}
        rowKey={(row) => row.guid}
      />
  )
raotaohub commented 9 months ago

之所以上面的代码没有直接使用 useSelections ,是因为需要同时关注 rows 和 rowKeys 所以代码成本差不多。 上面的代码一个简易示例,完整版已通过pr提交https://github.com/alibaba/hooks/pull/2477

raotaohub commented 9 months ago

video example

https://github.com/alibaba/hooks/assets/66949076/c5a9a372-221c-4f44-874f-af7d77ca78f6

crazylxr commented 8 months ago

@hchlq 你这边先调研一下呢

hchlq commented 8 months ago

这个 hooks 使用场景好像不是很多呢?

liuyib commented 8 months ago

@raotaohub 需要加这个 useAntdTableSelection hook 是不是因为 useSelections 不支持对象数组,导致你 useSelections 和 Table 结合起来很麻烦?

我前几天也遇到这种需要自己实现跨页选中的场景(一般组件库内置,但也会有需要手动实现的场景),使用 useSelections 实现不了,所以我改进了 useSelections: https://github.com/alibaba/hooks/pull/2485 (这个 PR 的改进我已经投入实际使用了,完全能 cover 住复杂场景的跨页选择)。

所以,我的理解,如果 useSelections 支持对象数组,这种跨页多选的场景(Table 跨页多选、List 跨页多选 等),都直接覆盖了。

liuyib commented 8 months ago

@raotaohub 你可以把这个 PR 的代码 copy 到你本地,新建一个临时 hook 文件粘进去(一些报错,缺失的工具方法,用 lodash-es 替换掉)。试试 useSelections 支持了对象数组后,是不是就满足你的需求了?

我不太建议新增 useAntdTableSelection 这个 hook 哈,要不然 Table 要加个 useAntdTableSelection、List 要加个 useListSelection、......

raotaohub commented 8 months ago

@raotaohub 需要加这个 useAntdTableSelection hook 是不是因为 useSelections 不支持对象数组,导致你 useSelections 和 Table 结合起来很麻烦?

我前几天也遇到这种需要自己实现跨页选中的场景(一般组件库内置,但也会有需要手动实现的场景),使用 useSelections 实现不了,所以我改进了 useSelections: #2485 (这个 PR 的改进我已经投入实际使用了,完全能 cover 住复杂场景的跨页选择)。

所以,我的理解,如果 useSelections 支持对象数组,这种跨页多选的场景(Table 跨页多选、List 跨页多选 等),都直接覆盖了。

是的,原有的useSelections确实无法覆盖 Table 在 rowSelection

另外还有一些原因

我考虑过改进useSelections,但是我翻了antd Table 在 rowSelection涉及逻辑中,使用到的单选,多选等逻辑,和已有的Checkbox组件(已被useSelections所支持)其实不完全一样 。

具体如 Checkbox 不关注对象数组,不关注数组对象的rowKey,不可切换为radio形态,而这在Table的rowSelection是需要的。

raotaohub commented 8 months ago

@raotaohub 你可以把这个 PR 的代码 copy 到你本地,新建一个临时 hook 文件粘进去(一些报错,缺失的工具方法,用 lodash-es 替换掉)。试试 useSelections 支持了对象数组后,是不是就满足你的需求了?

我不太建议新增 useAntdTableSelection 这个 hook 哈,要不然 Table 要加个 useAntdTableSelection、List 要加个 useListSelection、......

copy 到你本地

我会尝试copy 到你本地运行看看。目前我的项目中已经在使用 useAntdTableSelection 我会进行对比,看看是否有更好的方案

raotaohub commented 8 months ago

@raotaohub 你可以把这个 PR 的代码 copy 到你本地,新建一个临时 hook 文件粘进去(一些报错,缺失的工具方法,用 lodash-es 替换掉)。试试 useSelections 支持了对象数组后,是不是就满足你的需求了?

我不太建议新增 useAntdTableSelection 这个 hook 哈,要不然 Table 要加个 useAntdTableSelection、List 要加个 useListSelection、......

对于useAntdTable这个独立hook而言是只关注table和请求的相关的,而Table组件足够强大和复杂,单个hooks的出现是为了覆盖真正的高频场景。

我和你有一样的担忧,倘若每次都要针对Table一些功能再建立一个独立hook, 可能会起一个不好的头。这点还是需要看看这个功能是否足够高频和通用。crazylxr 正如他所说,应该调研看看~

之所以最终还是提出了这个issue和pr,是考虑到在大量2b业务中和个别业务领域里,这个功能也是相当高频

liuyib commented 8 months ago

不可切换为radio形态,而这在Table的rowSelection是需要的

这个有点疑惑,Table 中 radio 貌似没有意义啊?能否给出使用场景 @raotaohub

raotaohub commented 8 months ago

不可切换为radio形态,而这在Table的rowSelection是需要的

这个有点疑惑,Table 中 radio 貌似没有意义啊?能否给出使用场景 @raotaohub

实际业务中的确不常见,这里我是举个例子说明 useSelections 和 Table 的 rowSelection的差异(这里支持type为radio)