(null);
- const hasHandleId = connectionNodeId.includes('__');
- const sourceIdSplitted = connectionNodeId.split('__');
- const nodeId = sourceIdSplitted[0];
- const handleId = hasHandleId ? sourceIdSplitted[1] : null;
+ const nodeId = connectionNodeId;
+ const handleId = connectionHandleId;
useEffect(() => {
const nextSourceNode = nodes.find((n) => n.id === nodeId) || null;
diff --git a/src/components/Handle/BaseHandle.tsx b/src/components/Handle/BaseHandle.tsx
index e5250cec..f6964042 100644
--- a/src/components/Handle/BaseHandle.tsx
+++ b/src/components/Handle/BaseHandle.tsx
@@ -28,7 +28,7 @@ interface BaseHandleProps {
setConnectionNodeId: SetSourceIdFunc;
setPosition: (pos: XYPosition) => void;
isValidConnection: ValidConnectionFunc;
- id?: ElementId | boolean;
+ id?: ElementId | null;
className?: string;
style?: CSSProperties;
}
@@ -42,6 +42,7 @@ type Result = {
function onMouseDown(
event: ReactMouseEvent,
+ handleId: ElementId | null,
nodeId: ElementId,
setConnectionNodeId: SetSourceIdFunc,
setPosition: (pos: XYPosition) => void,
@@ -66,7 +67,7 @@ function onMouseDown(
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
});
- setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleType: handleType });
+ setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType });
if (onConnectStart) {
onConnectStart(event, { nodeId, handleType });
@@ -88,26 +89,33 @@ function onMouseDown(
const result: Result = {
elementBelow,
isValid: false,
- connection: { source: null, target: null },
+ connection: { source: null, target: null, sourceHandle: null, targetHandle: null },
isHoveringHandle: false,
};
if (elementBelow && (elementBelow.classList.contains('target') || elementBelow.classList.contains('source'))) {
- let connection: Connection = { source: null, target: null };
-
- if (isTarget) {
- const sourceId = elementBelow.getAttribute('data-nodeid');
- connection = { source: sourceId, target: nodeId };
- } else {
- const targetId = elementBelow.getAttribute('data-nodeid');
- connection = { source: nodeId, target: targetId };
- }
-
- const isValid = isValidConnection(connection);
-
- result.connection = connection;
- result.isValid = isValid;
result.isHoveringHandle = true;
+ if (
+ (isTarget && elementBelow.classList.contains('source')) ||
+ (!isTarget && elementBelow.classList.contains('target'))
+ ) {
+ let connection: Connection = { source: null, target: null, sourceHandle: null, targetHandle: null };
+
+ if (isTarget) {
+ const sourceId = elementBelow.getAttribute('data-nodeid');
+ const sourcehandleId = elementBelow.getAttribute('data-handleid');
+ connection = { source: sourceId, sourceHandle: sourcehandleId, target: nodeId, targetHandle: handleId };
+ } else {
+ const targetId = elementBelow.getAttribute('data-nodeid');
+ const targetHandleId = elementBelow.getAttribute('data-handleid');
+ connection = { source: nodeId, sourceHandle: handleId, target: targetId, targetHandle: targetHandleId };
+ }
+
+ const isValid = isValidConnection(connection);
+
+ result.connection = connection;
+ result.isValid = isValid;
+ }
}
return result;
@@ -150,7 +158,7 @@ function onMouseDown(
}
resetRecentHandle();
- setConnectionNodeId({ connectionNodeId: null, connectionHandleType: null });
+ setConnectionNodeId({ connectionNodeId: null, connectionHandleId: null, connectionHandleType: null });
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
@@ -171,7 +179,7 @@ const BaseHandle = ({
setConnectionNodeId,
setPosition,
className,
- id = false,
+ id = null,
isValidConnection,
...rest
}: BaseHandleProps) => {
@@ -187,17 +195,19 @@ const BaseHandle = ({
},
]);
- const nodeIdWithHandleId = id ? `${nodeId}__${id}` : nodeId;
+ const handleId = id || null;
return (
onMouseDown(
event,
- nodeIdWithHandleId,
+ handleId,
+ nodeId,
setConnectionNodeId,
setPosition,
onConnect,
diff --git a/src/components/Nodes/utils.ts b/src/components/Nodes/utils.ts
index 82d9322e..7d2a7d31 100644
--- a/src/components/Nodes/utils.ts
+++ b/src/components/Nodes/utils.ts
@@ -19,21 +19,14 @@ export const getHandleBounds = (
(handle): HandleElement => {
const bounds = handle.getBoundingClientRect();
const dimensions = getDimensions(handle);
- const nodeIdAttr = handle.getAttribute('data-nodeid');
+ const handleId = handle.getAttribute('data-handleid');
const handlePosition = (handle.getAttribute('data-handlepos') as unknown) as Position;
- const nodeIdSplitted = nodeIdAttr ? nodeIdAttr.split('__') : null;
-
- let handleId = null;
-
- if (nodeIdSplitted) {
- handleId = (nodeIdSplitted.length ? nodeIdSplitted[1] : nodeIdSplitted) as string;
- }
return {
id: handleId,
position: handlePosition,
- x: (bounds.left - parentBounds.left) * (1 / k),
- y: (bounds.top - parentBounds.top) * (1 / k),
+ x: (bounds.left - parentBounds.left) / k,
+ y: (bounds.top - parentBounds.top) / k,
...dimensions,
};
}
diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx
index 5de86c49..f37fb718 100644
--- a/src/container/EdgeRenderer/index.tsx
+++ b/src/container/EdgeRenderer/index.tsx
@@ -98,6 +98,10 @@ function getHandle(bounds: HandleElement[], handleId: ElementId | null): HandleE
handle = bounds.find((d) => d.id === handleId);
}
+ if (typeof handle === 'undefined') {
+ return null;
+ }
+
return handle;
}
@@ -132,18 +136,22 @@ function renderEdge(
selectedElements: Elements | null,
elementsSelectable: boolean
) {
- const [sourceId, sourceHandleId] = edge.source.split('__');
- const [targetId, targetHandleId] = edge.target.split('__');
+ const sourceId = edge.source;
+ const sourceHandleId = edge.sourceHandle || null;
+ const targetId = edge.target;
+ const targetHandleId = edge.targetHandle || null;
const sourceNode = nodes.find((n) => n.id === sourceId);
const targetNode = nodes.find((n) => n.id === targetId);
if (!sourceNode) {
- throw new Error(`couldn't create edge for source id: ${sourceId}`);
+ console.warn(`couldn't create edge for source id: ${sourceId}`);
+ return null;
}
if (!targetNode) {
- throw new Error(`couldn't create edge for target id: ${targetId}`);
+ console.warn(`couldn't create edge for target id: ${targetId}`);
+ return null;
}
if (!sourceNode.__rf.width || !sourceNode.__rf.height) {
@@ -157,6 +165,16 @@ function renderEdge(
const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom;
const targetPosition = targetHandle ? targetHandle.position : Position.Top;
+ if (!sourceHandle) {
+ console.warn(`couldn't create edge for source handle id: ${sourceHandleId}`);
+ return null;
+ }
+
+ if (!targetHandle) {
+ console.warn(`couldn't create edge for source handle id: ${targetHandleId}`);
+ return null;
+ }
+
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
sourceNode,
sourceHandle,
@@ -208,6 +226,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
const edges = useStoreState((state) => state.edges);
const nodes = useStoreState((state) => state.nodes);
const connectionNodeId = useStoreState((state) => state.connectionNodeId);
+ const connectionHandleId = useStoreState((state) => state.connectionHandleId);
const connectionHandleType = useStoreState((state) => state.connectionHandleType);
const connectionPosition = useStoreState((state) => state.connectionPosition);
const selectedElements = useStoreState((state) => state.selectedElements);
@@ -234,6 +253,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
{
+ setConnectionNodeId: action((state, { connectionNodeId, connectionHandleId, connectionHandleType }) => {
state.connectionNodeId = connectionNodeId;
+ state.connectionHandleId = connectionHandleId;
state.connectionHandleType = connectionHandleType;
}),
diff --git a/src/types/index.ts b/src/types/index.ts
index 60b70e13..adfad3e1 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -60,6 +60,8 @@ export interface Edge {
type?: string;
source: ElementId;
target: ElementId;
+ sourceHandle?: ElementId | null;
+ targetHandle?: ElementId | null;
label?: string;
labelStyle?: CSSProperties;
labelShowBg?: boolean;
@@ -243,6 +245,8 @@ export type OnLoadFunc = (params: OnLoadParams) => void;
export interface Connection {
source: ElementId | null;
target: ElementId | null;
+ sourceHandle: ElementId | null;
+ targetHandle: ElementId | null;
}
export enum ConnectionLineType {
@@ -276,6 +280,7 @@ export type OnConnectEndFunc = (event: MouseEvent) => void;
export type SetConnectionId = {
connectionNodeId: ElementId | null;
+ connectionHandleId: ElementId | null;
connectionHandleType: HandleType | null;
};
@@ -290,7 +295,7 @@ export interface HandleProps {
isConnectable?: boolean;
onConnect?: OnConnectFunc;
isValidConnection?: (connection: Connection) => boolean;
- id?: string;
+ id?: ElementId;
style?: CSSProperties;
className?: string;
}
diff --git a/src/utils/graph.ts b/src/utils/graph.ts
index 771c8cd9..d39c8f7e 100644
--- a/src/utils/graph.ts
+++ b/src/utils/graph.ts
@@ -40,29 +40,39 @@ export const removeElements = (elementsToRemove: Elements, elements: Elements):
});
};
-const getEdgeId = ({ source, target }: Connection): ElementId => `reactflow__edge-${source}-${target}`;
+const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection): ElementId =>
+ `reactflow__edge-${source}${sourceHandle}-${target}${targetHandle}`;
+
+const connectionExists = (edge: Edge, elements: Elements) => {
+ return elements.some(
+ (el) =>
+ isEdge(el) &&
+ el.source === edge.source &&
+ el.target === edge.target &&
+ (el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) &&
+ (el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle))
+ );
+};
export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elements => {
if (!edgeParams.source || !edgeParams.target) {
- throw new Error("Can't create edge. An edge needs a source and a target.");
+ console.warn("Can't create edge. An edge needs a source and a target.");
+ return elements;
}
- // make sure that there is node with the target and one with the source id
- [edgeParams.source, edgeParams.target].forEach((id) => {
- const nodeId = id.includes('__') ? id.split('__')[0] : id;
- if (!elements.find((e) => isNode(e) && e.id === nodeId)) {
- throw new Error(`Can't create edge. Node with id=${nodeId} does not exist.`);
- }
- });
-
+ let edge: Edge;
if (isEdge(edgeParams)) {
- return elements.concat({ ...edgeParams });
+ edge = { ...edgeParams };
+ } else {
+ edge = {
+ ...edgeParams,
+ id: getEdgeId(edgeParams),
+ } as Edge;
}
- const edge = {
- ...edgeParams,
- id: getEdgeId(edgeParams),
- } as Edge;
+ if (connectionExists(edge, elements)) {
+ return elements;
+ }
return elements.concat(edge);
};
@@ -112,6 +122,8 @@ export const parseElement = (element: Node | Edge): Node | Edge => {
...element,
source: element.source.toString(),
target: element.target.toString(),
+ sourceHandle: element.sourceHandle ? element.sourceHandle.toString() : null,
+ targetHandle: element.targetHandle ? element.targetHandle.toString() : null,
id: element.id.toString(),
type: element.type || 'default',
};
diff --git a/website/src/markdown/docs/api/edges.md b/website/src/markdown/docs/api/edges.md
index 66951563..c52d2ce9 100644
--- a/website/src/markdown/docs/api/edges.md
+++ b/website/src/markdown/docs/api/edges.md
@@ -24,6 +24,8 @@ If you wanted to display this edge, you would need a node with id = 1 (source no
- `id`: string *(required)*
- `source`: string (an id of a node) *(required)*
- `target`: string (an id of a node) *(required)*
+- `sourceHandle`: string (an id of a handle - you only need this when you have multiple handles)
+- `targetHandle`: string (an id of a handle - you only need this when you have multiple handles)
- `type`: 'default' (bezier), 'straight', 'step' and 'smoothedge' or a custom one depending on your implementation
- `animated`: boolean
- `style`: css properties for the edge line path
diff --git a/website/src/markdown/docs/api/handle.md b/website/src/markdown/docs/api/handle.md
index 25bd6ba0..8bdf1486 100644
--- a/website/src/markdown/docs/api/handle.md
+++ b/website/src/markdown/docs/api/handle.md
@@ -24,7 +24,7 @@ const targetHandleWithValidation = (
- `id`: string - you only need this when you have multiple source or target handles (otherwise the node id is used)
- `position`: 'left', 'right', 'top' or 'bottom' handle position - default: 'top' for type target, 'bottom' for type source
- `onConnect`: function that gets triggered on connect
-- `isValidConnection`: function receives a connection `{ target: 'some-id', source: 'another-id' }` as param, returns a boolean - default: `true`
+- `isValidConnection`: function receives a connection `{ target: 'some-id', source: 'another-id', sourceHandle: 'source handle id or null', targetHandle: 'target handle id or null' }` as param, returns a boolean - default: `true`
- `style`: css properties
- `className`: additional class name
@@ -36,5 +36,6 @@ The handle receives the additional class names `connecting` when the connection
### Multiple Handles
-If you need multiple source or target handles you can achieve this by creating a custom node. Normally you just use the id of a node for the `source` or `target` of an edge. If you have multiple source or target handles you need to pass an id to these handles. These ids get then added to the node id, so that you can connect a specific handle. If you have a node with an id = `1` and a handle with an id = `a` you can connect this handle by using the id = `1__a`.
+If you need multiple source or target handles you can achieve this by creating a custom node. Normally you just use the id of a node for the `source` or `target` of an edge. If you have multiple source or target handles you need to pass an id to these handles. These ids can be used by an edge with the `sourceHandle` and `targetHandle` options, so that you can connect a specific handle. If you have a node with an id = `1` and a handle with an id = `a` you can connect this handle by using the node `source=1` and the `sourceHandle=__a`.
+
You can find an example of how to implement a custom node with multiple handles in the [custom node example](https://github.com/wbkd/react-flow/blob/main/example/src/CustomNode/ColorSelectorNode.js#L18-L29).