Copyright notice: This article is the blogger’s original article, shall not be reproduced without the permission of the blogger. Please contact the author by leaving a comment at the bottom of the article.

The background,

According to the business needs, it is necessary to realize the drag and drop sorting of tree form, because it is developed based on ANTD, and antD official drag and drop demo is just a simple ordinary table drag, which cannot meet the actual business needs. As follows:

  • Drag and drop the sortingThe use ofreact-dnd , the API is slightly complicated to use and needs to be studied and learned, and the data is modified every time when dragging and dropping, which feels inferior to the followingreact-sortable-hoc. The advantage is that I personally feel that for implementationTree formDrag-and-drop sort is more friendly and convenient, so I ended up using this third party library implementation.
  • The advantage of the react-sortable-hoc used in the drag handle column is that there is no need to deal with some animation effects of drag, and the drag process is only a simple style modification, and data modification can be carried out when it is finally placed.

I decided to use React-DND for the effect after looking at two drag and drop third-party libraries used by ANTD. First put the effect picture:

Here’s how it works.

Two, drag and drop sort implementation

First of all, check the documents of react- DND, mainly refer to the example of sorting Sortable for implementation.

1. Install

npm install react-dnd react-dnd-html5-backend --save
Copy the code

2. UseDndProviderThe parceltable

Document Description: The DndProvider component provides React-DnD capabilities to your application. This must be injected with a backend via the backendprop, but it may be injected with a windowobject.

The DndProvider component provides your application with react-DND capabilities. Injection must be done through the backend property, and can also be done through the Window object.

import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { Table } from "antd"; .return (
    <DndProvider backend={HTML5Backend}>
        <Table
          .
        />
    </DndProvider>
)
Copy the code

3. Refer to the official ANTD example for transformation

The antD table API uses onRow:

And the use of components to override the default table element:

Next we can implement part of it: the components and onRow implementations

//data.js Prepares data

/ / table columns
export const columns = [
  {
    title: "Name".dataIndex: "name".key: "name"
  },
  {
    title: "The number".dataIndex: "number".key: "number"}];/ / data
export const tableData = [
  {
    parentId: 0.id: 1.name: "Group 1".number: "10".type: "group".children: [{parentId: 1.id: 2.name: "Group 1- Test 1".number: "2".type: "child"
      },
      {
        parentId: 1.id: 3.name: "Group 1- Test 2".number: "5".type: "child"
      },
      {
        parentId: 1.id: 4.name: "Group 1- Test 3".number: "3".type: "child"}]}, {parentId: 0.id: 5.name: "Group 2".number: "3".type: "group".children: [{parentId: 5.id: 6.name: "Group 2- Test 1".number: "2".type: "child"
      },
      {
        parentId: 5.id: 7.name: "Group 2- Test 2".number: "1".type: "child"}]}, {parentId: 0.id: 8.name: "Test child - 1".number: "3".type: "child"
  },
  {
    parentId: 0.id: 9.name: "Test child - 2".number: "2".type: "child"}];Copy the code
//app.js

import React, { useState, useCallback, useRef } from "react";
import { Table } from "antd";
import "antd/dist/antd.css";

import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";

import { columns, tableData } from "./utils/data";
import { DraggableBodyRow } from "./comp/row";

const App = () = > {
  const [data, setData] = useState(tableData);

  const components = {
    body: {
      row: DraggableBodyRow //}};const moveRow = useCallback(
    (props) = >{...// This is where the data is dragged and dropped
    },
    [data]
  );

  return (
    <div>
      <DndProvider backend={HTML5Backend}>
        <Table
          columns={columns}
          dataSource={data}
          components={components}
          rowKey={(record)= >Record. id} onRow={(record, index) => ({record, // current data data, // complete data index, // current data index moveRow // change data method})} /></DndProvider>
    </div>
  );
};

export default App;
Copy the code

4, implementation,DraggableBodyRowThis component

You can see that components override the default table element. Here we need to override the row in the body.

Some constants to prepare for:

//common.js

export const ItemTypes = "DraggableBodyRow";

// Operation type
export const optionsTyps = {
  didDrop: "didDrop".// Drag out the area
  hover: "hover".drop: "drop" / / place
};

// Data type
export const dataType = {
  group: "group".child: "child"
};
Copy the code

Pay special attention to using two methods in React-Dnd:

1) useDrag

UseDrag is a hook method that provides a way for your component to connect to a DnD system as a drag source. Passing in the document to useDrag specifies the parameters to pass, declaring a line description of the type that is being generated for drag, the item object representing the drag source, the properties of COLLECT, and so on. The useDrag method returns parameters: a set of properties collected by the collect function, as well as drag sources and drag preview elements

const [collected, drag, dragPreview] = useDrag(() = > ({
    type,
    item: { id }
}))
Copy the code

2) useDrop

UseDrop is a hook method that provides a way for your components to connect to a DnD system as placement targets. The document passed to useDrop specifies the parameters to pass, you can specify what type of data items to let the placement target accept, the properties of COLLECT, and so on. UseDrop returns an array containing the node to place the target on and the properties collected by the collect function.

const [collectedProps, drop] = useDrop(() = > ({
    accept
  }))
Copy the code

Concrete implementation:

// row.js

import React, { useRef } from "react";
import { useDrag, useDrop } from "react-dnd";
import { dataType, ItemTypes, optionsTyps } from ".. /utils/common";

export const DraggableBodyRow = (props) = > {
  let {
    record, // Current row data
    data,  // Complete data
    index, // Index of current row data
    className,
    style,
    moveRow, // The method of modifying data after movingfindRow, ... restProps } = props;if(! record)return null;

  let itemObj = {
    id: record.id,
    parentId: record.parentId,
    index,
    isGroup: record.type === dataType.group,
  };

  let isDrag = true; // All rows can be dragged, so there is no judgment limit

  const ref = useRef();

  // useDrop is a hook method that provides a way for your components to connect to a DnD system as placement targets.
  const [{ handlerId, isOver, dropClassName }, drop] = useDrop({
    accept: ItemTypes, // react only when type of useDrag is ItemTypes
    collect: (monitor) = > {
      const {
        id: dragId,
        parentId: dragParentId,
        index: dragPreIndex,
        isGroup
      } = monitor.getItem() || {}; // Get the same data as itemObj

      // If the dragged ID is equal to the current row, it is not processed
      if (dragId === record.id) {
        return {};
      }

      // Can drag and drop to replace
      let isOver = monitor.isOver();
      if (isGroup) {
        // The data to be overwritten is grouped, or the outermost child is replaceable, but not otherwise
        let recordIsGroup = record.type === dataType.group;
        if(! recordIsGroup) { isOver =false; }}else {
        // The data to be overwritten is a subitem, but not in the same group can not be replaced
        if(dragParentId ! == record.parentId) { isOver =false; }}return {
        isOver, // Whether to override
        dropClassName: "drop-over-downward".// Drag the hover style
        handlerId: monitor.getHandlerId()
      };
    },
    drop: (item) = > { // 
      let opt = {
        dragId: item.id, / / drag the id
        dropId: record.id, // The id of the row to be placed
        dropType: record.type,
        dropParentId: record.parentId,
        operateType: optionsTyps.drop
      };
      moveRow(opt); // Call the passed method to complete the data modification}});// useDrag is a hook method that provides a way for your component to be connected to the DnD system as a drag source.
  // isDragging is a attribute collected and deconstructed through Collect
  const [{ isDragging }, drag] = useDrag({
    type: ItemTypes, // Drag type
    item: itemObj, / / drag source
    collect: (monitor) = > ({ / / collector
      isDragging: monitor.isDragging() // CSS styles are required})});// ref This is done so that the component can be dragged as well as dragged
  drop(drag(ref));

  // The position of the drag row is transparent
  const opacity = isDragging ? 0 : 1;

  return (
    <tr
      ref={ref}
      className={` ${className}
      ${isOver ? dropClassName :"" ${}isDrag ? "can-drag" :""} `}style={isDrag ? { cursor: "move", opacity.. style } : { . style }}
      data-handler-id={handlerId}
      {. restProps} / >
  );
};

Copy the code

5, drag some details to do processing

When we drag, we have two details to deal with. The effect is shown below:

1) Every time you drag another row, you get pushed down or up.

The hover parameter needs to be added to the useDrop method, which is implemented by referring to the react-dnd example

// row.js

const [{ handlerId, isOver, dropClassName }, drop] = useDrop({
    accept: ItemTypes,
    collect: (monitor) = >{... },hover: (item, monitor) = > {
      if(! ref.current) {return;
      }
      const dragIndex = item.index;
      const dropIndex = index;
      // Don't replace items with themselves
      if (dragIndex === dropIndex) {
        return;
      }

      let opt = {
        dragId: item.id, / / drag the id
        dropId: record.id, // The id of the row to be placed
        dropType: record.type,
        dropParentId: record.parentId,
        operateType: optionsTyps.hover / / hover operations
      };

      moveRow(opt);
      // 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 = dropIndex;
    },
    drop: (item) = >{... }});Copy the code

2) And drag out of the drag area after releasing the mouse remains unchanged in the initial position.

UseDrag = useDrag = useDrag

// row.js

const [{ isDragging }, drag] = useDrag({
    type: ItemTypes,
    item: itemObj,
    collect: (monitor) = > ({
      isDragging: monitor.isDragging()
    }),
    end: (item, monitor) = > {
      const { id: droppedId, originalRow } = item;
      const didDrop = monitor.didDrop();
      // Out of the dragable zone, you need to restore the dragable row
      if(! didDrop) {let opt = {
          dragId: droppedId, / / drag the id
          dropId: originalRow.id, // The id of the row to be placed
          dropType: originalRow.type,
          dropParentId: originalRow.parentId,
          originalIndex,
          originalParentIndex,
          operateType: optionsTyps.didDrop }; moveRow(opt); }}});Copy the code

To restore the original location, we need to add a new method findRow to app.js

// App.js

const App = () = > {
  const [data, setData] = useState(tableData);

  const components = {
    body: {
      row: DraggableBodyRow
    }
  };

  const findRow = (id) = > {
    // Query the corresponding data information and index according to the id
    const { row, index, parentIndex } = findFromData(tableData, id);
    return {
      row,
      rowIndex: index,
      rowParentIndex: parentIndex
    };
  };

  const moveRow = useCallback(
    (props) = >{... }, [data] );return (
    <div>
      <DndProvider backend={HTML5Backend}>
        <Table
          columns={columns}
          dataSource={data}
          components={components}
          rowKey={(record)= > record.id}
          onRow={(record, index) => ({
            record,
            data,
            index,
            moveRow,
            findRow
          })}
        />
      </DndProvider>
    </div>
  );
};
Copy the code

6. Implement moveRow

Drag and drop data need to be processed in different situations, including the following:

  • For group drag sorting processing
  • For subitem drag sorting processing
  • Groups and sub-items at the same level of a group, drag and drop sorting also requires special treatment

Note: Quart group drag and drop sorting is not currently supported

//App.js.const moveRow = useCallback(
    (props) = > {
      let {
        dragId, / / drag the id
        dropId, / / set id
        dropParentId, // Place the parent ID
        operateType, / / operation
        originalIndex // Original index
      } = props;

      let {
        dragRow, / / drag and drop the row
        dropRow, / / set the row
        dragIndex, // Drag index
        dropIndex, // Place the index
        dragParentIndex, // Drag the index of the child's parent node
        dropParentIndex // Place the index of the child node's parent node
      } = getParam(data, dragId, dropId);

      // Whether drag is a group
      letdragIsGroup = dragRow.type === dataType.group || ! dragRow.parentId;// Whether the group is placed
      letdropIsGroup = ! dropParentId;// Find the row and index of the dragged row based on the changed data
      const {
        row,
        index: rowIndex,
        parentIndex: rowParentIndex
      } = findFromData(data, dragId);

      let newData = data;
      / / set of drag and drop
      if (dragIsGroup && dropIsGroup) {
        // Restore beyond the drag area
        if (operateType === optionsTyps.didDrop) {
          newData = update(data, {
            $splice: [
              [rowIndex, 1].// Delete the data of the currently dragged index
              [originalIndex, 0, row] // Insert drag data into the original index location]}); }else {
          // Change the drag position
          newData = update(data, {
            $splice: [
              [dragIndex, 1],
              [dropIndex, 0, dragRow] ] }); }}// Subitem drag under the same group
      else if(dragRow.parentId === dropRow? .parentId) {// Restore beyond the drag area
        if (operateType === optionsTyps.didDrop) {
          newData = update(data, {
            [dragParentIndex]: {
              children: {
                $splice: [
                  [rowIndex, 1],
                  [originalIndex, 0, row]
                ]
              }
            }
          });
        } else {
          // Change the drag position
          newData = update(data, {
            [dragParentIndex]: {
              children: {
                $splice: [
                  [dragIndex, 1],
                  [dropIndex, 0, dragRow] ] } } }); } } setData(newData); }, [data] ); .Copy the code

Click to view the above code demo address

reference

  • react-dnd
  • Antd table drag sort
  • React drag-and-sort component library comparison study
  • Drag component React-dnd drag sorting use
  • React-dnd