@atlaskit/pragmatic-drag-and-drop-docs
Version:
This package holds the documentation for Pragmatic drag and drop in one place. It is not intended to be consumed directly by consumers. The package is used by other tools for sharing examples
205 lines (164 loc) • 6.05 kB
text/mdx
---
title: Isolating experiences
description: How to selectively engage with what is dragging
order: 2
---
import SectionMessage from '@atlaskit/section-message';
By default, [drop targets](/components/pragmatic-drag-and-drop/core-package/drop-targets) will allow
any `draggable` to be dropped on it, and
[](/components/pragmatic-drag-and-drop/core-package/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.
<SectionMessage>
Something to keep in mind is that the drop target
[`canDrop()`](/components/pragmatic-drag-and-drop/core-package/drop-targets) function is called
repeatedly throughout a drag operation, whereas the monitor
[`canMonitor()`](/components/pragmatic-drag-and-drop/core-package/monitors) function is only called
once at the start of a drag (or when a monitor is mounted, if it is mounted during a drag).
</SectionMessage>
### Simple "type" checking
A common pattern is to allow only allow dropping / listening when a draggable is of a particular
`"type"`
```ts
// 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:
```ts
// 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`
### Using a parent child relationship
```ts
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);
},
});
```
```ts
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.
```tsx
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>
);
}
```