HOOKS

Public Hooks encapsulated table selection operation useTableRowSelection

Table Selection Operation with Encapsulated Public Hook: useTableRowSelection

Foreword

For front-end developers who frequently build enterprise management backends, working with tables to manipulate data is essential. When refactoring existing projects, it’s a good practice to encapsulate some reusable custom hooks. This article is for ‘useTableRowSelection.js’

Hooks encapsulated based on a personal project environment; this article only shares insights and best practices for hook encapsulation, so the accompanying code may not be directly applicable to others.

Project Environment

Vue 3.x + Ant Design Vue 3.x + Vite 3.x

Encapsulation Rationale: Why encapsulate useTableRowSelection.js

First of all, based on the encapsulation of Hooks(‘useTableData.js’,’ useQueryParams.js’), it is related to batch deletion and batch editing of common operations of managing background tables. packaging’ useTableRowSelection.js’ can be encapsulated and used, which can only be processed in the page table component ‘:row-selection = “rowSelection”’, instead of writing duplicate code of table selection in each single page file. Additionally, in the official component documentation for ‘Ant Design Vue’ or ‘Ant Design’, you can find a selection example for the Table component: after pagination, the previously selected data is cleared. Well, now the official team can finally provide this feature—tears of joy~ PS: At least August 2022 definitely does not have this ability, because my Hooks was encapsulated at that time, and only recently did I start writing articles again ~

Encapsulation and Decomposition: Table Multi-Selection

Because when these Hooks were initially wrapped, the official documentation did not provide support for cross-page multi‑selection, and since the official team has since introduced this functionality, the relevant code only illustrates the encapsulation approach. In multi-select mode, for tables that require cross-page selection, the selected data is cached. The checkbox needs to account for both individual selection and the “Select All” functionality (applied to the header of each paginated table). The selected ‘selectKeys’ and ‘selectItems’ are processed and exposed, and then provided to the page that needs to be selected for table selection for data processing, including but not limited to the parameter transmission or echo required by the request service interface.

//  选择
onSelect: (record, selected) => {
  if (selected) {
    selectKeys.value.push(record[rowId]);
    if ($cacheItem) selectItems.value = [...selectItems.value, ...[record]];
  } else {
    const index = selectKeys.value.findIndex(key => key === record[rowId]);
    if (index >= 0) {
      selectKeys.value.splice(index, 1);
      if ($cacheItem) {
        const $cacheSelectItems = selectItems.value;
        $cacheSelectItems.splice(index, 1);
        selectItems.value = [...$cacheSelectItems];
      }
    }
  }
},
  
// 全选
onSelectAll: selected => {
  if (selected) {
    tableData.value.forEach(item => {
      const index = selectKeys.value.findIndex(id => item[rowId] === id);
      if (index < 0) {
        selectKeys.value.push(item[rowId]);
        if ($cacheItem) selectItems.value = [...selectItems.value, ...[item]];
      }
    });
  } else {
    tableData.value.forEach(item => {
      const index = selectKeys.value.findIndex(key => key === item[rowId]);
      if (index >= 0) {
        selectKeys.value.splice(index, 1);
        if ($cacheItem) {
          const $cacheSelectItems = selectItems.value;
          $cacheSelectItems.splice(index, 1);
          selectItems.value = [...$cacheSelectItems];
        }
      }
    });
  }
},

Encapsulated Thinking: A Complete Table Selection Hook

Initially, this was done to align with the use of table-related hooks such as ‘useTableData’ and ‘useQueryParams,’ with the aim of reducing duplicate code throughout the project. The complete ‘useTableRowSelection’ should account for both multiple‑selection and single‑selection scenarios within the table. although multi-selection can realize the function of single selection , the UI presentation mode of multi-selection and single selection in the page is still different ~, so Hooks receives table source data’ tableData’, filter type’ selectType’, unique key’ rowId’ and whether caching is enabled (I .e. cross-page filtering, after all, not all tables need cross-page filtering) ‘cacheItem = false’. Here, the encapsulated Hooks are slightly different from the capabilities provided by the existing authorities. The screening of tables in official documents is cross-page selection, considering actual business (of course, only individual companies are involved in business scenarios, which may not be universal), and only when it is necessary to select personnel to participate in an actual business project or adjust the relevant visible range.

Encapsulation Breakdown: Exposing clearKeys and isEmptyKeys

Expose the clearKeys method to clear table filters, and expose the isEmptyKeys method to determine whether the table filters are empty.

Complete code for useTableRowSelection.js


export function useTableRowSelection(tableData, selectType, rowId, cacheItem = false) {
  if (!isRef(tableData)) throw new Error('参数 tableData 必须为 Ref 类型');

  const selectKeys = ref([]);

  const selectItems = shallowRef([]);

  const rowSelection = computed(() => {
    const $selectType = isRef(selectType) ? selectType.value : selectType;
    const $cacheItem = isRef(cacheItem) ? selectType.value : cacheItem;
    if ($selectType === ROW_SELECT_TYPE.CHECKBOX) {
      return {
        onSelect: (record, selected) => {
          if (selected) {
            selectKeys.value.push(record[rowId]);
            if ($cacheItem) selectItems.value = [...selectItems.value, ...[record]];
          } else {
            const index = selectKeys.value.findIndex(key => key === record[rowId]);
            if (index >= 0) {
              selectKeys.value.splice(index, 1);
              if ($cacheItem) {
                const $cacheSelectItems = selectItems.value;
                $cacheSelectItems.splice(index, 1);
                selectItems.value = [...$cacheSelectItems];
              }
            }
          }
        },
        onSelectAll: selected => {
          if (selected) {
            tableData.value.forEach(item => {
              const index = selectKeys.value.findIndex(id => item[rowId] === id);
              if (index < 0) {
                selectKeys.value.push(item[rowId]);
                if ($cacheItem) selectItems.value = [...selectItems.value, ...[item]];
              }
            });
          } else {
            tableData.value.forEach(item => {
              const index = selectKeys.value.findIndex(key => key === item[rowId]);
              if (index >= 0) {
                selectKeys.value.splice(index, 1);
                if ($cacheItem) {
                  const $cacheSelectItems = selectItems.value;
                  $cacheSelectItems.splice(index, 1);
                  selectItems.value = [...$cacheSelectItems];
                }
              }
            });
          }
        },
        getCheckboxProps: record => {
          return {
            disabled: record.disabled,
          };
        },
        selectedRowKeys: selectKeys.value,
        type: ROW_SELECT_TYPE.CHECKBOX,
      };
    } else {
      return {
        onSelect: record => {
          selectKeys.value = [record[rowId]];
          if ($cacheItem) selectItems.value = [record];
        },
        selectedRowKeys: selectKeys.value,
        getCheckboxProps: record => {
          return {
            disabled: record.disabled,
          };
        },
        type: ROW_SELECT_TYPE.RADIO,
      };
    }
  });

  const clearKeys = () => {
    selectKeys.value.length = 0;
    selectItems.value = [];
  };

  const isEmptyKeys = () => {
    return selectKeys.value.length === 0;
  };

  return {
    selectItems,
    selectKeys,
    rowSelection,
    clearKeys,
    isEmptyKeys,
  };
}

Practical Use: Reference Example

-Page component ‘Page.vue’

<a-table
  size="small"
  class="mt-2"
  row-key="id"
  :data-source="tableData"
  :loading="loading"
  :columns="columns"
  :row-selection="rowSelection"
  @change="onTableChange"
/>

-Specific usage

  const { rowSelection, selectKeys, clearKeys, isEmptyKeys } = useTableRowSelection(
    tableData,
    ROW_SELECT_TYPE.CHECKBOX,
    'id',
  );

  const confirmSelect = () => {
    if (isEmptyKeys()) { // 确认按钮,业务场景,表格筛选项非空校验
      proxy.$message.warning('请选择****');
      return;
    }
    const selectItem = tableData.value.find(item => item.id === selectKeys.value[0]);
    emits('on-select', {
      id: selectItem.id,
    });
    closeModal();
  };
  
  const closeModal = () => {
    clearKeys();
    resetParams();  // 还记得这个吗~`useQueryParams.js`Hooks内提供的方法~
  };

Related posts

View all →