A question often asked of developers building an application front end, web or otherwise is, "Can you add drag and drop to this?"
This guide will use component composition to create two components: one to add drag capabilities to a React component and one to turn a React component into a drop target.
The first component to create is the drag component. This component will be a container component that will enable dragging on its children. It will accept a single prop, dataItem
, as an identifier for the data being dragged and will be consumed like this:
1<Drag dataItem="item-1">
2 <div>Something to be dragged</div>
3</Drag>
The component will render the children inside a div
element like this:
1<div>
2 {props.children}
3</div>
To enable dragging on the component we need to do two things. First, we need to set the draggable
attribute on the element, and second, handle the onDragStart
event. In this handler we should call event.dataTransfer.setData
to set the data that can be used in a drop target to identify what has been dropped. In this component the data will be whatever has been passed in as the dataItem
prop.
The component should now look like this:
1function startDrag(ev) {
2 ev.dataTransfer.setData("drag-item", props.dataItem);
3}
4
5return(
6 <div draggable onDragStart={startDrag}>
7 {props.children}
8 </div>);
The children can now be dragged and identified using the drag-item
data on the dataTransfer
object.
We now have a component that can be dragged but nowhere to drop it, so we will create a drop target component. This component will, again, be a container component that wraps its children in a div
element; this component will have a single prop of an event handler that will be called when an item has been dropped inside it.
It will be consumed like this:
1<DropTarget onItemDropped={itemDropped}>
2 <div>...</div>
3</DropTarget>
To enable drop on the component, we need to handle two events: onDragOver
and onDrop
. The default drag over behavior of an element is to disable dropping, so in order to allow dropping the handler needs to prevent this default behavior by calling event.preventDefault()
. Drop will now be enabled on the component. The handler for the drop event should call event.dataTransfer.getData("drag-item")
to retrieve the identifier of the item being dropped and then call its onItemDropped
handler.
The drop target component will look like this:
1function dragOver(ev) {
2 ev.preventDefault();
3}
4
5function drop(ev) {
6 const droppedItem = ev.dataTransfer.getData("drag-item");
7 if (droppedItem) {
8 props.onItemDropped(droppedItem);
9 }
10}
11
12return (
13 <div onDragOver={dragOver} onDrop={drop}>
14 {props.children}
15 </div>);
Components can now be wrapped in the Drag component and dropped in the DropTarget component.
Currently, the drop target will allow anything to be dropped on it, and the drag cursor is always the same. These can be controlled using drop effects. The available effects include copy, move, link, and any combination of them. The effect for the object being dragged is set using event.dataTransfer.effectAllowed
in the start drag handler, and the effect for the drop target is set using event.dataTransfer.dropEffect
in both the drag over handler and the drag enter handler, onDragEnter
. Setting these properties will have the effect of both changing the drag cursor and controlling whether an item can be dropped on a particular target. We will now extend the components to implement drop effects.
First, we will add an optional prop of type string called dropEffect
to the Drag component and, in the start drag handler, set the effectAllowed
to the value of the prop.Since this prop will be optional, we will use the defaultProps
static property to give it a default value of 'all', meaning it will support all three effects. The main part of the Drag component will now look like this:
1function startDrag(ev) {
2 ev.dataTransfer.setData("drag-item", props.dataItem);
3 ev.dataTransfer.effectAllowed = props.dropEffect;
4}
5
6return(
7 <div draggable onDragStart={startDrag}>
8 {props.children}
9 </div>);
We will add an identical prop to the DropTarget component and set the dropEffect
to the value of the prop in the drag enter and drag over handlers. The DropTarget component will now look like this:
1function dragOver(ev) {
2 ev.preventDefault();
3 ev.dataTransfer.dropEffect = props.dropEffect;
4}
5
6function dragEnter(ev) {
7 ev.dataTransfer.dropEffect = props.dropEffect;
8}
9
10function drop(ev) {
11...
12}
13
14return (
15 <div onDragOver={dragOver} onDragEnter={dragEnter} onDrop={drop}>
16 {props.children}
17 </div>);
Valid values for drop effects are 'copy', 'link', 'move', 'copyMove', 'copyLink', 'linkMove', 'all', and 'none'. The meaning of these should be fairly clear, but in order to help consumers of the components these should be declared as constants, thus ensuring that if the dropEffect
prop is set using the constants then we are definitely using a valid value. In the sample code for this guide, the values are declared in a separate module, like so:
1export const All = "all";
2export const Move = "move";
3export const Copy = "copy";
4export const Link = "link";
5export const CopyOrMove = "copyMove";
6export const CopyOrLink = "copyLink";
7export const LinkOrMove = "linkMove";
8export const None = "none";
So if, for instance, say that the dropEffect
prop on the Drag component is set to dropEffects.Move
and on the DropTarget component dropEffects.Copy
drop will be disabled. If the DropTarget is set to dropEffects.CopyOrMove
, then drop is enabled.
We now have fully functioning drag and drop available. However, there are other techniques that can be used to improve the interface. In this section we will add an image to display when dragging and add styling to indicate when an element is being dragged and when a target is available.
At the moment, the image displayed when dragging is the default one for the browser, usually an opaque copy of the element being dragged. This image can be set to any element using the event.transferData.setDragImage
function.
We will use an image for the Drag component and add an optional string prop of dragImage
to contain the source of the image to display. If this prop is not set, we will use the default. In order to ensure the image is loaded before being used, we will add an effect hook to fire when the image prop is changed that will create and load the image into a ref like this:
1const image = React.useRef(null);
2React.useEffect(() => {
3 image.current = null;
4 if (props.dragImage) {
5 image.current = new Image();
6 image.current.src = props.dragImage;
7 }
8}, [props.dragImage]);
This can then be used in the onDragStart
handler like this:
1if (image.current) {
2 ev.dataTransfer.setDragImage(image.current, 0, 0);
3}
We can also use the onDragEnd
, onDragEnter
and onDragLeave
events when in a drag and drop operation.
The Drag component will handle the drag end event to help indicate to the user when a particular element is being dragged. We will add a state of isDragging
that will be used when rendering the component and set an opacity of 0.25 when it is true
. This state will be set to true
in the start drag handler and false
in the end drag handler; this way, when a user has dragged an element, it will appear opaque, and otherwise will appear normal.
The onDragEnter
component is fired when an item being dragged first goes into an element, and onDragLeave
is fired when it leaves the element. The DropTarget component will add a state of isOver
and handle these events, setting it to true
on enter and false
on exit. Rendering this state will control the background color and opacity in order to indicate to the user that they are dragging over the target.
The composition model in React allows us to write single, reusable components that can add drag-and-drop effects to any component. A sample application using the components can be found here.