DEV Community

Cover image for React Drag N' Drop
Matthew Palmer
Matthew Palmer

Posted on • Edited on

React Drag N' Drop

Introduction

If UI design was like a full course Thanksgiving meal, "Drag N' Drop" would easily qualify as bread and butter. Or the turkey. Or even the ham. ORR... ahem... you get the point! 😉 It's good stuff. This is an ideal feature to have on our websites. It helps users feel like they are truly in control of their experience while using our applications.

NOTE: This is going to be a 7 minute read, and it might take you a bit longer to follow along. I totally understand and sympathize with you not wanting to waste your time! I get wanting to follow the right tutorial. ❤️ So, if you want to see this live before continuing, Click here... on a desktop computer... This is not mobile friendly.

So, without further ado, let's get right down to it.

Important Info

There's a few concepts I'd like to cover here. If you're already familiar with web APIs, here's a couple sources for a quick recap on dataTransfer, dataTransfer.getData() and dataTransfer.setData():
dataTransfer
dataTransfer.getData()
dataTranser.setData()

These concepts were personally a tough one for me to understand, so not to worry -- I'll cover what exactly is happening in sufficient detail within this blog.

Setup

Let's start from scratch. Create a React application by typing npx create-react-app your-choice-appname into your terminal and pressing enter, with "your-choice-appname" being literally whatever you want to name this project.

Once this is complete, let's do some cleanup. Remove App.test.js and let's rename index.css to main.css. Just because we can. 👍

Next, you'll want to make sure you're importing your main.css inside of your index.js, like so:

import React from 'react';
import ReactDOM from 'react-dom';
import './main.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

...
Enter fullscreen mode Exit fullscreen mode

Perfect!

Creating Board and Card Components

We are going to be following the separation of concerns rule, so let's create a folder in our source folder called "components" -- src/components.

Inside of this folder, create two files Card.jsx and Board.jsx.

These will be functional components that will accept props as arguments between each other. This will be necessary for a transfer of data back and forth.

NOTE: This is where things begin to get slightly confusing. To understand what happens next will require understanding what is happening in both Card.jsx and Board.jsx simultaneously. I'll be providing a thorough explanation, so bare with me. Once it clicks, you'll have what I like to call an "AHA! moment."

Board.jsx

Let's begin with a Board component skeleton. This is what we will start off with:

import React from 'react';

export default function Board(props) {

    return (
        <div>

        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Before we jump into the applied logic of this component, we need to set some properties for our div. We need an id and a className assigned to it, and this is where props comes in. Let's change it to reflect the dynamic operation we want it to perform.

import React from 'react';

export default function Board(props) {

    return (
        <div 
            id={props.id} 
            className={props.className}
        >

        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

The reason for this is because later on we will be using this board component in a manner such as this:
<Board id="1" className="board"></Board>
As you can see, our props will be "id" and "className".

drop

Now we can begin adding in our functions. We want to handle two React events on our boards. They are onDrop (for when we drop a card in this board) and onDragOver (to handle tracking the data of a card as it is being dragged by your cursor into a board). Let's apply these events to our div.

NOTE: These events will be triggering from functions we haven't created yet, but we will be creating them next.

export default function Board(props) {

    return (
        <div 
            id={props.id} 
            className={props.className}
            onDrop={drop}
            onDragOver={dragOver}
        >

        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Alright, now for the fun part! We will be creating a function called drop and place this above our return():

export default function Board(props) {
     const drop = e => {
        const card_id = e.dataTransfer.getData('card_id');
        const card = document.getElementById(card_id);

        e.target.appendChild(card);
    }

    return (
        <div 
            id={props.id} 
            className={props.className}
            onDrop={drop}
            onDragOver={dragOver}
        >

        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

"Whoa, wait! What's going on here MATT?"

I'm glad you asked! Let's start with the first two declarations in our drop function.

const card_id = e.dataTransfer.getData('card_id') will be responsible for acquiring the data from the card we will be dragging into the board later on. We are setting a declaration of "card_id" set to this dataTransfer, which will be coming directly from our cursor when we drop a card. (Sorry if I'm being redundant/repeating myself. I feel if you "get the point", then I'm explaining this well. 😉)

Next, we will be setting another declaration of "card" which is being set towards grabbing the card element ID in the DOM so that it can be dropped into the board.

Finally, we are using e.target.appendChild(card) to add our card to e.target (e.target being the current board the card is being dropped into.).

dragOver

This one is short and sweet. All we want to do is create a dragOver function which takes e as an argument for event and preventing the default behavior of our onDragOver React event. Basically, we want to prevent onDragOver from snapping our card back to the original position it was dragged from on the board it came from. This event must be started but must not be completed in order for our onDrop event to fire.

 const dragOver = e => {
        e.preventDefault();
 }
Enter fullscreen mode Exit fullscreen mode

To wrap this up, we want all of our cards to be displayed on our page. To do this, we simply add { props.children } between our div.

Your finished Board.jsx component should look like this:

import React from 'react';

export default function Board(props) {
    const drop = e => {
        const card_id = e.dataTransfer.getData('card_id');
        const card = document.getElementById(card_id);

        e.target.appendChild(card);
    }

    const dragOver = e => {
        e.preventDefault();
    }

    return (
        <div 
            id={props.id} 
            className={props.className}
            onDrop={drop} 
            onDragOver={dragOver}
        >
            { props.children }
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Card.jsx

Time for our Card.jsx component! We are going to start off similarly to the way we set up our Board.jsx:

import React from 'react';

export default function Card(props) {
    return (
        <div>

        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Next, let's set some properties in our div. In addition to an id and className like we have in our Board.jsx component, we want to apply a special property to our cards called draggable. This property will need to be set to true in order for our cards to be, well... you guessed it -- draggable.

import React from 'react';

export default function Card(props) {
    return (
        <div
            id={props.id}
            draggable={props.draggable}
            className={props.className}
        >

        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

As you might've concluded, we will be using this component similar to the way we use <Board></Board> like so:

<Card id="1" className="card" draggable="true">
    <p>Card one</p>
</Card>
Enter fullscreen mode Exit fullscreen mode

Now we can begin adding in functions dragStart (which will handle moving the card data into your cursor) and dragOver (which will be used to prevent cards from being dropped into other cards). Both will be executed by React events onDragStart and onDragOver.

import React from 'react';

export default function Card(props) {
    return (
        <div
            id={props.id}
            draggable={props.draggable}
            className={props.className}
            onDragStart={dragStart}
            onDragOver={dragOver}
        >

        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

dragStart

Good stuff! Now let's add in those functions. Right above our return(), we can start with our dragStart function:

const dragStart = e => {
    const target = e.target;
    e.dataTransfer.setData('card_id', target.id)
}
Enter fullscreen mode Exit fullscreen mode

We are making a declaration of target which will be assigned to e.target (e.target being the card in question that we will be dragging). Next, we are introduced to another function of the HTML Drag and Drop API: e.dataTransfer.setData('card_id', target.id). What is happening here is we are setting the data in our cursor referenced as card_id and assigning the ID of the card we are dragging (target.id) to this reference.

DING DING... 💡 Remember e.dataTransfer.getData('card_id') in our Board.jsx component? The card data is SET in the Card.jsx component, and the Board.jsx GETS that data... See? I told you this would all click. 😉

dragOver

Our final function... dragOver. This one is short and straight forward. All we need to do for this function is to apply stopPropagation to the event. The purpose of this function is to prevent cards from being able to be dropped into other cards. Otherwise, things can quickly become messy for our users!

const dragOver = e => {
    e.stopPropagation();
}
Enter fullscreen mode Exit fullscreen mode

Finally, don't forget to add { props.children } to the div just like we did for Board.jsx.

There we have it! We are all set to apply these components.

Showtime

Go into your App.js and import Card.jsx and Board.jsx from src/component. Finally, we will place two boards and two cards in each board rendered to our web page. Here's what your App.js should look like:

import React, { Component } from 'react';
import Board from './components/Board.js';
import Card from './components/Card.js';

export default class App extends Component {  

  render() {
    return (
      <div className="App">
        <main className="flexbox">
          <Board id="board-1" className="board">
            <Card id="1" className="card" draggable="true">
              <p>Card one</p>
            </Card>
            <Card id="2" className="card" draggable="true">
              <p>Card two</p>
            </Card>
          </Board>

          <Board id="board-2" className="board">
            <Card id="3" className="card" draggable="true">
              <p>Card three</p>
            </Card>
            <Card id="4" className="card" draggable="true">
              <p>Card four</p>
            </Card>
          </Board>
        </main>
      </div>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

There's one more thing you'll want to do... Apply some styling in your main.css so you can easily see your components. This should suffice for now:

* {
    margin: 0; padding: 0; box-sizing: border-box;
}

body {
    background-color: #f3f3f3;
}

.flexbox {
    display: flex;
    justify-content: space-between;
    width: 100%;
    max-width: 786px;
    height: 100vh;

    overflow: hidden;

    margin: 0 auto;
    padding: 15px;
}

.flexbox .board {
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: 300px;
    background-color: #313131;
    padding: 15px;
}

.flexbox .board .card {
    padding: 15px 25px;
    background-color: #f3f3f3;

    cursor: pointer;
    margin-bottom: 15px;
}
Enter fullscreen mode Exit fullscreen mode

Crank up that npm start and play around with the cards!

Conclusion

As developers, we often place an awful stigma on processes that seem far more complicated than they actually are. The Drag N' Drop function, to me, sounded like it was going to be way worse than this method. Although there is way more you could do to make the experience more robust, hopefully this will set the rest of you off with a fair hand. :) Happy coding, y'all!

Top comments (2)

Collapse
 
developman profile image
Dmitry Mineev

Hey, Metthew!
Thanks for this cool tutorial. I guess you missed something. return methods of Board.jsx and Card.jsx should include {props.children} inside of div tags:

return (
  <div
    id={props.id}
    className={props.className}
    onDrop={drop}
    onDragOver={dragOver}>
      {props.children}
  </div>
)
Enter fullscreen mode Exit fullscreen mode

Otherwise, the board will not display any card and cards will not display the names (Card one, Card two, Card three etc)

Collapse
 
matthewpalmer9 profile image
Matthew Palmer

Thanks, Dmitry! I included it in my Board.jsx, but I forgot to showcase that in Card.jsx. I'll update the tutorial with that information. Great catch, my friend! I appreciate your careful eye!