Hello, DnD!

Drag & drop — do it yourself or use ready-made

First word

Drag & drop functionality allows to easily drag interface elements from one place to another. Due to its simplicity and ease of use, drag & drop can be found in many graphical interfaces. And, of course, there already are tons of ready-made solutions that make implementing such functionality a piece of cake.

However, drag & drop libraries are not always flexible enough to meet all the business requirements. When the requirements are different from the standard implementation, it is often faster to write your own drag & drop functionality rather than to add crutches for an existing codebase. Not to mention, your own implementation is easier to maintain in the face of frequently changing requirements (clients, clients and again clients hehe!).

It was also the case for us in one of our projects. React-beautiful-dnd is really beautiful, but in an attempt to customize it to fit our needs, it has acquired a lot of hacks: some advised by its authors, some being our original solutions. In the end, we decided to develop a completely custom solution from the ground up and kick the beautiful off.

A man with a gun.
bang bang

In this article we’ll discuss the intricacies of implementing drag & drop into your own project. Gonna use React as the environment for the examples.

Introduction

When dragging & dropping elements in a list, we need to (ugh, every time!) remember the positions of the elements.

Most of the time, you already have an initial list that needs to be displayed in a specific order. At this stage, we need to initialize the elements of the list with the values of the initial positions. Usually a certain constant is chosen as a step. The first element in the list receives the value of this step, the positions of the following elements are calculated by adding the step to the previous element.

The new position of a draggable element is calculated by taking the average between adjacent elements. For example, to move an element in a vertical list, the formula would look like this:

newPosition = (topItem.position + dowItem.position) / 2

For easier division, it is recommended to use float numbers for the positions of the draggable elements.

We will make a simplified example — without Redux and backend, but will assume 2 things — that at a later stage the positions of the elements will have to be stored somewhere, and that HTML elements have some level of complexity (nesting).

HTML Drag & Drop

Let’s take a look at the ̶b̶o̶r̶i̶n̶g̶ ̶s̶t̶u̶f̶f̶ documentation.

draggablean attribute that indicates that the element can be dragged.

Methods of a draggable element:

onDragStart — event handler for starting to drag an element.

onDragevent handler for dragging an element.

onDragEndevent handler for the end of dragging an element.

Methods for the area we’re dragging the elements into:

onDragEnterevent handler for entering the area of the element which we’re dragging to.

onDragLeaveevent handler for exiting the element area.

onDragOver — event handler for dragging above the current element.

onDrop — event handler for the drop (the same as end of dragging).

Let’s start! onDragStart!

Let’s create a list of draggable elements:

See, it doesn’t look too bad already, right?!

A list with draggable elements.
That’s how draggable elements look like

But so far this is only visuals. We will have to write the entire logic of the drag and drop ourselves.

First I would like to add styles to the list. Let’s make a draggable element disappear from the list and remain stuck to the mouse cursor.

To do this, let’s add a listener to the onDragStart event:

And let’s also implement the onDragStartHandler handler, which will remember the index of the draggable element in order to add styles to it later:

This is where the magic begins. Caution — not a long read!

Drag & drop’s default behavior is to take a screenshot of the draggable element at the time of the drag and display it near the mouse cursor. If in the onDragStart event handler we save the draggable element’s index synchronously and use it to set styles for the element, then new styles can appear on both — the element in the list and the screenshot on the mouse, as well as only on the dragged element. This depends on whether the engine creates a screenshot before applying new styles on the element or after. To make sure that the screenshot remains unchanged and styles are applied only to the element, we wrap the setDraggableIndex function call into a setTimeout function. This way, we take the operation of saving the index out of the stream, so that the engine takes a screenshot of the element with the old styles — before the new styles are applied to the element.

We should also mention the opacity property. As you can see in the animation above — the screenshot of the element hanging by the mouse cursor has opacity. The element receives this property from the internal processing of the event, and there don’t seem to be any standard way to influence it — delete or change it.

And, of course, the index must be reset at the end of the drag:

An element is removed from the list when dragged.
Hid a draggable element from the list.

See? Much better.

But all we did was just adding some styles:

Hagrid says to Harry that he is a developer.
hehe

Hover? No. onDragEnter.

At the moment of dragging, it is often necessary to highlight the area where the element is currently located, so that the user can see where it will go once the mouse button is released.

Sadly, CSS doesn’t have something like hover for DnD. But we will write our own, it will be even better. Trust me, I’m an engineer!

To do this, we will use the onDragEnter and onDragLeave events to save the index of the element we are currently above, as well as a <Separator /> component that will clearly show the user where the element will go in the list if the mouse button is released.

In our case, the <Separator /> component will look like a simple line:

But you can use any visual separator you like.

The onDragEnter and onDragLeave events have their own pitfalls. If the drag area has multiple nestings, these events may fire multiple times. This will create a discrepancy where the hover area will be considered hovered when it is not, or vice versa. For example, if we have an error in the calculations, and the depth value remains positive, then we will not be able to track when we left the tracked area:

Incorrect highlights of drop areas when dragging.
Incorrect processing of multiple onDragEnters and onDragLeaves leads to wrong highlights.

To fix this bug, we will use a little trick.

Let’s create a variable called dropDepthRef.current, in which we will store the depth counter. Each time the onDragEnter event fires, we will increment the counter by 1. And when the onDragLeave event fires, decrement the counter by 1 and reset the index only if the counter is 0, which will mean that we have left the top element:

Note that we are using useRef to store the counter. There is a reason for it. If you store the variable in state, the component will re-render every time either one of onDragEnter and onDragLeave events fire. Firstly, we do not need to re-render the component, and secondly, this can lead to an error in the depth calculation if the new event fires at the time of the state update.

Another bug you may encounter is incorrect rendering of the <Separator /> element. In the example above, we used this common construction:

{index === dragOverIndex && <Separator /> }

When using such a construct in your code, the fastest QA engineers will notice a case where, when quickly moving the cursor between elements, one of the onDragLeaveHandler is skipped due to component re-rendering, which leads to a depth calculation error and, as a result, the bug shown in the animation above. And it is quite difficult to escape the fastest one. Trust me.

How do we fix this? Instead of conditionally rendering the <Separator /> component, move the rendering logic inside the component:

<Separator active={list.length === dragOverIndex} />

Now the <Separator /> looks like this:

And this is what we get:

Highlighting a drop area with a line when draggable element is over it.
Highlighting a drop area

A small note — to also be able to drop to the end of the list, do not forget to add a listener there:

This is what the code looks like at this stage:

Finally, not just drag! Also drop. onDrop!

Let’s add an onDrop event handler. Now, the draggable element wrappers will look like this:

In the handler, we remove the draggable element from the list and insert it with a new position calculated as the arithmetic average of the positions of the top and bottom elements relative to the current dropIndex:

It’s important to note that the calculation of the position will depend on the data structure, performance requirements and the final goals. Our goal here is to make it descriptive. And fast ;-)

Launch, check and … it does not work. Needs a little more magic!

Needs more magic

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

This is non-obvious particularity of Drop Targets. Details can be found here.

And…done!

Drag and drop an element in the list.
We finally can drop!

Drag but deeper.

Not enough special effects? We can change the default behavior of taking a screenshot while dragging. Using the setDragImage method available on the dataTransfer event object, we can create our own screenshot using any element and any formula for calculating coordinates. For example, in the code below, the draggable element moves itself to the mouse cursor!

Here we have created an element and its screenshot. But we are only interested in the screenshot, so the element must be hidden. We can’t use display:none or zero opacity to hide it, because those styles will extend to our screenshot as well. Therefore, we use the following hack:

And do not forget to delete the created element at the end of the drag:

Let’s see what happens.

The draggable element moves to the mouse cursor.
The draggable element moves to the mouse cursor

So cute!

In fact, this is a necessary measure — in real conditions, native DnD just does not look very nice, creating an image with all the hovers, shadows and the surrounding environment.

DnD for other places!

With the help of drag & drop you can implement a lot of interesting things. You are only limited by your imagination.

For example, the code below removes items when dragging them to the trash can:

onDrop handler:

And here we go!

Throw items into the trash.
Throw items into the trash

DnD for everything!

Still not enough? We can do more! Much more!

For the next point, we will again expand the functionality a bit by adding handling for cases when the user drags a file or text. To do this, let’s turn to dataTransfer again, now the onDrop event.

To check whether there are files inside whatever the user is dragging, we will do this:

if (e.dataTransfer.types.filter(i => i === ‘Files’).length) {…}

But you can also drag and drop text. And it also has its own processing, like:

if (e.dataTransfer.types.filter(i => i === ‘text/plain’).length) {…}

In both cases, we check for the presence of the desired type in the dragged elements in order to process them.

In the end, our handler will look like this:

And, of course, the result:

Selected a text and dragged it into the list.
Drag & drop text
Took a file and dropped it into the list.
Drag & drop files

Here you can try it yourself!

When adding React Redux, the described functionality is enough to cover 98% of the tasks, and it will allow you to start a real DnD party!

A cat is shocked about so much DnD.

About us

Intspirit is an outsource company focused on developing complex web and mobile apps. We are in love with coding and widely share our knowledge.

Follow us on Medium to not miss new articles.

Follow us on LinkedIn and Facebook to be aware of other awesome projects we do.

Try yourself in our telegram-channel with JS quizzes frequently asked in job interviews.

Thank you for reading, and let’s make IT better!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Intspirit Ltd

Intspirit Ltd

High-level software engineers for powerful web solutions https://intspirit.com/