HOOKS

Public Hooks package custom table data column rendering useTableColumns

Encapsulation of Public Hooks: Custom Table Column Rendering with `useTableColumns`

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 ‘useTableColumns.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 Thinking: What Is Custom Table Data Column Rendering, and What Scenarios Does It Serve?

Based on actual business scenarios, and to mitigate legal risks, certain screenshot content has been anonymized.

As shown in the figure, when a table contains a large number of columns, the typical approach is to fix the columns on the left and right—namely, the key column on the left and the action column on the right—while allowing the central content area to scroll horizontally. However, when there are a lot of data columns and each column of data has its true meaning, we should consider adopting a better way. For different system users, everyone usually has different concerns. Therefore, everyone can take the initiative to change the table column items to show the data columns of concern.

! tableColumnsDemo

In light of the scenarios described above and considering optimization, we arrive at the following personalized solution: the encapsulation of ‘useTableColumns’.

Encapsulation and Decomposition: Main Ideas Columns

In conjunction with the widely used UI framework Ant Design Vue, a Column represents a data field and is one of the items in the Columns array; Columns utilize the same API. Common data structures are as follows:

  columns: [
    {
      title: '姓名',
      dataIndex: 'name',
      key: 'name',
    },
    {
      title: '年龄',
      dataIndex: 'age',
      key: 'age',
    },
    {
      title: '住址',
      dataIndex: 'address',
      key: 'address',
    },
  ],

In the data structure above, each column in the table is annotated with its corresponding header, data index, and other relevant metadata. In addition, a set of APIs is provided in the official documentation. Based on the screenshot provided earlier, the approach is actually quite clear: simply dynamically modify the JSON data. In actual use, the data columns that need to participate in the change should be assigned the custom attribute: ‘addFilter’.

Component Decomposition: Dropdown Menu – Filtered Data Column Display Component

<template #headerCell="{ column }">
  <template v-if="column.key === 'action'">
    <div class="flex justify-between items-center">
      <span>操作</span>
      <table-field-filter :columns="columns" @change="onFilterColumnChange" />
    </div>
  </template>
</template>

The above code is based on the UI framework used in a real-world project—Ant Design Vue. You can also refer to other frameworks, such as Element Plus, and examine the relevant APIs or coding patterns provided by their Table component.

In the project, I chose to place’ onFilterColumnChange’ on the header of the table operation column. in actual use, it can also be placed in other areas, such as screening modules, titles, etc. it depends on how to design it, without damaging the existing layout or requiring UI designers to design it a little ~

Encapsulation and Decomposition: Dynamically Changing Columns

let includeColumns = []; // 参与过滤的列表字段集合

let excludeColumns = []; // 不参与过滤的列表字段集合

const columns = shallowRef([]); // 需要展示的列表字段集合

if (options?.initFilterKeys.length > 0) {
  for (let i = 0, len = options.initFilterKeys.length; i < len; i++) {
    const key = options.initFilterKeys[i];
    const index = $columns.findIndex(item => item.key === key);
    $columns.splice(index, 1);
  }
}

Currently, the implementation involves placing the table columns used for filtering into one array, moving the items that are not subject to filtering into another array, and then merging the two arrays. In combination with the actual business, there may be no corresponding data for some special scenarios and will not participate in display control. For example, in the enterprise background-enterprise WeChat/or other similar ecological account login systems, account control is not actually required (or due to policy restrictions and other reasons, consider blocking the account display in the form). Of course, this is in combination with the actual business and will not be repeated here ~

Encapsulation and Decomposition: utils–getArrayDiff

/**
 * @description 筛选两个数组不同的元素
 */
export function getArrayDiff(arr1, arr2) {
  return arr1.concat(arr2).filter((item, index, arr) => arr.indexOf(item) === arr.lastIndexOf(item));
}

Complete code for useTableColumns.js


export function useTableColumns(defaultColumns, options) {
  if (!defaultColumns || !Array.isArray(defaultColumns)) return;

  let includeColumns = []; // 参与过滤的列表字段集合

  let excludeColumns = []; // 不参与过滤的列表字段集合

  const columns = shallowRef([]); // 需要展示的列表字段集合

  const onFilterColumnChange = keys => {
    const orKeys = includeColumns.map(item => item.key);
    const delKeys = getArrayDiff(orKeys, keys);
    const $columns = cloneDeep(includeColumns);
    for (let i = 0, len = delKeys.length; i < len; i++) {
      const key = delKeys[i];
      const index = $columns.findIndex(item => item.key === key);
      $columns.splice(index, 1);
    }
    columns.value = [...$columns, ...excludeColumns];
  };

  // 初始化字段处理
  const initColumns = () => {
    const $columns = cloneDeep(defaultColumns);
    if (options?.initFilterKeys.length > 0) {
      for (let i = 0, len = options.initFilterKeys.length; i < len; i++) {
        const key = options.initFilterKeys[i];
        const index = $columns.findIndex(item => item.key === key);
        $columns.splice(index, 1);
      }
    }
    for (let i = 0, len = $columns.length; i < len; i++) {
      const item = $columns[i];
      if (item.addFilter) {
        includeColumns.push(item);
      } else {
        excludeColumns.push(item);
      }
    }
    columns.value = $columns;
  };

  initColumns();

  return {
    columns,
    onFilterColumnChange,
  };
}

Further Reflection: Are There Any Issues with the Current Encapsulation? How Can It Be Optimized?

For Columns processing in Hooks, two arrays are used to temporarily store table columns that need to be filtered and table columns that do not need to be filtered respectively. Since only one page “As shown in the screenshot” in the actual project needs to process the above operations, for table column data, the ‘addFilter’ custom attribute is added except for the operation column. However, in actual business requirements, the following situations may occur in table fields:
Only a brief explanation

[字段A, 字段B, 字段C, 字段D, 字段E, 字段F, 字段G, 操作列 ]

If the ‘addFilter’ custom attribute is added to all columns except the action column, it would be no different from my current approach and would not pose any issues,
If the above fields (field a, field c, field g) do not participate in the filtering, and the rest of the fields participate in the filtering, the currently encapsulated Hooks do not meet the requirements, because according to the existing writing method, the columns array will become
after going through the’ onFilterChange’

[字段A, 字段C, 字段G, 字段B, 字段E, 字段F, 操作列 ]

In this case, for the original table, the original index order is broken, which only satisfies the filtering, but it is not the original location “delete/hide” filtering. For actual business, it may not be satisfied ~
How to solve it? Currently, due to the demand iteration, there is no time to deal with it ~ To be optimized later ~
Of course, I’d also love to hear your ideas for solutions, or even collaborate on optimizing the code!

Related posts

View all →