background

In my work, I encountered a project that needed to realize the drag adjustment order, so I investigated the implementation of drag sorting.

A freestanding implementation

Related events

HTML5 provides a drag and drop API. To implement a simple drag and drop function, you only need to use the following three events

  • Dragstart: Performs some processing at the start of a drag, such as recording the dragged element, adding some styles to the dragged element, etc
  • Dragover: Controls how elements are exchanged during element movement
  • Dragend: Clears the event set at the start of the drag at the end of the drag
A simple implementation
// Reorder the list according to where the element is dragged to achieve dynamic effect
const updateList = useCallback(
  (positionX, positionY) = > {
    constwrapperRect = dragWrapperRef.current? .getBoundingClientRect();if(! wrapperRect) {return;
    }
    const offsetX = positionX - wrapperRect.left;
    const offsetY = positionY - wrapperRect.top;
    if(! dragItemRef || ! dragItemRef.current || offsetX <0 ||
      offsetY < 0 ||
      offsetX > wrapperRect.width ||
      offsetY > wrapperRect.height
    ) {
      return null;
    }
    const newIndex =
      Math.floor(offsetX / (ITEM_MARGIN * 2 + ITEM_WIDTH)) +
      Math.floor(offsetY / (ITEM_MARGIN * 2 + ITEM_HEIGHT)) * COLUMN;
    let copy = [...dataList];
    let dragIndex = dataList.indexOf(dragItemRef.current);
    if (dragIndex < 0) {
      return null;
    }
    copy.splice(dragIndex, 1);
    if (dragIndex < newIndex) {
      copy.splice(newIndex + 1.0, dragItemRef.current);
    } else {
      copy.splice(newIndex, 0, dragItemRef.current);
    }
    setDataList(copy);
  },
  [dataList]
);

// Get a reference to the drag element and style the drag background as it is dragged
const handleDragStart = (event: DragEvent<HTMLElement>, item: DragDataItem) = > {
  dragItemRef.current = item;
  constel = dragWrapperRef.current? .querySelector(`[data_id="data_id_${item.id}"] `
  );
  if(dragWrapperRef) { el.classList.add(styles.dragItemActive); }};// Update the list when the element is dragged, switching positions
const handleDragOver = useCallback(
  (event: DragEvent<HTMLElement>) = > {
    event.preventDefault();
    updateList(event.clientX, event.clientY);
  },
  [updateList]
);

// After the drag ends, clear the Settings where the drag started
const handleDragEnd = (event: DragEvent<HTMLElement>) = > {
  const item = dragItemRef.current;
  constel = dragWrapperRef.current? .querySelector(`[data_id="data_id_${item.id}"] `
  );
  if (el) {
    el.classList.remove(styles.dragItemActive);
  }
  dragItemRef.current = null;
};

Copy the code
Implementation effect

disadvantages
  1. Poor encapsulation, if you want to do a general component needs to be encapsulated
  2. You have to set your own style, you have to write your own dynamic effects, right
  3. In order to achieve the ideal effect, we need to carry out a lot of development, large amount of code, long development cycle

react-dnd

The official documentation

The installation
npm i -s react-dnd react-dnd-html5-backend
Copy the code
The principle of

React-dnd is a higher-order component library that wraps child components with apis to make them drag-and-drop

  1. Item is a pure JS object that describes the drag component
  2. Type represents the type of item, indicating which drag sources can be placed on which targets
  3. The Monitor object exposes the drag state of a component, and its API lets you update the props of the component when the drag state changes
  4. The Connector object defines the component that the event listens to, and binds the component to predefined roles such as drag source and drag target. The result returned is memorized and does not break shouldComponentUpdate optimization
  5. Collect defines functions that get the state from monitor and the object from connector. React-dnd periodically calls Collect and merges its return value, passing it to the props of the component to decide to inject into the props of the component
  6. DragSource drags sources, registers as specific types, and returns specific components according to props
  7. DropTarget is similar to dragSource, except that it can register multiple types and handle hover or drop of components of multiple types
  8. The drag-and-drop event API used by Backends is HTML5 by default, but has compatibility issues and is not touch and IE friendly
  9. Hook API: useDrag, useDrop and useDragLayer
A simple implementation
const Demo: FunctionComponent = () = > {
  const [componentList, setComponentList] = useState(ComponentList)
  // Sort methods
  const moveCard = useCallback((dragIndex, hoverIndex) = > {
    let copy = [...componentList]
    let item = copy[dragIndex]
    copy.splice(dragIndex, 1)
    copy.splice(hoverIndex, 0, item)
    setComponentList(copy)
  }, [componentList])
  return (
    <div className={s.dndWrapper}>
      <DndProvider backend={HTML5Backend}>
      <div className={` ${s.dropAreaWrapper} wrapper`} >
        {
          componentList.map((item, index)=>{
            return <ComponentListItem key={item.id} id={item.id} index={index} text={item.text} index={index} moveCard={moveCard}/>})}</div>
      </DndProvider>
    </div>)}export default Demo

// Base drag component
const ComponentListItem: FunctionComponent = ({id, text, index, moveCard}) = > {
  const ref = useRef(null)
  const [{handlerId}, drop] = useDrop({
    accept: ComponentDropType,
    // Implement the dynamic effect of real-time switching
    hover(item, monitor) {
      if(! ref.current){return null
      }
      const dragIndex = item.index
      const hoverIndex = index
      if(dragIndex === hoverIndex){
        return
      }
      consthoverBoundingRect = ref.current? .getBoundingClientRect();const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
      const clientOffset = monitor.getClientOffset();
      const hoverClientY = clientOffset.y - hoverBoundingRect.top;
      if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
        return;
      }
      if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
        return; } moveCard(dragIndex, hoverIndex); item.index = hoverIndex; }})const [{ isDragging }, drag] = useDrag({
    type: ComponentDropType,
    item: () = > {
      return { 
        id, 
        index, 
        itemType: ComponentDropType, 
      }
    },
    collect: (monitor) = > ({
      isDragging: monitor.isDragging(),
    }),
  });
  const opacity = isDragging ? 0 : 1;
  drag(drop(ref));
  return (<div ref={ref} style={{ opacity }} data-handler-id={handlerId} className={s.componentListItem}>
    {text}
  </div>);
}
Copy the code
Implementation effect

advantages
  1. Can be used to drag irregular elements, a wide range of application
  2. Strong scalability, can achieve complex drag and drop requirements
disadvantages
  1. Learning costs are high, there are too many things you need to implement yourself, and the development cycle is long
  2. There is no default style, you need to customize the animation and so on

react-beautiful-dnd

The official documentation

The installation
npm install react-beautiful-dnd --save
Copy the code
The principle of

React drag-and-drop component library for lists. It supports mouse and keyboard events. It is easy to use and abstract. It mainly contains the following three concepts:

  1. DragDropContext: Outermost wrapper component, drag-and-drop background, no nesting supported
  2. Droppable: Area where drag components can be placed, wrapping outer components of Draggable components
  3. Draggable: Drag-and-drop components that must be placed inside the Droppable component
A simple implementation
const Demo: FC = () = > {
  const [dropDataSource, setDropDataSource] = useState(DataSourceGenerator(12));

  const onDragStart = () = > {};
  const onDragUpdate = (result, ... props) = > {
    const { source, destination } = result;
  };
  // Just do some work at the end of the drag. The library of elements that move during the drag is all taken care of
  const onDragEnd = (result) = > {
    const { source, destination } = result;
    if(! destination) {return;
    }
    const fromIndex = source.index;
    const fromDropId = source.droppableId;
    const toIndex = destination.index;
    const toDropId = destination.droppableId;
    // Determine if the drag source is the target drag source
    if (fromDropId == DropAreaId && toDropId == DropAreaId) {
      const copy = [...dropDataSource];
      const [from] = copy.splice(fromIndex, 1);
      copy.splice(toIndex, 0.from); setDropDataSource(copy); }};return (
    <DragDropContext
      onDragStart={onDragStart}
      onDragUpdate={onDragUpdate}
      onDragEnd={onDragEnd}
    >
      <Droppable droppableId={DropAreaId}>
        {(provided, snapshot) => {
          return (
            <div
              key="REACT_BEAUTIFUL_DND_DROP_WRAPPER"
              ref={provided.innerRef}
              className={` ${s.dragComponentWrapper} ${
                snapshot.isDraggingOver ? s.dragComponentWrapperActive : ""
              }`}
              {. provided.droppableProps}
              style={{ height: 45 * dropDataSource.length + 20 }}
            >
              {dropDataSource.map((item, index) => {
                return (
                  <>
                    <Draggable
                      key={` ${item.id}_drop`}
                      draggableId={` ${item.id}_drop`}
                      index={index}
                    >
                      {(provided, snapshot) => {
                        return (
                          <>
                            <div
                              key={` ${item.id}_drop`}
                              ref={provided.innerRef}
                              className={` ${s.dragComponentItem} ${
                                snapshot.isDragging
                                  ? s.dragComponentItemActive
                                  : ""
                              }`}
                              {. provided.dragHandleProps}
                              {. provided.draggableProps}
                            >
                              <span key={1} className={s.dragItemIndex}>
                                [{index}]
                              </span>
                              <span key={2} className={s.dragItemContent}>
                                {item.title}
                              </span>
                            </div>
                          </>
                        );
                      }}
                    </Draggable>
                  </>); })} {/* Automatically expand the settable area when dragging to the end of the settable area */} {provided.</div>
          );
        }}
      </Droppable>
    </DragDropContext>
  );
};
Copy the code
Implementation effect

advantages
  1. Out of the box, good encapsulation, support rapid development
  2. The animation is well handled and the drag process is very smooth
disadvantages
  1. The application scope is limited, only supports the list form, does not support the irregular area drag

conclusion

After using the above three ways to achieve drag sorting, I choose React-beautiful-Dnd to complete the project, because the use scenario is list drag, there are no other complex drag scenarios, and based on its powerful functions, the project can speed up the schedule.