Drag sorting Ant Design Table with dnd kit

Introduction

I was trying to implement Drag sorting with handler for Ant Design Table. I found that react-sortable-hoc is not going to be enhancement further and the author encourages to use dnd kit. This article summarizes what I did to implement it with dnd kit.

Prepare dummy data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function App() {
const columns = [
{
key: "dragHandle", dataIndex: "dragHandle", title: "Drag",
width: 30,
render: () => <MenuOutlined />,
},
{
key: "key", dataIndex: "key", title: "Key",
},
];

const dataSourceRaw = new Array(5).fill({}).map((item, index) => ({
// This will be transformed into `data-row-key` of props.
// Shall be truthy to be draggable. I don't know why.
// To this end, index of number type is converted into string.
key: index.toString(),
}));
const [dataSource, setDataSource] = useState(dataSourceRaw);

return (
<Table
columns={columns}
dataSource={dataSource}
/>
);

The important thing here is that the key used to identify an item must be truthy value. I struggled with a situation that the first item is not draggable. After some minutes of debugging, I found that if the key is falsy value, it is not draggable. But I couldn’t find the root cause for this.

Define a state variable for drag overlay

1
2
// ID to render overlay.
const [activeId, setActiveId] = useState(null);

activeId will be used for determining whether to render drag overlay or not.

Convert Table into sortable preset of dnd kit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);

return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<Table
columns={columns}
dataSource={dataSource}
components={{
body: {
wrapper: DraggableWrapper,
row: DraggableRow,
},
}}
/>
{/* Render overlay component. */}
<DragOverlay>{activeId ? activeId : null}</DragOverlay>
</DndContext>
);

According to sortable single container, enclose Table in DndContext, wrap body (tbody) with SortableContext and implement useSortable in row (tr). In this example, DraggableWrapper implements SortableContext and DraggableRow implements tr with useSortrable.

DragOverlay is rendered only when activeId has a valid ID value.

DraggableWrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function DraggableWrapper(props: any) {
const { children, ...restProps } = props;
/**
* 'children[1]` is `dataSource`
* Check if `children[1]` is an array
* because antd gives 'No Data' element when `dataSource` is an empty array
*/
return (
<SortableContext
items={children[1] instanceof Array ? children[1].map((child: any) => child.key) : []}
strategy={verticalListSortingStrategy}
{...restProps}
>
<tbody {...restProps}>
{
// This invokes `Table.components.body.row` for each element of `children`.
children
}
</tbody>
</SortableContext>
);
}

DraggableWrapper implements SortableContext. items shall be a list of keys to identify items, not items themselves. Inside tbody, children is a list of rows and each item will invoke Table.components.body.row, which is DraggableRow.

DraggableRow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function DraggableRow(props: any) {
const { attributes, listeners, setNodeRef } = useSortable({
id: props["data-row-key"],
});
const { children, ...restProps } = props;
/**
* 'children[1]` is a row of `dataSource`
* Check if `children[1]` is an array
* because antd gives 'No Data' element when `dataSource` is an empty array
*/
return (
<tr
ref={setNodeRef}
{...attributes}
{...restProps}
>
{
children instanceof Array ? (
children.map((child: any) => {
const { children, key, ...restProps } = child;
return key === "dragHandle" ? (
<td {...listeners} {...restProps}>
{child}
</td>
) : (
<td {...restProps}>{child}</td>
);
})
) : (
children
)
}
</tr>
);
}

DraggableRow implements tr with useSortable, where id must be the same with a key of each item. We want to make a row draggable, so assign setNodRef to tr. And we want to enable dragging only when a user grabs a drag handle, so assign listeners to td containing a drag handle.

handleDragStart

1
2
3
4
function handleDragStart(event: any) {
const { active } = event;
setActiveId(active.id);
}

handleDragEnd

1
2
3
4
5
6
7
8
9
10
11
12
13
function handleDragEnd(event: any) {
const { active, over } = event;
if (active.id !== over.id) {
setDataSource((items) => {
// In this example, find an item, where `item.key` === `useSortable.id`.
const oldIndex = items.findIndex((item) => item.key === active.id);
const newIndex = items.findIndex((item) => item.key === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
// Stop overlay.
setActiveId(null);
}

handleDragEnd performs swapping two items. Here, to find indexes of two items in data source, we need to compare key of an item and id of useSortable.

You can find demo here.