Lists also have basic functions, such as paging, search, selection, and operation. Repetitive logic in different services will result in bloated code that is difficult to read and maintain. React higher-order components can be used to solve these problems.

Ant-design-pro framework provides advanced table Protable function and supporting practice schemes. Personally, I think the function is bloated and it is difficult to use and configure, so I realized a customized version of Protable by referring to its advanced table.

I. Problems to be solved

The UI framework used in the project is ant-Design, which is common in the React background management project. Its Table component already supports functions such as sorting, searching, paging, batching, and customization of list data. What needs to be done here is a layer of encapsulation on the basis of it, making some preset and open customization behaviors. The summary is as follows:

  • Configurable request method.
  • Form search data, you can configure the type according to the list item field attribute to generate a search form.
  • Inherit the Table component Api and methods from Ant-Design.
  • Expose internal component methods such as selection, list data overloading, field export, and so on.

Finally, the introduction of the higher-order components in the page, simple configuration interface, list field attributes can be a key to generate add, delete, change and check page template.

Two. Function realization

1. Interface style

The interface here imitates the form Design of Ant-Design-Pro background management framework. The search area for list fields is on the top. When there are many search items, you can expand and fold them up. Below is the display area of paging list contents, which can be paging, sorting, refreshing, list item selection, single item and batch operation.

2. Parameter interface design

Protable is a layer of encapsulation on antD Table, which supports some presets. Here only lists the different APIS from ANTD Table:

  • Title: String, optional, table title or name.
  • Description: String, optional, table description.
  • toolBarRender: Function (ref, rows: {selectedRowKeys? :(string | number)[], selectedRows? :T[]} => react.reactNodes []), optional, render toolbar.
  • BeforeSearchSubmit: Function (params: T) => T, non-mandatory, data processing before submitting the form, can be processed in this conversion when the list field value and the back-end defined search field are not consistent.
  • Request: Function (params: searchParams)=> Promise
  • Columns: Object ProTableColumnProps[], required.
  • RowSelection: Object ProTableRowSelection, optional, internal automatic control, rowSelection setting.
  • Ref: (ref: Actions) => void, optional, get refresh, reset list, get search parameters of the object.

Columns configuration parameter, which is also based on the original Columns attribute to expand the list field search, list style and other configuration, here also lists the Api different from Antd Table Cloumns:

  • Type: String (please fill in the type controls for input | select | multiSelect | datePicker |…). If the type value is configured, the query form control that matches the type of the field will be automatically generated above the list.
  • FormItemProps: Object, optional. After determining the type control, configure some properties of the query form control.
  • Ellipsis: Boolean | (values: searchFormValue) = > Boolean, must not, whether the cell long character omitted.
  • IsCopy: Boolean | (values: searchFormValue) = > Boolean, not a must, if click copy content of cells.
  • HideInTable: Boolean | (values: searchFormValue) = > Boolean, not a must, if control fields to display in the list.
  • HideInSearch: Boolean | (values: searchFormValue) = > Boolean, not a must, control field is displayed in the search form

3. ProTable component writing

import React from "react";
import { Table, Card, Icon, Tooltip, Button, message } from "antd";
import TableForm from "./TableForm"; //列表搜索表单
import { isEqual, debounce } from "lodash";
import ColumnsSetting from "./columnsSetting";
import styled from "styled-components";

const Layout = styled.div`
    .table-title {
	    color: #555555;
		font-weight: 500;
		font-size: 15px;
		.table-title-description {
			display: inline-block;
			margin-left: 15px;
			cursor: pointer;
		}
        .left-selectionNum{
            display: inline-block;
            margin-left: 15px;
            font-size: 14px;
            color: #555555;
            transition: all 0.3s;
            .selection-num{
                display: inline-block;
                color: #ff4d4f;
                margin: 0 2px;
            }
        }
	}
	.copyStyle {
		display: inline-block;
		cursor: pointer;
		&:hover {
			.ant-typography-copy {
				visibility: visible !important;
			}
		}
		.ant-typography-copy {
			visibility: hidden !important;
		}
	}
	.table-operation {
		cursor: pointer;
		margin-left: 20px;
		display: inline-block;
	}
	/* 多行文本省略 */
	.tabel-more-ellipsis {
		overflow: hidden;
		text-overflow: ellipsis;
		display: -webkit-box;
		-webkit-line-clamp: 1;
		-webkit-box-orient: vertical;
		word-break: break-word;
		white-space: normal;
	}
`;

/**
 * @name 标准列表页组件
 * @description 包含 搜素表单 列表分页 工具条  这三部分生成
 * ProTable 封装了分页和搜索表单的 table组件
 */
class ProTable extends React.Component {
    constructor(props) {
        super(props);
        this.fetchData = debounce(this.fetchData, 30, { leading: true, trailing: false, maxWait: 300 });
    }

    state = {
        // table密度
        density: "default", // default | middle | small
        tableHeight: window.innerHeight,
        // data
        dataSource: [],
        total: 0,
        loading: false,
        // pagination
        pagination: {
            pageNo: 1, //搜索起始页
            pageSize: 10, //每页条数
        },
        // rowSelection
        selectedRowKeys: [],
        selectedRows: [],
        // form
        searchFormValue: { FIRST_TIME_LOADING_TAG: true },
        forceUpdate: false,
        isShowSearch: false,
    };

    componentDidMount() {
        this.props.onRef && this.props.onRef(this);
        this.setState({ tableColumns: this.props.columns });
    }

    componentDidUpdate(prevProps, prevState) {
        if (
            !isEqual(prevState.pagination, this.state.pagination) ||
            !isEqual(prevState.searchFormValue, this.state.searchFormValue) ||
            (!isEqual(prevState.forceUpdate, this.state.forceUpdate) && this.state.forceUpdate === true)
        ) {
            this.fetchData();
        }
    }

    //设置初始化选中值
    setInitialSelectionRowKeys = (selectedRowKeys) => {
        this.setState({ selectedRowKeys });
    };

    //设置分页
    setPagination = (pageNo, pageSize) => {
        this.setState({ pagination: { pageNo, pageSize } });
    };

    //请求列表数据
    fetchData = async (params = {}) => {
        const { request } = this.props;
        const { searchFormValue, pagination } = this.state;
        if (!request) return;
        this.setState({ loading: true });
        try {
            const data = await request({ ...searchFormValue, ...pagination, ...params });
            // 如果查总条数 小于 pageNo*pageSize
            if (data.total < (pagination.pageNo - 1) * pagination.pageSize + 1 && data.total !== 0) {
                this.setState(
                    (prevState) => {
                        return {
                            pagination: {
                                pageNo: prevState.pagination.pageNo - 1,
                                pageSize: prevState.pagination.pageSize,
                            },
                        };
                    },
                    () => {
                        this.fetchData();
                    }
                );
            }
            this.setState({ dataSource: data.data, total: data.total, loading: false, forceUpdate: false });
        } catch (error) {
            console.warn(error);
            message.warn(error.msg);
            this.setState({ dataSource: [], loading: false, forceUpdate: false });
        }
    };

    //设置搜索表单数据
    setSearchFormValues = (values) => {
        this.setState({ searchFormValue: values });
    };

    //重置表格选择
    resetRowSelection = () => {
        this.setState({ selectedRowKeys: [], selectedRows: [] });
    };

    getSearchFormValue = () => {
        const { searchFormValue, pagination } = this.state;
        return { params: { ...searchFormValue, ...pagination }, searchFormValue, pagination };
    };

    //列表刷新
    reload = () => {
        this.fetchData();
        this.resetRowSelection();
    };

    //搜索重置
    reset = () => {
        if (this.formRef && this.formRef.reset) {
            this.setState({ forceUpdate: true });
            this.formRef.reset();
            this.resetRowSelection();
        }
    };

    //列排序以及界面增删显示
    handleColumnsChange = (val) => {
        const { columns } = this.props;
        const tableColumns = [];
        for (let i of columns) {
            for (let j of val) {
                if (i.dataIndex == j) {
                    tableColumns.push(i);
                }
            }
        }
        this.setState({ tableColumns: [...tableColumns, columns[columns.length - 1]] });
    };

    render() {
        const { pagination, total, dataSource, selectedRowKeys, selectedRows, loading, tableColumns = [], density } = this.state;
        const {
            title = "高级表格",
            description = "",
            request,
            toolBarRender,
            beforeSearchSubmit = (v) => v,
            rowSelection,
            pagination: tablePaginationConfig,
            rowKey,
            columns,
            columnsSettingDisabled = false,
            density: densityDisabled,
            ...other
        } = this.props;

        const filterColumns = columns.filter((o) => o.type && !o.hideInSearch);
        return (
            <Layout>
                <div className="filter-wrap" hidden={filterColumns.length == 0}>
                    <Card bordered={false}>
                        <TableForm
                            wrappedComponentRef={(ref) => (this.formRef = ref)}
                            onSubmit={(values) => {
                                this.setPagination(1, pagination.pageSize);
                                this.resetRowSelection();
                                this.setSearchFormValues(beforeSearchSubmit(values));
                            }}
                            filterForm={filterColumns}
                            isShowSearch={this.state.isShowSearch}
                        />
                    </Card>
                </div>
                <Card bordered={false}>
                    {toolBarRender && (
                        <div style={{ display: "flex", justifyContent: "space-between" }}>
                            <div className="table-title">
                                <span style={{ fontWeight: "bold" }}>{title}</span>
                                {description && (
                                    <span className="table-title-description">
                                        <Tooltip placement="right" title={description}>
                                            <Icon style={{ color: "#666666" }} type="question-circle" theme="filled" />
                                        </Tooltip>
                                    </span>
                                )}
                                {selectedRowKeys.length > 0 && (
                                    <span className="left-selectionNum">
                                        已选中<span className="selection-num">{selectedRowKeys.length}</span>项
                                    </span>
                                )}
                            </div>
                            {
                                <div style={{ textAlign: "right" }}>
                                    {toolBarRender(this, { selectedRowKeys, selectedRows, dataSource })}
                                    {filterColumns.length > 0 ? (
                                        <div className="table-operation" onClick={() => this.setState({ isShowSearch: true })}>
                                            <Button>
                                                查询
                                            </Button>
                                        </div>
                                    ) : null}
                                    <div className="table-operation" onClick={() => this.reset()}>
                                        <Button
                                            style={{
                                                border: "1px solid #3571FF",
                                                background: "rgba(53, 113, 255, 0.07)",
                                                color: "#3571FF",
                                            }}
                                            type="primary"
                                            icon="redo"
                                        >
                                            刷新
                                        </Button>
                                    </div>

                                    {!columnsSettingDisabled && (
                                        <Tooltip title="列设置">
                                            <div className="table-operation">
                                                <ColumnsSetting columns={columns} columnsChange={this.handleColumnsChange} />
                                            </div>
                                        </Tooltip>
                                    )}
                                </div>
                            }
                        </div>
                    )}

                    {/* {selectedRowKeys.length > 0 && `已选中${selectedRowKeys.length}项`} */}
                    <Table
                        {...other}
                        size={densityDisabled || density}
                        loading={loading}
                        rowKey={rowKey}
                        dataSource={dataSource}
                        columns={tableColumns
                            .filter((o) => !o.hideInTable)
                            .map((o) => {
                                if (!o.render) {
                                    if (o.type == "select" && !o.render && o.formItemProps && Array.isArray(o.formItemProps.options)) {
                                        o.render = (text) => {
                                            try {
                                                const target = o.formItemProps.options.find((item) => item.value == text);
                                                return (target ? target.label : text) || "-";
                                            } catch (e) {
                                                return typeof text == "undefined" || text == null ? "-" : text;
                                            }
                                        };
                                    } else {
                                        o.render = (text) => (
                                            <div
                                                onClick={() => {
                                                    if (!o.isCopy) return;
                                                    let inputDom = document.createElement("input");
                                                    document.body.appendChild(inputDom);
                                                    inputDom.value = text;
                                                    inputDom.select(); // 选中
                                                    document.execCommand("copy", false);
                                                    inputDom.remove(); //移除
                                                    message.success("复制成功");
                                                }}
                                            >
                                                {typeof text === "undefined" || text == null || text === "" ? "-" : text}
                                            </div>
                                        );
                                    }
                                }
                                if (o.ellipsis) {
                                    o.ellipsis = false;
                                    let render = o.render;
                                    o.render = (text, record, index) => (
                                        <div className="tabel-more-ellipsis">
                                            <Tooltip placement="topLeft" title={render(text, record, index)}>
                                                {render(text, record, index)}
                                            </Tooltip>
                                        </div>
                                    );
                                }
                                return o;
                            })}
                        pagination={{
                            ...tablePaginationConfig,
                            total,
                            showSizeChanger: true,
                            showQuickJumper: true,
                            current: pagination.pageNo,
                            pageSize: pagination.pageSize,
                            showTotal: (total, range) => `共${total}条`,
                            onChange: (pageNo, pageSize) => {
                                this.setPagination(pageNo, pageSize);
                            },
                            onShowSizeChange: (current, pageSize) => {
                                this.setPagination(current, pageSize);
                            },
                        }}
                        rowSelection={
                            rowSelection
                                ? {
                                    selectedRowKeys,
                                    ...rowSelection,
                                    onChange: (selectedRowKeys, selectedRows) => {
                                        if (rowSelection && rowSelection.onChange) {
                                            rowSelection.onChange(selectedRowKeys, selectedRows);
                                        }
                                        this.setState({ selectedRowKeys, selectedRows });
                                    },
                                    getCheckboxProps: (record) => {
                                        if (rowSelection && rowSelection.getCheckboxProps) {
                                            return rowSelection.getCheckboxProps(record, {
                                                selectedRowKeys,
                                                selectedRows,
                                                dataSource,
                                            });
                                        }
                                        return undefined;
                                    },
                                }
                                : undefined
                        }
                    />
                </Card>
            </Layout>
        );
    }
}

export default ProTable;
Copy the code

3. TableForm component compilation

The TableForm component is a search form for a list, including a list search reset function, which can load different types of search controls based on the Type type of Cloumns configuration item. Currently, the supported search controls include single-line text/number input, single-item/multiple selectors, and time/date selectors. The simple factory mode design is also convenient for the subsequent expansion of other types of controls, the following is the TableForm component code:

import React from 'react' import { Form, Button, Row, Col } from 'antd' import styled from 'styled-components'; import FilterControl from './filterControl' const Layout = styled.div` height: 100%; background: #ffffff; .collapse { color: #4569d4; text-decoration: none; transition: opacity .2s; outline: none; cursor: pointer; display: inline-block; margin-left: 16px; }. The table - form - anticon {margin - left: 0.5 em; Transition: all 0.3s ease 0s; transform: rotate(0); display: inline-block; color: inherit; font-style: normal; line-height: 0; text-align: center; text-transform: none; vertical-align: -.125em; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; } `; class FilterForm extends React.Component { constructor(props) { super(props) this.state = { isCollapse: CollapseNum: collapse collapseNum ComponentDidMount () {this.submit()} submit = () => {const {getFieldsValue} = this.props const values = getFieldsValue() if (this.props.onSubmit) { this.props.onSubmit(values) } } reset = () => { const { resetFields } = this.props.form resetFields() this.submit() } render() { const { getFieldDecorator } = this.props.form const { filterForm = [] } = this.props const { isCollapse, collapseNum } = this.state const formItemLayout = { labelCol: { xs: { span: 8 }, sm: { span: 6 }, }, wrapperCol: { xs: { span: 16 }, sm: { span: 18}}, } return ( <Layout> <Form layout="inline" onSubmit={e => { e.preventDefault() this.submit() }} onReset={e => { e.preventDefault() this.reset() }} {... formItemLayout} > <Row gutter={[24, 24]} type="flex" align="middle"> {filterForm.map((item, index) => ( (((index + 1) > collapseNum && ! isCollapse) || (index + 1) <= collapseNum) && ( <Col xxl={6} xl={8} md={12} xs={24} key={index}> <Form.Item style={{ width: '100%' }} label={item.formItemProps? .labelTitle || item.title}> {getFieldDecorator( item.key || item.dataIndex, { initialValue: item.initialValue } )(FilterControl.getControlComponent(item.type, { ... item }))} </Form.Item> </Col> ) ))} {filterForm.length > 0 && ( <Col xxl={6} xl={8} md={12} xs={24}> <div style={{ marginLeft: '27px', padding: "0 20px 0 0" }}> <Button htmlType="submit" type="primary" style={{ marginLeft: </Button> <Button htmlType="reset" type="reset" style={{marginLeft: </Button> <div className="collapse" onClick={() => {this.setState((state) =>  { return { isCollapse: ! state.isCollapse } }) }}> {isCollapse ? 'a' : 'fold'} < / div >} < / div > < / Col >)} < / Row > "< / Form > < / Layout >)}} export default Form. The create () (FilterForm)Copy the code

4. Write FilterControl

FilterControl collects the basic search controls and exports them in index: index.js:

import React from "react"; import SingleInput from "./singleInput"; // Single line import SingleSelect from "./ SingleSelect "; Import RangePickerSelect from './ RangePickerSelect '; // Select import MultiSelect from './ MultiSelect '; Import SearchSelect from './ SearchSelect '// remote SearchSelect const ControlUI = new Map(); ControlUI.set("input", SingleInput) ControlUI.set("select", SingleSelect) ControlUI.set("rangePickerSelect", RangePickerSelect) ControlUI.set("multiSelect", MultiSelect) ControlUI.set("searchSelect", SearchSelect) export default class ControlFactory { static getControlComponent(uiCode, args) { if (ControlUI.has(uiCode)) { return React.createElement(ControlUI.get(uiCode), { ... args }); } else {return (<> developing </>)}}}Copy the code

The basic control here shows just a single line of input, accepts the value of the property and passes the onChange method, and the Form value is collected by the Form. SingleInput. Js:

import React from 'react' import { Input } from 'antd'; Const SingleInput = (props, ref) => {const {value, onChange, formItemProps: {placeholder = 'input'} = {}} = props; return ( <Input style={{ width: "100%" }} placeholder={placeholder} value={value} onChange={(e) => { onChange(e.target.value); }} /> )} export default React.forwardRef(SingleInput);Copy the code

Three. How to use

The component code has been written, and it is relatively simple to use. The component request method is called to request asynchronous data, and the paging search method has been implemented inside the component. The type of columns field can be configured to realize the current field control search, and the operation in the row can also obtain the internal status value and method of the component from the action:

<ProTable title={" sample table "} description={" This is a sample table with query pagination, no logical code to write, } rowKey="id" ref={(ref) => (this.actions = ref)} request={(params) => {return new Promise((resolve, reject) => { getAction("/api/demoList", params).then((res) => { resolve({ data: res.data, total: res.total, }); }); }); }} columns={[{dataIndex: "name", title: "name", type: "input",}, {dataIndex: "sex", title: "sex", type: "Select ", formItemProps: {options: [{value: false, label:" female ",}, {value: true, label: "Male",},],,}}, {dataIndex: "age", the title: "age", sorter: true}, {dataIndex: "birthday", the title: "birthday", type: 'datePicker'}, {title: "", width: 100, render: (text, record) => (<div> <a onClick={() => {}> Edit </a> <Divider type="vertical" /> <Popconfirm title={" Do you want to delete this item? "} onConfirm={() => {console.log(" cancelText=" cancelText=" cancelText "> <a style={{color: RGB (51, 51, 51); "Red"}}> Delete </a> </Popconfirm> <Divider type="vertical" /> <Dropdown overlay={<Menu> < menu. Item key="1"> <a onClick={() = > {}} > view < / a > < / Menu Item > <. Menu Item key = "2" > < a onClick = {() = > {}} > assign < / a > < / Menu Item > > < < Menu >} a onClick = {(e) => e.preventDefault()} more </a> </Dropdown> </div>),},]} toolBarRender={(actions, {selectedRowKeys, SelectedRows}) = > (< > < Button type = "primary" onClick = {() = > {}} > new < / Button > < / a >)} rowSelection = {() = > {}} / >Copy the code

4. To summarize

Here is an example of the use of a component from the hooks version, with the more complete use code. The project uses a DVA and mock environment to simulate requests for data, hoping to provide some help.

The level is limited, if some mistakes do not know, please also hope that you correct.