Search code examples
javascriptreactjsmongodbmeteorreact-dnd

React-dnd drag update db in mongo with meteor inorder to persist order when page is refreshed


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)}>&times;</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"
        }
      ]
    }
  ]
}

Solution

  • 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);
      }