In this article we will create a 2D grid were each item can be dragged and moved to a different place, quick demo.
We will be writing most of the things from scratch to see how things work under the hood but will be using react-spring to animate because it takes the animation out of React for performance reasons! don't worry logic will be still ours, though you can surely remove react-spring out of the picture and use something else or just React ;) , we will see it at the end.
What we will cover, this will be a 3 parts series
- Creating a single draggable block
- Creating 2D blocks layout with custom hook useDraggable
- Rearranging blocks using react-spring [2nd week of June]
Creating a single draggable block
What is a draggable block? block which moves with the mouse pointer, when the mouse key is pressed until the pressure from the key is released.
There are 3 events involved here
- Mouse key/track-pad is pressed i.e.
mouseDown
- Mouse is moved hence the pointer moves i.e.
mouseMove
- The pressure is released i.e.
mouseUp
mouseDown
will give us the initial coordinates, on each mouseMove
this will be fired whenever there is a movement even for 1px will give us the accurate path and mouseUp
will give us the ending coordinates. Our block (it can be anything, div, image etc.) have to move with the mouse, so we will bind appropriate methods with the mouse events.
Let's create a block.
import * as React from "react";
// For CSS in JS
import styled from "styled-components";
const BlockWrapper = styled("div")`
position: relative;
border-radius: 4px;
margin-right: 8px;
display: flex;
align-items: center;
justify-content: center;
height: 120px;
width: 120px;
background: red;
`;
const StyledText = styled("p")`
color: white;
font-weight: 600;
font-size: 24px;
`;
const Block = () => {
return (
<BlockWrapper>
<StyledText>1</StyledText>
</BlockWrapper>
);
};
export default Block;
Great we have a static block now let move it. Let's apply mouseDown
to our block. Before jumping to actual code, let's try to derive the calculation needed.
Block Initial Coordinates:
[0,0]
Pointer Initial Coordinates:
[10,2]
(whenmouseDown
was fired)
Pointer Final Coordinates:
[110,102]
(whenmouseUp
was fired)
Pointer Movement= Final-Initial:
[100, 100]
Block Coordinates = [Block Initial Coordinates + Pointer Movement]
Now block may have some initial coordinates, but that will be covered as we are adding the difference to it.
const Block = () => {
const [coordinate, setCoordinate] = React.useState({
block: {
x: 0,
y: 0,
},
pointer: { x: 0, y: 0 },
dragging: false,
});
const handleMouseMove = React.useCallback(
(event) => {
if (!coordinate.dragging) {
return;
}
const coordinates = { x: event.clientX, y: event.clientY };
setCoordinate((prev) => {
const diff = {
x: coordinates.x - prev.pointer.x,
y: coordinates.y - prev.pointer.y,
};
return {
dragging: true,
pointer: coordinates,
block: { x: prev.block.x + diff.x, y: prev.block.y + diff.y },
};
});
},
[coordinate.dragging]
);
const handleMouseUp = React.useCallback(() => {
setCoordinate((prev) => ({
...prev,
dragging: false,
}));
}, []);
const handleMouseDown = React.useCallback((event) => {
const startingCoordinates = { x: event.clientX, y: event.clientY };
setCoordinate((prev) => ({
...prev,
pointer: startingCoordinates,
dragging: true,
}));
event.stopPropagation();
}, []);
return (
<BlockWrapper
style={{ top: coordinate.block.y, left: coordinate.block.x }}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
>
<StyledText>1</StyledText>
</BlockWrapper>
);
};
Try it here.
It's buggy
If the pointer is moved fast enough the block will be lost in the way as now the pointer has crossed the block, onMouseMove
doesn't triggers anymore, hence no more dragging, a simple way to fix it is add mousemove
and mouseup
to document or the parent div.
We cannot add handler directly on document, we have to use addEventListener
and with parent we can move our state upward and pass handleMouseUp
and handleMouseMove
to parent div. Something like this
<div
style={{ border: "1px solid", height: "100%", width: "100%" }}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}>
<Block
style={{ top: coordinate.block.y, left: coordinate.block.x }}
onMouseDown={handleMouseDown}
/>
</div>
So which one? The parent one, there is two reason behind it:
- Not all area of app is going to be draggable, probably one section of it so if the mouse moves out of the parent div, our block will stay inside, but in case of events on document, we have check that on every
mouseMove
event. - Other reason is handlers are more "React Way" of doing things, also you don't have to remove them ;) on unmount.
That's all for today! Next up we will move our code responsible for dragging into a hook and will create a 2D layout.
It should be noted there are many libraries which provides hook out of the box for dragging, one is use-gesture
which works seamlessly with react-spring
and also takes dragging out of React, giving a little performance boast. Though we will not be covering it here as our target is to learn the basics.
Sourced from dev.to 🧑💻 👩💻