Logo

dev-resources.site

for different kinds of informations.

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

Published at
9/16/2024
Categories
nextjs
typescript
approuter
react
Author
nifty-little-me
Categories
4 categories in total
nextjs
open
typescript
open
approuter
open
react
open
Author
15 person written this
nifty-little-me
open
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!

approuter Article's
23 articles in total
Favicon
Show a loading screen when changing pages in Next.js App router
Favicon
Learning Next.js 13 App Router: A Comprehensive Guide 🚀
Favicon
Guide to build a modern Web App using Next.js 14 (App Router), Fully authentication (NextAuth), Theming and i18n
Favicon
A practical Guide - Migrating to Next.js App Router
Favicon
Adding Chat Functionality To Your Next.Js Project With Firebase
Favicon
Spicing Up Your Next.Js Projects With 3D: What Are Your Options?
Favicon
No More Pages
Favicon
How To Create A Basic Infinity Canvas For Your Next.Js Project
Favicon
How To Implement Text-To-Speech Functionality For BlockNote In Next.Js
Favicon
How To Add Drag-And-Drop Functionality With Editable Draggable Items In Next.js
Favicon
Adding Drag And Drop Functionality In Your Next.Js Project Without A Library
Favicon
Creating NPM Packages in Next.Js for Next.Js
Favicon
Using Firebase To Store Folders and BlockNote Documents In Next.Js
Favicon
An Alternative To Editor.js: BlockNote For React
Favicon
How To Add Editor.Js To Your Next.Js Code
Favicon
How To Use The Quill Rich Text Editor in Your Next.Js App Router Project
Favicon
Simple NextJS GraphQL client
Favicon
Implementing Internationalization (i18n) in Next.js 14 using App Router
Favicon
Web Streams API in Action: Delivering 6000+ Log Lines Concurrently Across 20 Tabs
Favicon
How to use React-Toastify with Next.js App router
Favicon
NextAuth - Implementando "Custom Login Pages" com "Credentials Sign in" utilizando App Router
Favicon
The origin of App Router - A Next.Js Rewind
Favicon
How to add multiple routers in a node application without using app.use() for each router ?

Featured ones: