What is the correct way to implement a sortable table using React?

My current implementation:

export const SORT_ORDER = {
    ASC: "ascending",
    DESC: "descending",
    OTHER: "other"
};

export default class Table extends React.Component {

    constructor(props) {
        super(props);

        this.sortIcon = new Map();
        this.sortIcon.set(SORT_ORDER.ASC, sortAsc);
        this.sortIcon.set(SORT_ORDER.DESC, sortDesc);
        this.sortIcon.set(SORT_ORDER.OTHER, sortOther);

        this.state = {
            sortField: this.props.defaultSortColumn,
            sortOrder: this.props.defaultSortOrder
        };
    }

    componentDidMount() {
        this.sort(this.props.defaultSortColumn, this.props.defaultSortOrder)();
    }

    retrieveOrder = (columnId) => {
        return columnId === this.state.sortField ? this.state.sortOrder : SORT_ORDER.OTHER;
    };

    nextOrder = (current) => {
        if (current === SORT_ORDER.DESC) {
            return SORT_ORDER.ASC;
        } else if (current === SORT_ORDER.ASC) {
            return SORT_ORDER.DESC;
        } else {
            return this.props.defaultSortOrder;
        }
    };

    sort = (columnId, order) => () => {
        let descriptor = this.props.structure[columnId];
        let values = this.props.value.slice();

        let orderFactor = order === SORT_ORDER.ASC ? 1 : -1;
        values.sort((a, b) => {
            let first = descriptor.render(a);
            let second = descriptor.render(b);
            return first > second ? orderFactor : first < second ? -orderFactor : 0;
        });

        this.setState({
            sortField: columnId,
            sortOrder: order
        });
        this.props.onSort(values);
    };

    renderHeader = (id, descriptor) => {
        let order = this.retrieveOrder(id);
        let iconSrc = this.sortIcon.get(order);
        let nextOrder = this.nextOrder(this.retrieveOrder(id));
        return (
            <th key={id} className={descriptor.headStyle}>
                <a href="#" aria-sort={order} onClick={this.sort(id, nextOrder)}>
                    <img src={iconSrc}/>
                    {descriptor.label}
                </a>
            </th>
        );
    };

    render()  {
        return (
            <table>
                Table structure
            </table>
        );
    }
}

      

The parent component declares it like this:

<Table structure={this.tableHeader} value={this.state.tableValue} onSort={this.handleChange('tableValue')}
     defaultSortColumn="created" defaultSortOrder={SORT_ORDER.DESC} />

      

The table value is defined in the attributes as value

. onSort

is a function that changes the state of the parent component => changes the table value. Also I have defaultSortColumn

and defaultSortOrder

for the sorting of the table after its completion.

The problem is that my table can be declared multiple times per page.

So,

1) I am unable to store the table value in my state. Should I?

2) How can I implement the default sort without using it componentDidMount

? With the current implementation, the default sort only happens once when called componentDidMount

, but I have more than 1 on the page <Table/>

. I tried to use a function componentWillReceiveProps

, but it is also called when I change the state <Table/>

in sort

. So I cannot use it.

+3


source to share


1 answer


My final solution is:

export const SORT_ORDER = {
    ASC: "ascending",
    DESC: "descending",
    OTHER: "other"
};

class TableRow extends React.Component {

    render() {
        return (
            <tr>
                {this.props.children}
            </tr>
        );
    }
}


class TableHeader extends React.Component {

    constructor(props) {
        super(props);

        this.sortIcon = new Map([
            [SORT_ORDER.ASC, {icon: sortAsc, title: "Ascending"}],
            [SORT_ORDER.DESC, {icon: sortDesc, title: "Descending"}],
            [SORT_ORDER.OTHER, {icon: sortOther, title: "Unsorted"}]
        ]);
    }

    render() {
        const {children, onClick, sortOrder} = this.props;
        return (
            <th>
               {onClick ? (
                <a href="#" aria-sort={sortOrder} onClick={onClick}>
                    <img src={this.sortIcon.get(sortOrder).icon} title={this.sortIcon.get(sortOrder).title} />
                    {children}
                </a>
            ) : children}
            </th>
        );
    }
}

export default class Table extends React.Component {

    constructor(props) {
        super(props);

        this.state = {
            sortField: props.defaultSortColumn,
            sortOrder: props.defaultSortOrder
        };
    }

    retrieveOrder = (columnId) => {
        return columnId === this.state.sortField ? this.state.sortOrder : SORT_ORDER.OTHER;
    };

    nextOrder = (current) => {
        if (current === SORT_ORDER.DESC) {
            return SORT_ORDER.ASC;
        } else if (current === SORT_ORDER.ASC) {
            return SORT_ORDER.DESC;
        } else {
            return this.props.defaultSortOrder;
        }
    };

    sortedRows = () => {
        let descriptor = this.props.structure.find(d => d.attribute === this.state.sortField);
        let values = this.props.value.slice();

        let orderFactor = this.state.sortOrder === SORT_ORDER.ASC ? 1 : -1;
        return values.sort((a, b) => {
            let first;
            let second;
            // null and undefined values should be changed to empty string
            if (typeof a[descriptor.attribute] === "number" || typeof b[descriptor.attribute] === "number") {
                first = a[descriptor.attribute] || "";
                second = b[descriptor.attribute] || "";
            } else {
                first = descriptor.render(a) || "";
                second = descriptor.render(b) || "";
            }
            return first > second ? orderFactor : first < second ? -orderFactor : 0;
        });
    };

    renderHeaders = () => {
        return this.props.structure.map((descriptor, id) => {
            let header;
            if (this.props.sortable) {
                const order = this.retrieveOrder(descriptor.attribute);
                const nextOrder = this.nextOrder(order);
                header = (
                    <TableHeader key={id} onClick={() => {this.setState({sortField: descriptor.attribute, sortOrder: nextOrder})}}
                        sortOrder={order}>
                        {descriptor.label}
                    </TableHeader>
                )
            } else {
                header = (
                    <TableHeader key={id}>
                        {descriptor.label}
                    </TableHeader>
                )
            }
            return header;
        });
    };

    renderRows = () => {
        const Row = this.props.customRow || TableRow;
        const values = this.props.sortable ? this.sortedRows() : this.props.value;
        return values.map((value, idx) => (
            <Row key={idx} value={value}>
                {this.props.structure.map((descriptor, id) => (
                    <td key={id}>
                        descriptor.render(value, idx)
                    </td>
                ))}
            </Row>
        ));
    };

    render()  {
        return (
            <table className={this.props.className}>
                <thead>
                    <tr>
                        {this.renderHeaders()}
                    </tr>
                </thead>
                <tbody>
                    {this.renderRows()}
                </tbody>
            </table>
        );
    }
}

      

An example of using a table:



            this.tableStructure = [
                {
                    attribute: "number", label: "Row Number"
                    render: (row) => {return row.number}
                },
                {
                    attribute: "created", label: "Creation time"
                    render: (row) => {return this.dateToString(row.created)}
                },
                {
                    attribute: "type", label: "Row Type"
                    render: (row) => {return row.type}
                },
                {
                    attribute: "state", label: "State",
                    render: (row) => {return row.state}
                },
                {
                    attribute: "action", label: "Action"
                    render: (row) => {
                            return (
                                <button onClick={this.doSomething}>
                                </button>
                            );
                    }
                }
            ];

            <Table structure={this.tableStructure} value={this.state.someValue} sortable
                                        defaultSortColumn="created" defaultSortOrder={SORT_ORDER.DESC} />

      

The implementation is based on http://styleguide.cfapps.io/react_base_tables.html

0


source







All Articles