react-dnd: drag out a line

2.6k views Asked by At

I am building a component that allows the user to build a graph with drag-n-drop. There is a node well where they can drag out new nodes, they can drag the nodes around the canvas, etc.

Now, I need to allow them to create edges by dragging from one node's output to the next node's input side. Strictly speaking this isn't drag-n-drop, because the draggable stays put and instead a line is to be displayed originating from the draggable and filling under the cursor, until eventually, when the user is releases while hovering over an active target, the edge is completed.

Drag-n-drop seems to do almost everything I want. It has the hover signal, highlighting drop targets when an eligible Draggable is dragging, and so forth. There are two things I can't figure out how to do. One is stop the draggable from moving at all. I can trick it by placing two copies of the element, one underneath the other, and then disabling the drag preview, but if there's a simple flag that would be better.

The other one seems like more of a show-stopper. The collect function doesn't continuously fire events as I drag (I know, by design). I need something that fires onMouseMove to keep updating the line. Since drag-n-drop does do some stuff I need, and since I've already incurred the size cost of depending on it, it'd be great to reuse it.

The best idea I've had so far is to install an onMouseMove handler on beginDrag and cleaning up the line on endDrag, establishing any new edge on drop. Unfortunately, I think something is stopping propagation of mousemove events, because my handler never fires even though I do enter the beginDrag here when I start dragging.

     let mouseMoveHandler = (ev: JQueryMouseEventObject) => {
       console.log("Draw a line from ", node.position, " to ", { x: ev.clientX, y: ev.clientY });
     };
     console.log("Dragging");
     $("body").on("mousemove", mouseMoveHandler);
     return { id, node, mouseMoveHandler: mouseMoveHandler};
  },
  endDrag: ({id, node}, monitor: DragSourceMonitor) => {
    const item = monitor.getItem() as any;
    $("body").off("mousemove", item.mouseMoveHandler);
  }
1

There are 1 answers

1
akraines On BEST ANSWER

The project I'm currently working on involves the same thing (drawing a graph, connect edges with react-dnd) and ran into the same problem. However I noticed that your assumption that

The collect function doesn't continuously fire events as I drag

is incorrect. At first I thought so too, but then I realized that the reason the display stuttered is because my "canvas" render function was too heavy. I solved the problem by following the directions on how to optimise my react render function but dividing out my nodes layer and links layer into separate sub components that only re-render if they changed. This made my main render function much lighter. Then when I rendered my "pending" link. it followed my mouse cursor beautifully, without a stutter / jank.

Here's what is retruned from my render function:

render() {
    /* 
    Don't do anything heavy in the rendering function (incl, nodelayer and link layer) - even a console.log.
    Otherwise the pending link render will b e choppy. 
    */
    const { renderPendingLink } = this;
    const { connectDropTarget } = this.props;
    const canvas = connectDropTarget(<div style={{ width: '100%', height: '1000px' }} ref={ref => this._canvasRef = ref}>
        <div style={{ position: 'absolute', width: '100%', height: '100%' }}>
            <NodesLayer nodes={this.nodesValuesArray} createLink={this.createLink} />
            <svg style={{ width: '100%', height: '100%' }}>
                <LinksLayer links={this.linksValuesArray} />
                {renderPendingLink()}
            </svg>
        </div>
    </div>)
    return (<div>
        <PanelToolboxWidget />
        {canvas}
    </div>)
}
}

And:

renderPendingLink() {
    const { item, itemType, isDragging } = this.props;
    if (isDragging && itemType == ItemTypes.PORT) {
        const { port } = item;
        const { currentOffset } = this.props;
        if (!currentOffset | !this._canvasRef) {
            return null;
        }
        return (
            <PendingLinkWidget
                start={port.getPortCenterRelativeToCanvas}
                end={getLocalisedDropCoords(currentOffset, this._canvasRef)}
            />
        )
    }
    return null;
}

The LinksLayerWidget looks like this (I'm using mobX):

@observer class LinksLayer extends React.PureComponent {
render() {
    const { links } = this.props;
    return (<svg>
        {_.map(links, l => <LinkWidget link={l} key={l.uuid} />)}
    </svg>)
}

}

It works for me, and I hope it helps you.