Wonderland
  • README.md
  • Notebook
    • Crypto
      • Solana
        • Troubleshooting
          • BPF SDK path does not exist
    • Language
      • Rust
        • Reference
          • Capturing the Environment with Closures
          • Understanding &mut &mut Reference
      • C++
        • Reference
          • Code for MS rand() and srand() in VC++ 6.0
  • Textbook
    • Serials
      • A Real-Time Cryptocurrency Ticker Dashboard
        • 0 - Some Preparation
    • Frontend
      • A Simple Progress Bar
      • A React Ribbon Component
      • An Easy to Use React DnD Sortable Component
      • Sticky Header, Sticky Footer and Fluid Content
      • How To Set Same Height As Width In CSS
  • dictionary
    • Alphabet
      • MySQL
      • FFmpeg
    • Algorithm
      • Diary
        • 2022
          • 07
            • 2022-07-02
            • 2022-07-01
          • 06
            • 2022-06-30
            • 2022-06-29
            • 2022-06-28
            • 2022-06-27
            • 2022-06-26
            • 2022-06-25
            • 2022-06-24
            • 2022-06-23
            • 2022-06-22
            • 2022-06-21
            • 2022-06-20
            • 2022-06-19
            • 2022-06-18
            • 2022-06-17
            • 2022-06-16
            • 2022-06-15
            • 2022-06-14
            • 2022-06-13
            • 2022-06-12
            • 2022-06-11
            • 2022-06-10
            • 2022-06-09
            • 2022-06-08
            • 2022-06-07
            • 2022-06-06
            • 2022-06-05
            • 2022-06-04
            • 2022-06-03
            • 2022-06-02
            • 2022-06-01
          • 05
            • 2022-05-31
            • 2022-05-30
            • 2022-05-29
            • 2022-05-28
            • 2022-05-27
            • 2022-05-26
            • 2022-05-25
            • 2022-05-24
            • 2022-05-23
            • 2022-05-22
            • 2022-05-21
            • 2022-05-20
            • 2022-05-19
            • 2022-05-18
            • 2022-05-17
            • 2022-05-16
            • 2022-05-15
    • Troubleshooting
      • A Weird Python Command Not Found Problem
Powered by GitBook
On this page
  • Target
  • Code
  • Usage
  • Example
  • Reference
  1. Textbook
  2. Frontend

An Easy to Use React DnD Sortable Component

I hope this is actually easy to use...

Last updated 3 years ago

Target

  • Easy to use

  • Less code

Code

You can simply copy the following code to a dnd-sortable.tsx file, and use as a component.

import React, { FC, ReactNode, useCallback, useRef, useState } from "react";
import { useDrag, useDrop } from "react-dnd";
import type { XYCoord, Identifier } from "dnd-core";
import update from "immutability-helper";

export interface NodeProps {
  id: any;
  index: number;
  node: ReactNode;
  moveNode: (dragIndex: number, hoverIndex: number) => void;
}

interface DragItem {
  index: number;
  id: string;
  type: string;
}

export const Node: FC<NodeProps> = ({ id, index, node, moveNode: moveNode }) => {
  const ref = useRef<HTMLDivElement>(null);
  const [{ handlerId }, drop] = useDrop<DragItem, void, { handlerId: Identifier | null }>({
    accept: "item",
    collect(monitor) {
      return {
        handlerId: monitor.getHandlerId(),
      };
    },
    hover(item: DragItem, 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 as XYCoord).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
      moveNode(dragIndex, hoverIndex);

      // 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({
    type: "item",
    item: () => {
      return { id, index };
    },
    collect: (monitor: any) => ({
      isDragging: monitor.isDragging(),
    }),
  });

  const opacity = isDragging ? 0.5 : 1;
  drag(drop(ref));
  return (
    <div ref={ref} style={{ opacity }} data-handler-id={handlerId}>
      {node}
    </div>
  );
};

export interface Item {
  id: number;
  node: ReactNode;
}

export default function DndSortable({ nodeList }: { nodeList: Array<ReactNode> }) {
 // Set id for every react node for react-dnd to use
  const [itemList, setItemList] = useState(nodeList.map((node, index) => ({ id: index, node: node })));

  const moveNode = useCallback((dragIndex: number, hoverIndex: number) => {
    setItemList((prevNodes: Item[]) =>
      update(prevNodes, {
        $splice: [
          [dragIndex, 1],
          [hoverIndex, 0, prevNodes[dragIndex] as Item],
        ],
      })
    );
  }, []);

  const renderNode = useCallback((id: number, index: number, node: ReactNode) => {
    return <Node key={id} index={index} id={id} node={node} moveNode={moveNode} />;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return <>{itemList.map((item, index) => renderNode(item.id, index, item.node))}</>;
}

Usage

const cards = useRef([
  {
    id: 1,
    text: "Write a cool JS library",
  },
  {
    id: 2,
    text: "Make it generic enough",
  },
  {
    id: 3,
    text: "Write README",
  },
  {
    id: 4,
    text: "Create some examples",
  },
  {
    id: 5,
    text: "Spam in Twitter and IRC to promote it (note that this element is taller than the others)",
  },
  {
    id: 6,
    text: "The origin text here is a symbol that is not ok",
  },
  {
    id: 7,
    text: "PROFIT",
  },
]);

...

<DndSortable
  nodeList={cards.current.map((card, i) => {
    return (
      <div key={i} className="dnd-sortable">
        {card.text}
      </div>
    );
  })}
/>

Example

Reference

The react-dnd official simple sortable example:

Sortable Simple
React DnD