reaflow icon indicating copy to clipboard operation
reaflow copied to clipboard

[Feature Request] Implement `onPortLink`

Open emil14 opened this issue 4 years ago • 7 comments

My case is very simple - I need to link two ports between two nodes. How one should do it? This is what I tried to do:

<rf.Canvas
    nodes={nodes}
    edges={edges}
    onNodeLink={(event, from, to) => {
    // no `fromPort` and `toPort` here; does not run on port links
    console.log({ from, to })
    }}
    node={nodeProps => (
    <rf.Node
        dragType="port"
        port={
        <Port
            onEnter={(event, port) => 
            setTargetPortId(port.id)
            }}
            onLeave={(event, port) => {
            setTargetPortId(null)
            }}
            onDragStart={(event, pos, port) => {
            setSrcPortId(port.id)
            }}
            onDragEnd={(event, pos, port) => {
            setSrcPortId(null)
            setTargetPortId(null)
            }}
        />
        }
    />

First I thought I can do it with onDragEnd but there is no node there

emil14 avatar Oct 25 '21 23:10 emil14

I think I figured it out

// const defaultNodes = ...
// const defaultEdges = ...

function PortDraggingExample(props) {
  // dragging state
  const [srcNodeId, setSrcNodeId] = useState(null);
  const [srcPortId, setSrcPortId] = useState(null);
  const [targetNodeId, setTargetNodeId] = useState(null);
  const [targetPortId, setTargetPortId] = useState(null);

  // graph state
  const [nodes, setNodes] = useState(defaultNodes);
  const [edges, setEdges] = useState(defaultEdges);

  return (
    <Canvas
      nodes={nodes}
      edges={edges}
      node={(nodeProps) => (
        <Node
          dragType="port"
          onEnter={(_event, node) => setTargetNodeId(node.id)}
          port={
            <Port
              onEnter={(_event, port) => setTargetPortId(port.id)}
              onLeave={(_event, _port) => setTargetPortId(null)}
              onDragStart={(_event, _pos, port) => {
                setSrcNodeId(nodeProps.id);
                setSrcPortId(port.id);
              }}
              onDragEnd={(_event, _pos, _port) => {
                setSrcPortId(null);
                setTargetPortId(null);
                setEdges([
                  ...edges,
                  {
                    id: `${srcNodeId}.${srcPortId}-${targetNodeId}.${targetPortId}`,
                    from: srcNodeId,
                    fromPort: srcPortId,
                    to: targetNodeId,
                    toPort: targetPortId,
                  },
                ]);
              }}
            />
          }
        />
      )}
    />
  );
}

emil14 avatar Oct 26 '21 00:10 emil14

  1. It would be great to add this example to docs
  2. Would be great to have simpler API for that :)

emil14 avatar Oct 26 '21 00:10 emil14

@emil14 - Glad to hear you were able to figure it out.

  1. Absolutely - Could you make a PR to add what you think is missing?
  2. The API design is so tricky - the goal of what we implemented was to make it declarative and interchangeable. The alternative would be a bunch of top level props which would get messy too. I agree its not the most clear - I'm open to suggestions!

amcdnl avatar Oct 26 '21 12:10 amcdnl

  1. As soon as I'll be sure this approach does the trick, I need a bit more testing for now
  2. I see 2 ways to implement this:

first way is to add onPortLink prop and second is to extend onNodeLink and pass fromPort/toPort properties

emil14 avatar Oct 26 '21 21:10 emil14

  1. I tested code and found a few bugs, now I have a working solution. Will add example to docs later

emil14 avatar Oct 27 '21 09:10 emil14

@emil14 Can you share your working solution snippet for our reference. I am also trying to implement a similar functionality.

rishab-sharma avatar Jan 30 '22 19:01 rishab-sharma

@rishab-sharma here's a piece from my project

function NetworkEditor(props: NetworkProps) {
  const [selections, setSelections] = useState<string[]>([])

  // dragging state
  const [srcNodeId, setSrcNodeId] = useState<string | null>(null)
  const [srcPortId, setSrcPortId] = useState<string | null>(null)
  const [dstNodeId, setDstNodeId] = useState<string | null>(null)
  const [dstPortId, setDstPortId] = useState<string | null>(null)

  const renderContextMenu = (e: MouseEvent) => {
    e.preventDefault()
    ContextMenu.show(
      <Menu>
        <MenuItem text="Add node" onClick={props.onAddNode} />
      </Menu>,
      { left: e.clientX, top: e.clientY },
      () => {}
    )
  }

  const { nodes, edges } = rfGraph(props.module)

  return (
    <div
      style={{
        position: "relative",
        width: "100%",
        height: "100%",
      }}
      onContextMenu={event => renderContextMenu(event.nativeEvent as any)}
    >
      <div
        style={{
          position: "absolute",
          left: 0,
          right: 0,
          top: 0,
          bottom: 0,
          background: "black",
        }}
      >
        <rf.Canvas
          nodes={nodes}
          edges={edges}
          selections={selections}
          onCanvasClick={() => setSelections([])}
          node={nodeProps => (
            <rf.Node
              dragType="port"
              onClick={(_event, node) => {
                if (selections.includes(node.id)) {
                  props.onNodeClick(node.id)
                  setSelections([])
                  return
                }
                setSelections([node.id])
              }}
              onRemove={(_event, node) => props.onRemoveNode(node.id)}
              port={
                <Port
                  onEnter={(_event, port) => {
                    setDstNodeId(nodeProps.id)
                    setDstPortId(port.id)
                  }}
                  onLeave={(_event, _port) => {}}
                  onDragStart={(_event, _pos, port) => {
                    setSrcNodeId(nodeProps.id)
                    setSrcPortId(port.id)
                  }}
                  onDragEnd={(_event, _pos, _port) => {
                    props.onAddConnection({
                      from: {
                        node: srcNodeId,
                        port: removePortPrefix(srcPortId),
                      },
                      to: {
                        node: dstNodeId,
                        port: removePortPrefix(dstPortId),
                      },
                    })
                    setSrcPortId(null)
                    setDstPortId(null)
                  }}
                />
              }
              onEnter={(_event, node) => {}}
            />
          )}
          edge={edge => (
            <Edge
              onClick={(_event, edge) => setSelections([edge.id])}
              onRemove={(_event, edge) => {
                props.onRemoveConnection({
                  from: {
                    node: edge.from,
                    port: removePortPrefix(edge.fromPort),
                  },
                  to: {
                    node: edge.to,
                    port: removePortPrefix(edge.toPort),
                  },
                })
              }}
              // onAdd={}
            />
          )}
        />
      </div>
    </div>
  )
}

emil14 avatar Jan 31 '22 14:01 emil14