Drag and Drop Interface

Implement a basic drag and drop interface where users can move items between different containers.

Solution

Move Me!
Move Me!

Explanation

JS Drag & Drop API

The solution mimics the pattern for vanilla JavaScript with the exception that it uses state and refs to keep track of the elements.

The HTML structure

We have 2 main div containers, the first of which has two items. The 2 parent containers have the properties onDrop and onDragOver and a ref to make sure we can access which dropZone we are targetting.

The items currently placed in the first container have the properties draggable and onDragStart. draggable tells the API that the element can be dragged. onDragStart allows us to know which element is being dragged.

dragStartHandler

When the draggable item begins to be moved, this triggers a drag event. The dragStartHandler function takes the current property of the ref and tells us which item is being dragged. This is stored in state as draggedItem.

dragOverHandler

This function is present to allow dragging and provide the animation.

dropHandler

Now to drop the item, the dropHandler function establishes if we are over a box by looking for the box style. Then if that is true and we have a draggedItem (which we do), then it will append the draggedItem to the box.

Code

import { useRef, useState } from "react";
import styles from "./Solution.module.css";

const Solution = () => {
  const dragItem1 = useRef<HTMLDivElement>(null);
  const dragItem2 = useRef<HTMLDivElement>(null);
  const dropZone1 = useRef<HTMLDivElement>(null);
  const dropZone2 = useRef<HTMLDivElement>(null);

  const [draggedItem, setDraggedItem] = useState<HTMLDivElement | null>(null);

  const dragStartHandler = (event: React.DragEvent<HTMLDivElement>) => {
    // Set the dragged item based on which element initiated the drag
    if (event.target === dragItem1.current) {
      setDraggedItem(dragItem1.current);
    } else if (event.target === dragItem2.current) {
      setDraggedItem(dragItem2.current);
    }
  };

  const dropHandler = (event: React.DragEvent<HTMLDivElement>) => {
    // required to allow dragging
    event.preventDefault();
    // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
    const dropZone = (event.target as HTMLElement).closest(`.${styles.box/}`); // Ensures we're dropping into a box
    if (dropZone && draggedItem) {
      dropZone.appendChild(draggedItem);
    }
  };

  const dragOverHandler = (event: React.DragEvent<HTMLDivElement>) => {
    // required to allow dragging
    event.preventDefault();
    event.dataTransfer.dropEffect = "move";
  };

  return (
    <>
      <div className={styles.container}>
        <div
          className={styles.box}
          onDrop={dropHandler}
          onDragOver={dragOverHandler}
          ref={dropZone1}
        >
          <div
            ref={dragItem1}
            draggable="true"
            onDragStart={dragStartHandler}
            className={`${styles.dragItem} ${styles.one}`}
          >
            Move Me!
          </div>
          <div
            ref={dragItem2}
            draggable="true"
            onDragStart={dragStartHandler}
            className={`${styles.dragItem} ${styles.two}`}
          >
            Move Me!
          </div>
        </div>
        <div
          className={styles.box}
          onDrop={dropHandler}
          onDragOver={dragOverHandler}
          ref={dropZone2}
        ></div>
      </div>
    </>
  );
};

export default Solution;

Styling

.container {
  display: flex;
  gap: 15px;
}

.box {
  display: flex;
  gap: 15px;
  width: 300px;
  height: 300px;
  padding: 10px;
  border: 1px solid black;
}

.dragItem {
  width: 100px;
  height: 100px;
  display: grid;
  place-content: center;
  cursor: pointer;
}

.one {
  background-color: rgb(246, 168, 168);
}

.two {
  background-color: rgb(168, 246, 182);
}

Links