I am using react-dnd drag and drop and have a sorted list that gets mapped through, it works somewhat I am able to drag and drop and on refresh it seems things stayed in the right position but the element moves one row than where I dragged it.
The main issue is when I drag the item and drop it the cards state in the moveCardDb
is different slightly then outside the function, why it would be different at that point I can't seem to figure out.
Here is a minimal setup of what I have https://codesandbox.io/s/gifted-goodall-qu43p?file=/src/Container.jsx
If you look at the console log on the moveCardDb function you will see the cards stae variable slightly out of order
Thanks ahead of time
I have the following code for the drag and drop
The mapping function and update of position
const [cards, setCards] = useState([]);
let stateReplace = useMemo(() => {
if (!isLoading && formBuilder?.inputs?.length) {
return formBuilder.inputs;
}
return [];
}, [isLoading]);
useEffect(() => {
setCards(stateReplace);
}, [setCards, stateReplace]);
// console.log(cards);
const moveCard = useCallback(
(dragIndex, hoverIndex) => {
console.log(dragIndex);
console.log(hoverIndex);
const dragCard = cards[dragIndex];
setCards(
update(cards, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragCard],
],
})
);
},
[cards]
);
const moveCardDb = useCallback(() => {
//console.log(cards);
Meteor.call("formLeadBuilderDrag.update", cards, params._id, function (
error,
result
) {
console.log(result);
console.log(error);
});
}, [cards]);
const renderCard = (card, index) => {
return (
<>
{isLoading ? (
<div className="loading">loading...</div>
) : (
<>
<Card
key={card.dragPositionId}
index={index}
id={card.dragPositionId}
input={card.inputType}
moveCard={moveCard}
moveCardDb={moveCardDb}
/>
</>
)}
</>
);
};
return (
<>
{isLoading ? (
<div className="loading">loading...</div>
) : (
<form>
<div style={style}>{cards.map((card, i) => renderCard(card, i))}</div>
<input type="submit" />
</form>
)}
</>
);
The card rendered
import React, { useRef } from "react";
import { useDrag, useDrop } from "react-dnd";
import { ItemTypes } from "./ItemTypes";
const style = {
border: "1px dashed gray",
padding: "0.5rem 1rem",
marginBottom: ".5rem",
backgroundColor: "white",
cursor: "move",
};
export const Card = ({ id, input, index, moveCard, moveCardDb }) => {
const ref = useRef(null);
const [, drop] = useDrop({
accept: ItemTypes.CARD,
hover(item, monitor) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return;
}
// Determine rectangle on screen
const hoverBoundingRect = ref.current?.getBoundingClientRect();
// Get vertical middle
const hoverMiddleY =
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get pixels to the top
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
// Time to actually perform the action
moveCard(dragIndex, hoverIndex);
moveCardDb();
// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
item.index = hoverIndex;
},
});
const [{ isDragging }, drag] = useDrag({
item: { type: ItemTypes.CARD, id, index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
const opacity = isDragging ? 0 : 1;
drag(drop(ref));
return (
<div ref={ref} style={{ ...style, opacity }}>
<p>{input}</p>
<input
name={input + id}
defaultValue="test"
// ref={register}
/>
{/* <button type="button" onClick={onEditToggle}>
<BiEditAlt size={25} />
</button> */}
{/* <button onClick={() => deleteLead(leads)}>×</button> */}
</div>
);
};
My Object from the beginning
{
"_id": "showRoomId",
"type": "Show Room Lead",
"createdAt": "2020-11-14",
"userId": "83nfd298dn382",
"inputs": [
{
"inputType": "shortText",
"dragPositionId": "1",
"label": "First Name:"
},
{
"inputType": "phoneNumber",
"dragPositionId": "2",
"label": "Cell Phone Number"
},
{
"inputType": "email",
"dragPositionId": "3",
"label": "Work Email"
},
{
"inputType": "Address",
"dragPositionId": "4",
"label": "Home Address"
},
{
"inputType": "multipleChoice",
"dragPositionId": "5",
"label": "Preferred Method of Contact",
"options": [
{
"dragPositionId": "1",
"label": "Email"
},
{
"dragPosition": "2",
"label": "Cell Phone"
}
]
},
{
"inputType": "dropDown",
"dragPositionId": "6",
"label": "How did you find us?",
"options": [
{
"dragPositionId": "1",
"label": "Google"
},
{
"dragPosition": "2",
"label": "Referral"
}
]
}
]
}
I ended up putting a time on a function on the monitor.didDrop function. It seems a bit hacky to me, so if anyone can offer a better solution let me know. I also decided to store to localstorage first, and then wil submit to database on form submit.
const [{ isDragging, didDrop }, drag] = useDrag({
item: { type: ItemTypes.CARD, id, index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
didDrop: monitor.didDrop(),
}),
});
const opacity = isDragging ? 0 : 1;
function droppy(dropped) {
var delayInMilliseconds = 1000; //1 second
setTimeout(function () {
dropped();
}, delayInMilliseconds);
}
if (didDrop) {
droppy(moveCardDb);
}