DEV Community

It's Just Nifty
It's Just Nifty

Posted on • Originally published at niftylittleme.com on

Adding Drag And Drop Functionality In Your Next.Js Project Without A Library

Let’s be honest, the libraries out there suck. You have the dnd kit, which is more suitable for sorted drag-and-drop lists. React DnD is not even worth looking into. Then there’s react-beautiful-dnd, which is really only for sorted lists.

And, I know, sorted lists are amazing. They’re awesome, but for that to be the only talked about use case is just sad. I mean, use your imagination. Drag-and-drop can do much more than make stupid forms.

With that being said, do we really need one of these small-minded libraries that will only hinder us later? No. Instead, let’s add drag-and-drop functionality in our Next.js projects without sticking to our pathetic options. And, adding drag-and-drop functionality in next.js is not that hard. So, let’s get started.

Unsplash Image by Christopher Gower

(Image Source)

Creating Draggable Component

We can start by creating our own draggable items. So, let’s make a components folder with a Draggable.tsx file inside. In this file, we will create a draggable item. Copy and paste this code:

import { useRef, useState } from 'react';

const DraggableItem = ({ id, children, onDragStart }: { id: string; children: React.ReactNode; onDragStart: () => void }) => {
  const itemRef = useRef(null);
  const [dragging, setDragging] = useState(false);

  const handleDragStart = (e: any) => {
    e.dataTransfer.setData('text/plain', id);
    e.dataTransfer.effectAllowed = 'move';

    setDragging(true);
    if (onDragStart) onDragStart(); 
  };

  const handleDragEnd = () => {
    setDragging(false);
    console.log(`Drag ended for item with id: ${id}`);
  };

  return (
    <div
      ref={itemRef}
      draggable
      onDragStart={handleDragStart}
      onDragEnd={handleDragEnd}
      style={{
        cursor: 'grab',
        padding: '10px',
        margin: '5px',
        backgroundColor: dragging ? '#e0e0e0' : '#f0f0f0',
        border: '1px solid #ccc',
        borderRadius: '4px',
        width: '100%',
        height: '100%',
      }}
    >
      {children}
    </div>
  );
};

export default DraggableItem;
Enter fullscreen mode Exit fullscreen mode

Creating Drop Zone

Now, we could create our drop zone. So, create a file named DropZone.tsx inside the components folder. Inside this file, we want to do a couple of things. In this tutorial, we’ll make the drop zone a grid with zoom-in and zoom-out functionality. We’ll also make sure the draggable item can still be dragged and dropped after placing it in the drop zone. So, copy and paste this code:

import { useState, useEffect } from 'react';
import DraggableItem from './Draggable';

const DropZone = ({ onDrop }: { onDrop: (id: string) => void }) => {
  const [zoomLevel, setZoomLevel] = useState(1);
  const [gridSize, setGridSize] = useState(3);
  const [grid, setGrid] = useState(Array(3 * 3).fill(null));
  const [draggedItemIndex, setDraggedItemIndex] = useState<number | null>(null);

  useEffect(() => {
    console.log(`Grid size changed: ${gridSize}`);
    setGrid((prevGrid) => {
      const newGrid = Array(gridSize * gridSize).fill(null);
      prevGrid.forEach((item, index) => {
        if (item && index < newGrid.length) {
          newGrid[index] = item;
        }
      });
      return newGrid;
    });
  }, [gridSize]);

  const handleDrop = (e: any, index: any) => {
    e.preventDefault();
    const id = e.dataTransfer.getData('text/plain');
    const newGrid = [...grid];
    if (draggedItemIndex !== null && draggedItemIndex !== index) {
      newGrid[draggedItemIndex] = null;
      newGrid[index] = id;
      setDraggedItemIndex(null);
    } else {
      newGrid[index] = id;
    }
    setGrid(newGrid);
    onDrop(id);
  };

  const handleDragOver = (e: any) => {
    e.preventDefault();
  };

  const handleDragStart = (index: any) => {
    setDraggedItemIndex(index);
  };

  const handleZoomIn = () => {
    console.log("handleZoomIn");
    const newSize = Math.max(1, Math.floor(gridSize / 1.1));
    console.log(`New grid size on zoom in: ${newSize}`);
    setZoomLevel(prevZoomLevel => prevZoomLevel + 0.1);
    setGridSize(newSize);
  };

  const handleZoomOut = () => {
    console.log("handleZoomOut");
    const newSize = Math.max(1, gridSize + 1);
    console.log(`New grid size on zoom out: ${newSize}`);
    setZoomLevel(prevZoomLevel => prevZoomLevel - 0.1);
    setGridSize(newSize);
  };

  return (
    <div>
      <div className="zoom-controls">
        <button onClick={handleZoomIn}>Zoom In</button>
        <button onClick={handleZoomOut}>Zoom Out</button>
      </div>
      <div
        style={{
          display: 'grid',
          gridTemplateColumns: `repeat(${gridSize}, 1fr)`,
          border: '2px dashed #ccc',
        }}
        className='w-full h-screen overflow-y-auto overflow-x-auto'
      >
        {grid.map((item, index) => (
          <div
            key={index}
            onDrop={(e) => handleDrop(e, index)}
            onDragOver={handleDragOver}
            style={{
              width: '100%',
              height: '100%',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              backgroundColor: item ? '#e0e0e0' : 'transparent',
              border: '1px solid #ccc',
            }}
          >
            {item ? (
              <DraggableItem id={item} onDragStart={() => handleDragStart(index)}>
                {item}
              </DraggableItem>
            ) : null}
          </div>
        ))}
      </div>
    </div>
  );
};

export default DropZone;
Enter fullscreen mode Exit fullscreen mode

Putting The Pieces Together

So, now that we have our components, let’s go to our src/app/page.tsx file and display our drop zone and draggable items:

'use client';
import { useState } from 'react';
import DraggableItem from './components/Draggable';
import DropZone from './components/DropZone';

const Home = () => {
  const [items, setItems] = useState(['item1', 'item2', 'item3']);
  const [droppedItems, setDroppedItems] = useState<string[]>([]);
  const [draggedItemIndex, setDraggedItemIndex] = useState<number | null>(null);

  const handleDragStart = (index: any) => {
    setDraggedItemIndex(index);
  };

  const handleDrop = (id: any) => {
    setDroppedItems([...droppedItems, id]);
    setItems(items.filter(item => item !== id));
  };

  return (
    <main>
      <h1>Drag and Drop Example</h1>
      <div style={{ display: 'flex', flexDirection: 'row' }}>
        {items.map((item, index) => (
          <DraggableItem key={item} id={item} onDragStart={() => handleDragStart(index)}>
            {item}
          </DraggableItem>
        ))}
      </div>
      <DropZone onDrop={handleDrop} />
      <div>
        <h2>Dropped Items:</h2>
        {droppedItems.map((item, index) => (
          <div key={index}>{item}</div>
        ))}
      </div>
    </main>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

That wraps up this tutorial on how to add drag-and-drop functionality in your Next.js project. Maybe I was a little bit too harsh in the introduction. If you want to go with a drag-and-drop library go for it. No one is stopping you. And I’m not saying that this tutorial will be the solution to your problems. Obviously, you would need to tweak this code a little or a lot to get it to work how you imagined. But, for me, creating my own drag-and-drop functionality for my project was a step in the right direction.

If you liked this tutorial follow me on Medium. Also, subscribe to my newsletter.

Happy Coding!

Top comments (0)