@dnd-kit/react source and target in sortable list always the same in onDragEnd handler
When composing a multiple sortable list and handling the onDragEnd event, both operation.source and operation.target are always set to the currently dragged item when re-arranging the order of a parent list (i.e.: moving the position of a kanban column).
When dragging the column, the target is only ever the correct one when you are dragging directly over the top of the target element itself.
When @dnd-kit/react automatically re-positions other columns in the sortable list to "preview" where the dragged column will be dropped, it causes the target to be subsequently set to the currently dragged column when there's no column "beneath" it besides the "ghost column".
This makes it near enough impossible to trigger a change such as updating a column's position in an external DB:
const handleDragEnd = (cancelled: boolean, operation: KanbanDragEvent<TGroup, TItem>) => {
if (!cancelled && operation.source && operation.target) {
const { source, target } = operation
switch (source.type) {
case 'column':
props.onGroupReorder(source.data, target.data)
break
case 'item':
props.onItemMove(source.data, target.data)
break
}
}
}
Reproduction
I've created a reproduction of this issue by forking a minimal version of the multiple sortable lists example on CodeSandbox. View the reproduction on CodeSandbox
I'm facing the same issue even in a Sortable list.
This is because the DOM gets optimistically updated by @dnd-kit by default. If you would like to manage DOM and state manually, you can call event.preventDefault() in onDragOver
<DragDropProvider
onDragOver={(event) => {
event.preventDefault();
}}
Even if dnd-kit automatically handles the transition optimistically, how do we access the resulting index within onDragEnd @clauderic? My use-case is essentially that we need to retrieve the new index and send it to the B/E for persistence.
One of the issues I continue to run into is that if I track the last dragged over item via onDragOver, and move an item in position N, to N+1 and then back to N, all without dropping the item, the last dragged over item will still be N+1, and therefore I cannot accurately determine what position the resulting drop occurred at.
There could potentially be a solution to this. If I clone the columns and items arrays, then have dnd-kit optimistically update the cloned arrays within onDragOver. Then inside of onDragEnd I can determine the new position based on the cloned array, or roll back if event.cancelled.
You can read the index and group on start and end:
event.source.sortable.index and event.source.sortable.group
Makes sense @clauderic, thanks for clarifying, I'll post my updated implementation here shortly incase anyone else experiences something similar.
One last thing though, what would the correct generics be for DragOperation<T, U> then when using Sortable? From what I can see Sortable doesn't actually extend Draggable which is what the generics expect, rather it's a container that houses both a Draggable and a Droppable element. However, source and target appear to both expect Draggable or Droppable as their value.
You can import the isSortable type guard from @dnd-kit/dom/sortable
The isSortable type-guard is not publicly exposed by the package, but using a type-guard would be ideal.
It is exported: https://github.com/clauderic/dnd-kit/blob/experimental/packages/dom/src/sortable/index.ts#L3
You have to import it from @dnd-kit/dom/sortable but I will make a note to re-export it from @dnd-kit/react/sortable
Maybe I'm just being obtuse here, but TS doesn't seem to think it's exported, and when looking inside of the built JS files in v0.0.5, I cannot see the function being re-exported as part of sortable.js
Usage
sortable.js
From what I can see, it appears as though the type-guard is being inlined during the build and is not actually being exported by the API of @dnd-kit/dom/sortable
Oh, it must only exported in the nightly builds, I have not merged 0.0.6 yet.
Try installing one of the nightly builds, the latest one is 0.0.6-beta-20241204184550
Ah yes nice one, it's present in the nightly build 🚀 May also make sense to export SortableDraggable and SortableDroppable for people to use in their DragOperation generics on custom handlers.
Related to this, I was trying to import the DragEndEvent to create my own handler but I cannot find its export anywhere. This type is declared in next docs:
What is the correct way to import/create it? Since I couldn't import it, I have create a type as follows:
export type DragEndEvent = {
operation: DragOperation<Draggable<Data>, Droppable<Data>>;
canceled: boolean;
suspend(): {
resume(): void;
abort(): void;
};
};
But it looks like the isSortable guard doesn't have the same type for its parameters...
I'm importing
isSortablefrom@dnd-kit/dom/sortablesince is not exported from@dnd-kit/react/sortablev0.0.8.
Any help? Thank you!
(TL;DR: Apparently @dnd-kit/react is way too new to use.)
Even though evt.operation.source.sortable appears when running...
It's also not public:
Even worse, I just tried inspecting source.sortable.index and target.sortable.index and they're still the same. So, problem not solved. Is there actually a way to track the order of items as they are reordered? This seems pretty mission critical.
p.s. This is on v0.0.9
I also have this problem..
I needed nested draggable/droppable areas. Since this is broken, I reverted back to the old @dnd-kit/core / @dnd-kit/utilities packages, re-developed my code to stop using @dnd-kit/sortable, and used the raw elements instead - <DndContext>, useDndContext, useDraggable, useDroppable.
I followed this helpful discussion pretty far (not even all the way), and was able to get it working fairly decently.
TL;DR: Don't use @dnd-kit/react. It's still in beta. If you need nested dnd, you'll probably need to implement it manually without useSortable. But the raw pieces still provide a decent / fairly intuitive interface to do so.
Will this be fixed at any time soon? or any progress on @dnd-kit/react
Will this be fixed at any time soon? or any progress on @dnd-kit/react
https://github.com/clauderic/dnd-kit/issues/1664
First, Thanks @clauderic I have a solution, maybe you can refer it:
const [mapping, setMapping] = useState<string[]>([])
const [mappingSnapshot, setMappingSnapshot] = useState<string[]>([])
...
const handleBeforeDragStart: DragDropEvents['beforedragstart'] = (e) => {
console.log('before drag start:', e)
// save snapshot
setMappingSnapshot(mapping)
}
const handleDragOver: DragDropEvents['dragover'] = (e) => {
console.log('drag over event:', e)
setMapping((mapping) => move(mapping, e))
}
const handleDragEnd: DragDropEvents['dragend'] = (e) => {
console.log('drag end event:', e)
if (e.canceled) {
setMapping(mappingSnapshot)
return
}
const sourceId = e.operation.source?.id
const targetId = e.operation.target?.id
if (!sourceId || !targetId) {
setMapping(mappingSnapshot) // restore
return
}
const sourceItem = siteMap[sourceId]
const originIndex = mappingSnapshot.indexOf(sourceId as string) // !!! Find origin index from snapshot
const currentIndex = mapping.indexOf(sourceId as string)
if (!sourceItem || originIndex == -1 || currentIndex == -1) {
return
}
if (originIndex === currentIndx) {
// no change position
return
}
const positionId = mappingSnapshot[currentIndex]
const positionItem = siteMap[positionId]
...
}
Because of this issue, I forced the DragDropProvider to use the key based on the list ID in the database. It's a method I really hate, but I can't seem to find a solution. A solution would be incredibly helpful.
import { arrayMove } from '@dnd-kit/helpers'
const handleDragEnd = (event, manager) => {
const { operation, canceled } = event
const { source, target } = operation
if (canceled) {
console.log(`Cancelled dragging ${source?.id}`)
return
}
if (target && isSortable(source)) {
const newIndex = source.sortable.index
const oldIndex = source.sortable.initialIndex
if (oldIndex !== newIndex) {
const newList = arrayMove(arrSource, oldIndex, newIndex)
setArrSource(newList)
}
}
Should be simple enough here.
Tested with TS for the manual control of the array source using onDragEnd. I still keep the optimistic update enabled.
Sometimes, when the source array used to render the sortable list is updated after the onDragEnd, the next sorting behavior with onDragEnd now will have source.sortable.index === source.sortable.initialIndex but target will become different from source. I think this is very likely a bug