By default, drop targets will allow
any draggable
to be dropped on it, and
monitors will listen to all drag
operations.
You can limit what types of draggables
can be dropped on a drop target by using canDrop()
, and
what drag operations a monitor will listen to using canMonitor()
.
This page explores how you can limit what draggables can be dropped on drop targets, and limit what drag operations a monitor will listen to.
Something to keep in mind is that the drop target
canDrop()
function is called
repeatedly throughout a drag operation, whereas the monitor
canMonitor()
function is only called
once at the start of a drag (or when a monitor is mounted, if it is mounted during a drag).
A common pattern is to allow only allow dropping / listening when a draggable is of a particular
"type"
// bind our draggable
draggable({
element: myDraggableElement,
getInitialData: () => ({
// the id of our card
cardId,
// the id the column belongs to
columnId,
// specifying this is a "card"
type: 'card',
}),
});
dropTargetForElements({
element: myDropTargetElement,
getData: () => ({ columnId }),
// only allow dropping if a "card" is being dragged
canDrop: ({ source }) => source.data.type === 'card',
});
monitorForElements({
// only listen for drag operations of "card" draggables
canMonitor: ({ source }) => source.data.type === 'card',
});
If you want to make it so that a "card"
can only be dropped inside of its home list, you could add
a further check:
// only allow dropping of tasks that started inside this column
dropTargetForElements({
element: myDropTargetElement,
getData: () => ({ columnId }),
canDrop: ({ source }) => source.data.type === 'card' && source.data.columnId === columnId,
});
// only listen for drags of cards that started inside a column with columnId
monitorForElements({
canMonitor: ({ source, initial }) =>
source.data.type === 'card' && source.data.columnId === columnId,
});
There might be cases where you want to embed the same experience multiple times on the same page,
and you want to ensure that these experiences do not impact each other. The above approaches would
work if each entity on the page has a unique id (eg a unique columnId
), but sometimes the same
logical entity (eg "card"
) might appear in multiple places.
In these cases, you can give an experience (eg a "board") a unique identitier and then check against
that identifier. A unique identifier could be anything from a DOM node to a Symbol
dropTargetForElements({
element: myDropTargetElement,
getData: () => ({ columnId }),
canDrop: ({ source }) => {
// our previous check
if (source.data.type !== 'card' || source.data.columnId !== columnId) {
return false;
}
// our new additional check: only accept dropping of elements that are inside this list
return myDropTargetElement.contains(source.element);
},
});
dropTargetForElements({
element: myDropTargetElement,
getData: () => ({ columnId }),
canDrop: ({ source }) => {
// our previous check
if (source.data.type !== 'card' || source.data.columnId !== columnId) {
return false;
}
// our new additional check: only accept dropping if the drop target
// and the draggable are inside the same "board" element
const boardDropTargetIsIn = myDropTargetElement.closest('.board');
const boardDraggableIsIn = source.element.closest('.board');
return boardDropTargetIsIn === boardDraggableIsIn;
},
});
This is the same as "Using a shared parent", but we are using a unique react
context value
(instanceId
), rather than DOM nodes, to establish a shared parent relationship.
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
import invariant from 'tiny-invariant';
import {
draggable,
dropTargetForElements,
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
const BoardContext = createContext<Symbol | null>(null);
function Card({ cardId, columnId }: { cardId: string; columnId: string }) {
const instanceId = useContext(BoardContext);
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const element = ref.current;
invariant(element);
return draggable({
element,
getInitialData: () => ({ cardId, type: 'card', columnId, instanceId }),
});
}, [cardId, columnId, instanceId]);
return <div ref={ref}>Card: {cardId}</div>;
}
function Column({ columnId }: { columnId: string }) {
const instanceId = useContext(BoardContext);
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const element = ref.current;
invariant(element);
return dropTargetForElements({
element: element,
getData: () => ({ columnId }),
canDrop: ({ source }) => {
// our previous check
if (source.data.type !== 'card' || source.data.columnId !== columnId) {
return false;
}
// our new check: only accept dropping within this experience
return source.data.instanceId === instanceId;
},
});
}, [columnId, instanceId]);
return (
<div ref={ref}>
<Card cardId="hello" columnId={columnId} />
</div>
);
}
// each <Board/> will be isolated
export function Board() {
const [instanceId] = useState(() => Symbol('instance-id'));
return (
<BoardContext.Provider value={instanceId}>
<Column columnId="first" />
</BoardContext.Provider>
);
}