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';
...
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>
)
}
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>
)
}
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>
)
}
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>
)
}
"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();
}
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>
)
}
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>
)
}
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>
)
}
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>
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>
)
}
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)
}
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();
}
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>
)
}
}
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;
}
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)
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:
Otherwise, the board will not display any card and cards will not display the names (Card one, Card two, Card three etc)
Thanks, Dmitry! I included it in my
Board.jsx
, but I forgot to showcase that inCard.jsx
. I'll update the tutorial with that information. Great catch, my friend! I appreciate your careful eye!