diff --git a/examples/vite-app/src/examples/Undirectional/index.tsx b/examples/vite-app/src/examples/Undirectional/index.tsx
index 4ea676a5..4f25f4a5 100644
--- a/examples/vite-app/src/examples/Undirectional/index.tsx
+++ b/examples/vite-app/src/examples/Undirectional/index.tsx
@@ -1,4 +1,4 @@
-import React, { MouseEvent, useCallback } from 'react';
+import { MouseEvent, useCallback } from 'react';
import ReactFlow, {
useReactFlow,
NodeTypes,
@@ -184,6 +184,7 @@ const UpdateNodeInternalsFlow = () => {
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
+
const { project } = useReactFlow();
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
setEdges((els) => updateEdge(oldEdge, newConnection, els));
diff --git a/packages/core/src/components/ConnectionLine/index.tsx b/packages/core/src/components/ConnectionLine/index.tsx
index 3693f769..fae0c3b1 100644
--- a/packages/core/src/components/ConnectionLine/index.tsx
+++ b/packages/core/src/components/ConnectionLine/index.tsx
@@ -6,16 +6,15 @@ import { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
import { internalsSymbol } from '../../utils';
-import type { ConnectionLineComponent, HandleType, ReactFlowStore } from '../../types';
+import type { ConnectionLineComponent, HandleType, ReactFlowState, ReactFlowStore } from '../../types';
import { Position, ConnectionLineType, ConnectionMode } from '../../types';
type ConnectionLineProps = {
- connectionNodeId: string;
- connectionHandleType: HandleType;
- connectionLineType: ConnectionLineType;
- isConnectable: boolean;
- connectionLineStyle?: CSSProperties;
- CustomConnectionLineComponent?: ConnectionLineComponent;
+ nodeId: string;
+ handleType: HandleType;
+ type: ConnectionLineType;
+ style?: CSSProperties;
+ CustomComponent?: ConnectionLineComponent;
};
const oppositePosition = {
@@ -26,69 +25,62 @@ const oppositePosition = {
};
const ConnectionLine = ({
- connectionNodeId,
- connectionHandleType,
- connectionLineStyle,
- connectionLineType = ConnectionLineType.Bezier,
- isConnectable,
- CustomConnectionLineComponent,
+ nodeId,
+ handleType,
+ style,
+ type = ConnectionLineType.Bezier,
+ CustomComponent,
}: ConnectionLineProps) => {
const { fromNode, handleId, toX, toY, connectionMode } = useStore(
useCallback(
(s: ReactFlowStore) => ({
- fromNode: s.nodeInternals.get(connectionNodeId),
+ fromNode: s.nodeInternals.get(nodeId),
handleId: s.connectionHandleId,
toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2],
toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2],
connectionMode: s.connectionMode,
}),
- [connectionNodeId]
+ [nodeId]
),
shallow
);
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
- let handleBounds = fromHandleBounds?.[connectionHandleType];
+ let handleBounds = fromHandleBounds?.[handleType];
if (connectionMode === ConnectionMode.Loose) {
- handleBounds = handleBounds
- ? handleBounds
- : fromHandleBounds?.[connectionHandleType === 'source' ? 'target' : 'source'];
+ handleBounds = handleBounds ? handleBounds : fromHandleBounds?.[handleType === 'source' ? 'target' : 'source'];
}
- if (!fromNode || !isConnectable || !handleBounds) {
+ if (!fromNode || !handleBounds) {
return null;
}
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
- const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode?.width ?? 0) / 2;
- const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode?.height ?? 0;
- const fromX = (fromNode?.positionAbsolute?.x || 0) + fromHandleX;
- const fromY = (fromNode?.positionAbsolute?.y || 0) + fromHandleY;
-
+ const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.width ?? 0) / 2;
+ const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.height ?? 0;
+ const fromX = (fromNode.positionAbsolute?.x ?? 0) + fromHandleX;
+ const fromY = (fromNode.positionAbsolute?.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position;
+ const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
- if (!fromPosition) {
+ if (!fromPosition || !toPosition) {
return null;
}
- const toPosition: Position = oppositePosition[fromPosition];
-
- if (CustomConnectionLineComponent) {
+ if (CustomComponent) {
return (
-
-
-
+
);
}
@@ -103,29 +95,62 @@ const ConnectionLine = ({
targetPosition: toPosition,
};
- if (connectionLineType === ConnectionLineType.Bezier) {
+ if (type === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position
[dAttr] = getBezierPath(pathParams);
- } else if (connectionLineType === ConnectionLineType.Step) {
+ } else if (type === ConnectionLineType.Step) {
[dAttr] = getSmoothStepPath({
...pathParams,
borderRadius: 0,
});
- } else if (connectionLineType === ConnectionLineType.SmoothStep) {
+ } else if (type === ConnectionLineType.SmoothStep) {
[dAttr] = getSmoothStepPath(pathParams);
- } else if (connectionLineType === ConnectionLineType.SimpleBezier) {
+ } else if (type === ConnectionLineType.SimpleBezier) {
[dAttr] = getSimpleBezierPath(pathParams);
} else {
dAttr = `M${fromX},${fromY} ${toX},${toY}`;
}
- return (
-
-
-
- );
+ return
;
};
ConnectionLine.displayName = 'ConnectionLine';
-export default ConnectionLine;
+type ConnectionLineWrapperProps = {
+ type: ConnectionLineType;
+ component?: ConnectionLineComponent;
+ containerStyle?: CSSProperties;
+ style?: CSSProperties;
+};
+
+const selector = (s: ReactFlowState) => ({
+ nodeId: s.connectionNodeId,
+ handleType: s.connectionHandleType,
+ nodesConnectable: s.nodesConnectable,
+ width: s.width,
+ height: s.height,
+});
+
+function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) {
+ const { nodeId, handleType, nodesConnectable, width, height } = useStore(selector, shallow);
+ const isValid = !!(nodeId && handleType && width && nodesConnectable);
+
+ if (!isValid) {
+ return null;
+ }
+
+ return (
+
+ );
+}
+
+export default ConnectionLineWrapper;
diff --git a/packages/core/src/container/EdgeRenderer/index.tsx b/packages/core/src/container/EdgeRenderer/index.tsx
index c687a8d8..e25f305d 100644
--- a/packages/core/src/container/EdgeRenderer/index.tsx
+++ b/packages/core/src/container/EdgeRenderer/index.tsx
@@ -1,27 +1,19 @@
-import { memo } from 'react';
+import { memo, ReactNode } from 'react';
import { shallow } from 'zustand/shallow';
import cc from 'classcat';
import { useStore } from '../../hooks/useStore';
import useVisibleEdges from '../../hooks/useVisibleEdges';
-import ConnectionLine from '../../components/ConnectionLine/index';
import MarkerDefinitions from './MarkerDefinitions';
import { getEdgePositions, getHandle, getNodeData } from './utils';
import { GraphViewProps } from '../GraphView';
-import { devWarn } from '../../utils';
import { ConnectionMode, Position } from '../../types';
import type { Edge, ReactFlowState } from '../../types';
type EdgeRendererProps = Pick<
GraphViewProps,
| 'edgeTypes'
- | 'connectionLineType'
- | 'connectionLineType'
- | 'connectionLineStyle'
- | 'connectionLineComponent'
- | 'connectionLineContainerStyle'
- | 'connectionLineContainerStyle'
| 'onEdgeClick'
| 'onEdgeDoubleClick'
| 'defaultMarkerColor'
@@ -40,11 +32,10 @@ type EdgeRendererProps = Pick<
| 'disableKeyboardA11y'
> & {
elevateEdgesOnSelect: boolean;
+ children: ReactNode;
};
const selector = (s: ReactFlowState) => ({
- connectionNodeId: s.connectionNodeId,
- connectionHandleType: s.connectionHandleType,
nodesConnectable: s.nodesConnectable,
edgesFocusable: s.edgesFocusable,
elementsSelectable: s.elementsSelectable,
@@ -52,35 +43,38 @@ const selector = (s: ReactFlowState) => ({
height: s.height,
connectionMode: s.connectionMode,
nodeInternals: s.nodeInternals,
+ onError: s.onError,
});
-const EdgeRenderer = (props: EdgeRendererProps) => {
- const {
- connectionNodeId,
- connectionHandleType,
- nodesConnectable,
- edgesFocusable,
- elementsSelectable,
- width,
- height,
- connectionMode,
- nodeInternals,
- } = useStore(selector, shallow);
- const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeInternals, props.elevateEdgesOnSelect);
+const EdgeRenderer = ({
+ defaultMarkerColor,
+ onlyRenderVisibleElements,
+ elevateEdgesOnSelect,
+ rfId,
+ edgeTypes,
+ noPanClassName,
+ onEdgeUpdate,
+ onEdgeContextMenu,
+ onEdgeMouseEnter,
+ onEdgeMouseMove,
+ onEdgeMouseLeave,
+ onEdgeClick,
+ edgeUpdaterRadius,
+ onEdgeDoubleClick,
+ onEdgeUpdateStart,
+ onEdgeUpdateEnd,
+ children,
+}: EdgeRendererProps) => {
+ const { edgesFocusable, elementsSelectable, width, height, connectionMode, nodeInternals, onError } = useStore(
+ selector,
+ shallow
+ );
+ const edgeTree = useVisibleEdges(onlyRenderVisibleElements, nodeInternals, elevateEdgesOnSelect);
if (!width) {
return null;
}
- const {
- connectionLineType,
- defaultMarkerColor,
- connectionLineStyle,
- connectionLineComponent,
- connectionLineContainerStyle,
- } = props;
- const renderConnectionLine = connectionNodeId && connectionHandleType;
-
return (
<>
{edgeTree.map(({ level, edges, isMaxLevel }) => (
@@ -91,11 +85,11 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
height={height}
className="react-flow__edges react-flow__container"
>
- {isMaxLevel &&
}
+ {isMaxLevel &&
}
{edges.map((edge: Edge) => {
- const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source)!);
- const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target)!);
+ const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source));
+ const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target));
if (!sourceIsValid || !targetIsValid) {
return null;
@@ -103,31 +97,29 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
let edgeType = edge.type || 'default';
- if (!props.edgeTypes[edgeType]) {
- devWarn(
- `Edge type "${edgeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300`
- );
-
+ if (!edgeTypes[edgeType]) {
+ onError?.('003', `Edge type "${edgeType}" not found. Using fallback type "default".`);
edgeType = 'default';
}
- const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default;
- // when connection type is loose we can define all handles as sources
+ const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default;
+ // when connection type is loose we can define all handles as sources and connect source -> source
const targetNodeHandles =
connectionMode === ConnectionMode.Strict
? targetHandleBounds!.target
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []);
- const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle || null);
- const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle || null);
+ const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle);
+ const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
if (!sourceHandle || !targetHandle) {
- devWarn(
- `Couldn't create edge for ${!sourceHandle ? 'source' : 'target'} handle id: ${
+ onError?.(
+ '008',
+ `Couldn't create edge for ${!sourceHandle ? 'source' : 'target'} handle id: "${
!sourceHandle ? edge.sourceHandle : edge.targetHandle
- }; edge id: ${edge.id}. Help: https://reactflow.dev/error#800`
+ }", edge id: ${edge.id}.`
);
return null;
@@ -146,7 +138,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
{
sourcePosition={sourcePosition}
targetPosition={targetPosition}
elementsSelectable={elementsSelectable}
- onEdgeUpdate={props.onEdgeUpdate}
- onContextMenu={props.onEdgeContextMenu}
- onMouseEnter={props.onEdgeMouseEnter}
- onMouseMove={props.onEdgeMouseMove}
- onMouseLeave={props.onEdgeMouseLeave}
- onClick={props.onEdgeClick}
- edgeUpdaterRadius={props.edgeUpdaterRadius}
- onEdgeDoubleClick={props.onEdgeDoubleClick}
- onEdgeUpdateStart={props.onEdgeUpdateStart}
- onEdgeUpdateEnd={props.onEdgeUpdateEnd}
- rfId={props.rfId}
+ onEdgeUpdate={onEdgeUpdate}
+ onContextMenu={onEdgeContextMenu}
+ onMouseEnter={onEdgeMouseEnter}
+ onMouseMove={onEdgeMouseMove}
+ onMouseLeave={onEdgeMouseLeave}
+ onClick={onEdgeClick}
+ edgeUpdaterRadius={edgeUpdaterRadius}
+ onEdgeDoubleClick={onEdgeDoubleClick}
+ onEdgeUpdateStart={onEdgeUpdateStart}
+ onEdgeUpdateEnd={onEdgeUpdateEnd}
+ rfId={rfId}
ariaLabel={edge.ariaLabel}
isFocusable={isFocusable}
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
@@ -193,23 +185,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
))}
- {renderConnectionLine && (
-
- )}
+ {children}
>
);
};
diff --git a/packages/core/src/container/EdgeRenderer/utils.ts b/packages/core/src/container/EdgeRenderer/utils.ts
index eee01706..edd74543 100644
--- a/packages/core/src/container/EdgeRenderer/utils.ts
+++ b/packages/core/src/container/EdgeRenderer/utils.ts
@@ -72,21 +72,18 @@ export function getHandlePosition(position: Position, nodeRect: Rect, handle: Ha
}
}
-export function getHandle(bounds: HandleElement[], handleId: string | null): HandleElement | null {
+export function getHandle(bounds: HandleElement[], handleId?: string | null): HandleElement | null {
if (!bounds) {
return null;
}
- // there is no handleId when there are no multiple handles/ handles with ids
- // so we just pick the first one
- let handle: HandleElement | null = null;
- if (bounds.length === 1 || !handleId) {
- handle = bounds[0];
- } else if (handleId) {
- handle = bounds.find((d) => d.id === handleId)!;
+ if (handleId) {
+ return bounds.find((d) => d.id === handleId)!;
+ } else if (bounds.length === 1) {
+ return bounds[0];
}
- return typeof handle === 'undefined' ? null : handle;
+ return null;
}
interface EdgePositions {
@@ -167,16 +164,15 @@ export function isEdgeVisible({
return overlappingArea > 0;
}
-export function getNodeData(node: Node): [Rect, NodeHandleBounds | null, boolean] {
+export function getNodeData(node?: Node): [Rect, NodeHandleBounds | null, boolean] {
const handleBounds = node?.[internalsSymbol]?.handleBounds || null;
- const isInvalid =
- !node ||
- !handleBounds ||
- !node.width ||
- !node.height ||
- typeof node.positionAbsolute?.x === 'undefined' ||
- typeof node.positionAbsolute?.y === 'undefined';
+ const isValid =
+ handleBounds &&
+ node?.width &&
+ node?.height &&
+ typeof node?.positionAbsolute?.x !== 'undefined' &&
+ typeof node?.positionAbsolute?.y !== 'undefined';
return [
{
@@ -186,6 +182,6 @@ export function getNodeData(node: Node): [Rect, NodeHandleBounds | null, boolean
height: node?.height || 0,
},
handleBounds,
- !isInvalid,
+ !!isValid,
];
}