React/AntDesign How to make rows draggable? (Table Drag Sorting)
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;}
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>
have you added CSS of antd sortable table to show the dragging effect?
#components-table-demo-drag-sorting tr.drop-over-downward td { border-bottom: 2px dashed #1890ff;}#components-table-demo-drag-sorting tr.drop-over-upward td { border-top: 2px dashed #1890ff;}
here is the sandbox link