preface

The React drag-and-drop function is often encountered in our development process, and there are many different scenarios for the drag-and-drop function. Today, we will implement a simple, universal drag-and-drop component step by step to solve the requirements of element drag-and-drop.

Application scenarios

First, let’s take a look at the final application of the drag-and-drop component we implemented:

Drag and drop elements to reposition

Using the drag-and-drop component we encapsulated, we can adjust the position and order of elements by dragging and dropping:

Drag and drop elements to copy to another region

Using our encapsulated drag and drop component, we can also make elements complex from one region to another:

Of course, in addition to the two application scenarios shown above, we can use this drag component to meet a variety of drag requirements.

So, next, we’ll focus on implementing a simple, universal React drag component.

Drag and drop API

HTML provides a number of apis for implementing Drag and Drop, which are the building blocks for implementing Drag and Drop components.

Implementing a drag component requires our element to have two properties, drag and drop.

Drag the

The drag related events defined by HTML are:

  • ondragTriggered when elements are dragged.
  • ondragendTriggered when the drag operation ends.
  • ondragenterTriggered when an element is dragged to a releasable target.
  • ondragexitTriggered when an element becomes no longer the selected target of a drag operation.
  • ondragleaveTriggered when drag elements leave a releasable target.
  • ondragoverTriggered when an element is dragged onto a releasable target (every 100 milliseconds).
  • ondragstartTriggered when the user starts dragging an element.

To give an element the Drag property, you simply add the draggable attribute to the element in response to drag-related events on the element.

Drop the related

HTML defines only one event related to drop:

  • ondropEmitted when an element is released on a releasable target.

To make an element drop, you need to add a listener for the onDrop event and onDragover event on the element.

The dataTransfer object

In addition, related to drag and drop is a dataTransfer object, which is present in all drag and drop related event objects, dragEvents.

Set the Dropeffct of the dataTransfer to control how the mouse appears when dragging elements over droppable elements.

The Dropeffct property can be set to:

  • move
  • copy
  • link
  • none

By setting appropriate values for dropeffct in different drag-and-drop application scenarios, you can render a better visual effect.

Please refer to the official API documentation for details.

In our React component, we use onDragStart, onDragEnd, onDragover, onDragLeave, and onDrop events.

Drag and drop component design

The React drag component is mainly composed of four parts: draggableConnect, droppableConnect, DndComponent and DndManager.

  • DndManager: manager that manages data, state, and interactions of drag and drop components.
  • DndComponent: container for dragable elements. It is the body of dragable components and supports drag-related configurations.
  • draggableConnect: in order toReactElement to adddragAssociated properties and events.
  • droppableConnect: in order toReactElement to adddropAssociated properties and events.

DndManager

DndManager consists of DndManagerContext and DndManager components.

During drag, all draggable elements can be defined as sources, and all Droppable elements can be defined as targets.

DndManagerContext

In creating the DndManagerContext, we used our React Context knowledge. Context can provide a shared “global” data for a component tree, and components in the tree can consume this “global” data without having to pass it layer by layer through props. Context

DndManagerContext should contain management of all source and target collections, as well as the state of the drag process and the elements currently being dragged. So in DndManagerContext, you need to define sourceMap and targetMap, add and delete methods, drag and drop the result record and change the record method changeResult.

export enum EDragResultStatus {
  DRAG = 'DRAG',
  DROP = 'DROP',
  CANCEL = 'CANCEL',}export type sourceId = string | null;
export type targetId = string | null;
export type dropMode = DataTransfer['dropEffect'];
export type source = any;
export type target = any;

export type sourceMap = Record<string, source>;
export type targetMap = Record<string, target>;

export interface IResult {
  sourceId: sourceId;
  targetId: targetId;
  status: EDragResultStatus;
  hoverId: targetId;
}

export interface IDndManager {
  dropMode: dropMode; sourceMap: sourceMap; targetMap: targetMap; result? : IResult; changeResult? :(result: Partial<IResult>) = > void;
  addSource: (sourceId: sourceId, source: source) = > void;
  removeSource: (sourceId: sourceId) = > void;
  addTarget: (targetId: targetId, target: target) = > void;
  removeTarget: (targetId: targetId) = > void;
}

Copy the code

First, we create a DndManagerContext with React. CreateContext and set the initial value to defaultContext.

import React from 'react';
import { IDndManager } from './type';

// Initial value of DndManagerContext
const defaultContext: IDndManager = {
  dropMode: 'move'.sourceMap: {},
  targetMap: {},
  addSource: console.log,
  removeSource: console.log,
  addTarget: console.log,
  removeTarget: console.log,
};

// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
export const DndManagerContext = React.createContext<IDndManager>(defaultContext);
Copy the code

DndManager Component

Next, we need to create the DndManager component, we need to give the DndManagerContext valid values and methods in the DndManager component, With context.provider, any component in the component tree can subscribe to the contents of DndManagerContext.

The DndManager Component accepts dropMode and onDragEnd as props. DropMode supports external Settings for drag modes, and the onDragEnd method is executed when the drag state is’ DROP ‘.

Each Context object returns a Provider React component, which allows the consuming component to subscribe to changes to the Context

interface Props {
  children: React.ReactNode;
  onDragEnd: (result: IResult) = > void; dropMode? : dropMode; }interface State {
  dropMode: dropMode;
  sourceMap: sourceMap;
  targetMap: targetMap;
  result: IResult;
}

export default class DndManager extends Component<Props.State> {
  // value of Context 
  state: State = {
    dropMode: this.props.dropMode || 'move'.sourceMap: {},
    targetMap: {},
    result: {
      targetId: null.sourceId: null.status: null.hoverId: null,}};// change the result of dnd
  public changeResult(result: Partial<IResult>) {
    const { onDragEnd } = this.props;
    constnewResult = { ... this.state.result, ... result };// when the status is 'drop', trigger 'onDragEnd' event
    if (result.status && result.status === DragResultStatusEnum.DROP) {
      onDragEnd(newResult);
    }
    this.setState({ ... this.state,result: newResult });
  }
  // add a source that draggable elemnt
  public addSource(sourceId: sourceId, source: source){... }// add a target that droppable elemnt
  public addTarget(targetId: targetId, target: target){... }// remove a source 
  public removeSource(sourceId: sourceId){... }// remove a target
  public removeTarget(targetId: targetId){... }// get the value of dropMode from props and set to the state
  componentWillReceiveProps(nextProps: Props) {
    const { dropMode } = this.state;
    if(dropMode ! == nextProps.dropMode) {this.setState({
        dropMode: nextProps.dropMode, }); }}render() {
    const { children } = this.props;

    return (
      // Every Context object comes with a Provider React component
      // that allows consuming components to subscribe to context changes
      <DndManagerContext.Provider
        value={{
          . this.state.addSource: this.addSource.bind(this),
          addTarget: this.addTarget.bind(this),
          removeSource: this.removeSource.bind(this),
          removeTarget: this.removeTarget.bind(this),
          changeResult: this.changeResult.bind(this),}} >
        {children}
      </DndManagerContext.Provider>); }}Copy the code

DndComponet

DndComponet is the main part of drag and drop components. It mainly has three responsibilities:

  • throughReact.useContext()To subscribe toDndManagerContext.
  • toDndManagerreportsourcetargetThe change.
  • judgesourceIdtargetId, the implementation ofdraggableConnectdroppableConnect.

export interface IDragDropProps {
  children: React.ReactElement sourceId? :string; targetId? :string;
}

const DndComponent = ({ children, sourceId, targetId }: IDragDropProps): React.ReactElement => {
  // get ref
  const dndContainerRef = React.useRef<HTMLElement>();
  // subscribe context 
  const dndManager = React.useContext(DndManagerContext);

  React.useEffect(() = > {
    // check targetId and sourceId and then execute dndManager.add
    if (targetId) {
      dndManager.addTarget(targetId, dndContainerRef);
    }
    if (sourceId) {
      dndManager.addSource(sourceId, dndContainerRef);
    }
  }, [children, sourceId, targetId]);
  // when component unMount, execute dndManager.remove
  React.useEffect(() = > {
    return () = > {
      if (sourceId) dndManager.removeSource(sourceId);
      if(targetId) dndManager.removeTarget(targetId); }; } []);// dropabbleConnect or draggableConnect with children
  return cloneElement(
    dropabbleConnect(
      draggableConnect(
        children,
        sourceId,
        dndManager
      ),
      targetId,
      dndManager
    ),
    { ref: dndContainerRef }
  );
};
export default DndComponent;
Copy the code

draggableConnect

DraggableConnect uses React. CloneElement () to clone the target element and add drag related attributes to it, returning the React element with drag properties.

CloneElement clones the Element element and returns a new React element. The props of the element is the result of superficially merging the new props with the props of the original element. The new child element replaces the existing child element, and the key and ref from the original element are retained.

export function draggableConnect(element: React.ReactElement, sourceId: sourceId, dndManager: IDndManager) :React.ReactElement {... .return React.cloneElement(element, {
    draggable: true.onDragStart: dragStartHandler,
    onDragEnd: dragEndHandler,
  });
}
Copy the code

The added properties include Draggable, onDragStart, and onDragEnd.

OnDragStart event is the starting point of drag and drop process, in dragStartHandler, set the drag mode e.d ataTransfer. DropEffect, Set the drag state of dndManager to ‘drag’ and set the id of the current element to the sourceId for the entire drag process.

  const dragStartHandler = (e: React.DragEvent<HTMLDivElement>) = > {
    if (dndManager.dropMode) e.dataTransfer.dropEffect = dndManager.dropMode;
    // init status of dnd and set sourceId dndManager? .changeResult({status: DragResultStatusEnum.DRAG,
      sourceId: sourceId,
      hoverId: null.targetId: null}); };Copy the code

In dragEndHandler, determine the sourceId and targetId of dndManager.result. If both exist, it means that the element is normally dropped. Otherwise, the drag event is cancelled and the status is set to ‘Cancel’.

  const dragEndHandler = (e: React.DragEvent<HTMLDivElement>) = > {
    e.preventDefault();
    // Set different status values by judging the sourceId and targetId
    if(dndManager.result.sourceId && dndManager.result.targetId) { dndManager? .changeResult({status: DragResultStatusEnum.DROP,
        hoverId: null}); }else{ dndManager? .changeResult({status: DragResultStatusEnum.CANCEL,
        hoverId: null.sourceId: null.targetId: null}); }};Copy the code

droppableConnect

DroppableConnect uses React. CloneElement () to clone the target element and add the drop-related attributes onDrop, onDragOver, and onDragLeave to it, returning the React element with the drop feature.

export function dropabbleConnect(element: React.ReactElement, targetId: targetId, dndManager: IDndManager) :React.ReactElement {... .// clone element 
  return React.cloneElement(element, {
    onDrop: dropHandler,
    onDragOver: dragOverHandler,
    onDragLeave: dragLeaveHandler,
  });
}
Copy the code

OnDrop is triggered when the element finishes dropping, setting the targetId of the current dndManager drag to the Id of the current element in the dropHandler.

  const dropHandler = (e: React.DragEvent<HTMLDivElement>) = > {
    e.preventDefault();
    if (dndManager.dropMode) e.dataTransfer.dropEffect = dndManager.dropMode;
    // set targetId
    dndManager.changeResult({
      targetId: targetId,
    });
  };
Copy the code

OnDragOver and onDragLeave are triggered when a drag element enters and leaves a dropable element. The event listener is used to set the hoverId of dndManager.

  const dragOverHandler = (e: React.DragEvent<HTMLDivElement>) = > {
    e.preventDefault();
    if (dndManager.dropMode) e.dataTransfer.dropEffect = dndManager.dropMode;
    // set hoverId
    dndManager.changeResult({
      hoverId: targetId,
    });
  };
  const dragLeaveHandler = (e: React.DragEvent<HTMLDivElement>) = > {
    e.preventDefault();
    // clear hoverId
    dndManager.changeResult({
     hoverId: null}); };Copy the code

This completes the creation of the four main parts of the drag component, and implements the drag function through the DndManager component and DndComponent:

    <DndManager
      onDragEnd={(v) = > {
        console.log(v);
      }}
    >
      <div
        style={{
          display: 'flex',
          justifyContent: 'space-between',
          width: '300px',
          marginTop: '200px'}} >
        <DndComponent sourceId="source_1">
          <button>Source</button>
        </DndComponent>
        <DndComponent targetId="target_1">
          <button>Target</button>
        </DndComponent>
      </div>
    </DndManager>
Copy the code

However, this is not the end, there are still some cases to deal with.

Enhancements

When you drop a traget element into a drop-able traget element, you want to add specific styles to the target, such as changing the element’s background, as shown below:

To do this, we need to determine whether the current element hover during drag. In dndManager.result, there is information about the hover element: hoverId. By judging the hoverId and the id of the current element, you can determine whether the element is the hover element, that is, isDragOver.

{ isDragOver: dndManager.result.hoverId === targetId }
Copy the code

In addition, you need to be able to get the isDragOver value in a child element within the DndComponent.

How do I get the contents of a component in its children? To do this, have the DndComponent support children of the function type, with isDragOver as the argument of the function.

interface IDragDropProps {
  children: React.ReactElement | (({ isDragOver }: { isDragOver: boolean }) = >React.ReactElement); . }constDndComponent = ({ children, sourceId, targetId }: IDragDropProps): React.ReactElement => { ... .return cloneElement(
    dropabbleConnect(
      draggableConnect(
        // Determine the type of children
        typeof children === 'function'
          // Pass isDragOver to children
          ? children({ isDragOver: dndManager.result.hoverId === targetId }) 
          : children,
        sourceId,
        dndManager
      ),
      targetId,
      dndManager
    ),
    { ref: dndContainerRef }
  );
};

Copy the code

This allows us to get the isDragOver property in the target element and implement the effects.

<DndComponent targetId="target_1">
  {({ isDragOver }) = > (
    <button style={{ background: isDragOver ? '#e6f7ff' : '#fff' }}>Target</button>
  )}
</DndComponent>
Copy the code

Boundary case processing

We also found a problem in our component,targetThe element responded differentlymanagersourceElements of thedropEvents.

When drag components belong to different managers, it usually means that they have different drag behavior, so there should be no interaction between drag components managed by different managers.

Fixing this requires adding a sourceId determination to the target element’s event.

  const dropHandler = (e: React.DragEvent<HTMLDivElement>) = > {
    e.preventDefault();
    const { sourceId } = dndManager.result;
    // check sourceId
    if (sourceId && dndManager.sourceMap[sourceId]) {
      if (dndManager.dropMode) e.dataTransfer.dropEffect = dndManager.dropMode;
      dndManager.changeResult({
        targetId: targetId, }); }};const dragOverHandler = (e: React.DragEvent<HTMLDivElement>) = > {
    e.preventDefault();
    const { sourceId } = dndManager.result;
    // check sourceId
    if (sourceId && dndManager.sourceMap[sourceId]) {
      if (dndManager.dropMode) e.dataTransfer.dropEffect = dndManager.dropMode;
      dndManager.changeResult({
        hoverId: targetId,
      });
    } else {
      e.dataTransfer.dropEffect = 'none'; }};const dragLeaveHandler = (e: React.DragEvent<HTMLDivElement>) = > {
    e.preventDefault();
    const { sourceId } = dndManager.result;
    // check sourceId
    if (sourceId && dndManager.sourceMap[sourceId]) {
      dndManager.changeResult({
        hoverId: null}); }};Copy the code

Drag effect after solution:

conclusion

In this article we have developed a simple and generic drag-and-drop component design with the drag-and-drop API. In the process, we have summarized several key points:

  • Keeping the components simple and exposing only manager and DND components to the outside can satisfy a variety of requirements.
  • Manager is used to manage the drag and drop behavior and state of multiple components in a unified manner.
  • Implementing a manager using a Context has two advantages:
    • Use Manager as a global variable in the component tree
    • You don’t care where you drag and drop components in the entire component tree, which means you can combine internal elements as you like
  • With cloneElement, you can extend an element by adding additional attributes without affecting its existing attributes.
  • DraggableConnect and droppableConnect remain separate, and elements can have either a single feature or both.