React/AntDesign How to make rows draggable? (Table Drag Sorting)

12,381

Solution 1

Here is a simple example I came up with based off of a lot of different things I found online and eventually formulated something that worked for my code:

Basically this is the body of a react component table getting called and I am mapping a list of places via the indexes and each row is draggable to allow the user to modify the table. I have also included the function calls associated with the attributes

return (
        <Tbody>
            {items.map((item, index) => (
                    <Tr key={index} className="item.name"
                        draggable={true}
                        onDragStart={this.dragstart}
                        onDragEnd={this.dragend}
                        onDragLeave={this.dragend}
                        onDragEnter={this.dragend}
                        onDrop={this.drop}
                        onDragOver={this.dragend}
                        id={index} >
                        <Td><span>{item.name}</span></Td>
                        <Td><span>{item.latitude}</span></Td>
                        <Td><span>{item.longitude}</span></Td>
                        <Td type="data"><span>{this.checkForZero(this.props.propDistances[index])}</span></Td>
                        <Td type="data"><span>{this.checkForZero(this.props.propCumulativeDist[index])}</span></Td>
                    </Tr>
            ))}
            {this.finishItineraryTable()}
        </Tbody>
    );

dragstart(event) {
    let dataTransfer = event.dataTransfer;
    let node = event.target;
    dataTransfer.setData('text/plain',node.innerHTML);
    dataTransfer.setData('id',node.id);
    event.stopPropagation();
}

dragend(event) {
    event.preventDefault();
}

drop(event) {
    event.preventDefault();
    let dragObjHtml = event.dataTransfer.getData("text/plain");
    let dragObjId = document.getElementById(event.dataTransfer.getData("id"));
    let dropTarget = event.target.closest("tr");
    let temp = dropTarget.innerHTML;
    dropTarget.innerHTML = dragObjHtml;
    dragObjId.innerHTML = temp;
}

Solution 2

Before a paste my long blob of code, I want to mention a few things:

  • It's heavily based on the original drag sorting example.
  • Luckily React DnD has perfect TypeScript support as it's written in TypeScript.
  • The code below is a drop in replacement for Ant designs <table>.
  • It's generic.
  • It uses functional components and hooks.
  • There is a onDragSort, which gets called back every time some drag sort happened. The parameter includes the sorted data.

First install the packages react-dnd and react-dnd-html5-backend. Add this to your DragSortingTable.tsx:

import * as React from 'react';
import Table, { TableProps, TableComponents } from 'antd/lib/table';
import { DndProvider, DropTarget, DragSource, DragElementWrapper, DropTargetSpec } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';

let dragingIndex = -1;

interface BodyRowProps {
  isOver: boolean;
  connectDragSource: DragElementWrapper<{}>;
  connectDropTarget: DragElementWrapper<{}>;
  style?: React.CSSProperties;
  index: number;
  className?: string;
}

const BodyRow: React.FC<BodyRowProps> = props => {
  const style = { ...props.style, cursor: 'move' };

  let { className } = props;
  if (props.isOver) {
    if (props.index > dragingIndex) {
      className += ' drop-over-downward';
    }
    if (props.index < dragingIndex) {
      className += ' drop-over-upward';
    }
  }

  return props.connectDragSource(
    props.connectDropTarget(
      <tr className={className} style={style}>
        {props.children}
      </tr>
    )
  );
};

const rowSource = {
  beginDrag(props) {
    dragingIndex = props.index;
    return {
      index: props.index
    };
  }
};

interface DropProps {
  index: number;
  moveRow: (dragIndex: number, hoverIndex: number) => void;
}

const rowTarget: DropTargetSpec<DropProps> = {
  drop(props, monitor) {
    const dragIndex = monitor.getItem().index;
    const hoverIndex = props.index;

    // Don't replace items with themselves
    if (dragIndex === hoverIndex) {
      return;
    }

    // Time to actually perform the action
    props.moveRow(dragIndex, hoverIndex);

    // Note: we're mutating the monitor item here!
    // Generally it's better to avoid mutations,
    // but it's good here for the sake of performance
    // to avoid expensive index searches.
    monitor.getItem().index = hoverIndex;
  }
};

const DragableBodyRow = DropTarget<DropProps>('row', rowTarget, (connect, monitor) => ({
  connectDropTarget: connect.dropTarget(),
  isOver: monitor.isOver()
}))(
  DragSource('row', rowSource, connect => ({
    connectDragSource: connect.dragSource()
  }))(BodyRow)
);

interface DragSortingTableProps<T> extends TableProps<T> {
  onDragSort?: (sortedData: T[]) => void;
}

type extractTableType<T> = T extends DragSortingTableProps<infer T> ? T : never;

export const DragSortingTable: <T>(
  props: DragSortingTableProps<T>
) => React.ReactElement<DragSortingTableProps<T>> = props => {
  const [dataSource, setDataSource] = React.useState(props.dataSource);

  React.useEffect(() => {
    setDataSource(props.dataSource);
  }, [props.dataSource]);

  const components: TableComponents = {
    body: {
      row: DragableBodyRow
    }
  };

  const moveRow: DropProps['moveRow'] = (dragIndex, hoverIndex) => {
    const dragRow = dataSource[dragIndex];
    const remaining = dataSource.filter(i => i !== dragRow);
    const sorted = [...remaining.slice(0, hoverIndex), dragRow, ...remaining.slice(hoverIndex)];

    setDataSource(sorted);

    if (props.onDragSort) {
      props.onDragSort(sorted);
    }
  };

  const tableProps: TableProps<extractTableType<typeof props>> = {
    ...props,
    className: props.className ? props.className + ' drag-sorting-table' : 'drag-sorting-table',
    components,
    dataSource,
    onRow: (_record, index) => ({ index, moveRow })
  };

  return (
    <DndProvider backend={HTML5Backend}>
      <Table {...tableProps}>{props.children}</Table>
    </DndProvider>
  );
};

Add the following styles to your antd.less overwrites:

.drag-sorting-table tr.drop-over-downward td {
  border-bottom: 2px dashed @primary-color;
}

.drag-sorting-table tr.drop-over-upward td {
  border-top: 2px dashed @primary-color;
}

Use your new drag sorting table as follows:

<DragSortingTable<Users>
      dataSource={props.users}
      rowKey='id'
    >
      <Column
        title='Title'
        dataIndex='id'
        key='id'
      />
</DragSortingTable>
Share:
12,381
Adrián Méndez
Author by

Adrián Méndez

Updated on June 11, 2022

Comments

  • Adrián Méndez
    Adrián Méndez almost 2 years

    I have been coding this simple one-column drag and drop table, but the rows aren´t moving when I drag them. Where do I fix or check that?

    I'm using React, AntDesign and JavaScript (with TypeScript)

    import * as React from 'react';
    
    import ReactDOM from "react-dom";
    
    import { Table } from "antd";
    
    import { DndProvider, DragSource, DropTarget } from "react-dnd";
    
    import HTML5Backend from "react-dnd-html5-backend";
    
    import update from "immutability-helper";
    
    let dragingIndex = -1;
    
    interface propsDD {
        isOver: any,
        connectDragSource: any,
        connectDropTarget: any,
        moveRow: any,
        restProps: {
            readonly [x: string]: any;
            children?: React.ReactNode;
        }
        className: any,
        index: any,
    }
    
    class BodyRow extends React.Component<propsDD>{
        render() {
            const { isOver, connectDragSource, connectDropTarget, moveRow, ...restProps } = this.props;
            const style = { ...restProps, cursor: 'move' };
            let { className } = restProps;
            if (isOver) {
                if (restProps.index > dragingIndex) {
                    className += " drop-over-downward";
                }
                if (restProps.index < dragingIndex) {
                    className += " drop-over-upward";
                }
            }
            return connectDragSource(
                connectDropTarget(
                    <tr {...restProps} className={className} style={style} />
                )
            );
        }
    }
    
    const rowSource = {
        beginDrag(props: any) {
            dragingIndex = props.index;
            return {
                index: props.index,
            };
        },
    };
    
    const rowTarget = {
        drop(props: any, monitor: any) {
            const dragIndex = monitor.getItem().index;
            const hoverIndex = props.index;
            if (dragIndex === hoverIndex) {
                return;
            }
            props.moveRow(dragIndex, hoverIndex);
            monitor.getItem().index = hoverIndex;
        },
    };
    
    const DragableBodyRow = DropTarget("row", rowTarget, (connect, monitor) => ({
        connectDropTarget: connect.dropTarget(),
        isOver: monitor.isOver()
    }))(
        DragSource("row", rowSource, connect => ({
            connectDragSource: connect.dragSource()
        }))(BodyRow)
    );
    
    const columns = [
        {
            title: 'Orden de Ejecución',
            dataIndex: 'attributes.name',
            key: 'name',
        },
    ];
    
    type propsFromList = {
        receivedTasks: Task[],
        onReceivedTasks: (tasks: Task[]) => void,
    }
    
    export default class DDTasks extends React.Component<propsFromList, State>{
        public state: State = {
            data: [],
        };
    
        components = {
            body: {
                row: DragableBodyRow,
            },
        };
    
        onReceivedTasks(tasks: Task[]): void {
            this.setState({
                data: this.props.receivedTasks,
            } as State)
        }
    
        moveRow = (dragIndex: any, hoverIndex: any) => {
            const { data } = this.state;
            const dragRow = data[dragIndex];
            this.setState(
                update(this.state, {
                    data: {
                        $splice: [[dragIndex, 1], [hoverIndex, 0, dragRow]]
                    }
                })
            );
        };
    
        render() {
            return (
                < DndProvider backend={HTML5Backend} >
                    <Table
                        rowKey="id"
                        bordered={true}
                        pagination={false}
                        columns={columns}
                        dataSource={this.props.receivedTasks}
                        components={this.components}
                        onRow={(index) => ({
                            index,
                            moveRow: this.moveRow,
                        })}
                    />
                </DndProvider >
            );
        }
    }
    

    I expect to drag rows. The content of the rows are actually displayed.

  • Adrián Méndez
    Adrián Méndez over 4 years
    Yes, I have already added the CSS style, as you said. In previous tests, the dragging effect showed, however, in this version doesn´t.
  • Vipin Yadav
    Vipin Yadav over 4 years
    Which version of antd you are using can you update your antd version and then check
  • Adrián Méndez
    Adrián Méndez over 4 years
    I´m not really sure about which AntD version I´m using, however, here is the original source code I´m trying to replicate: ant.design/components/table/#components-table-demo-drag-sort‌​ing