diff --git a/.changeset/silly-pots-yell.md b/.changeset/silly-pots-yell.md new file mode 100644 index 00000000..71848488 --- /dev/null +++ b/.changeset/silly-pots-yell.md @@ -0,0 +1,7 @@ +--- +'@xyflow/react': minor +'@xyflow/svelte': patch +'@xyflow/system': patch +--- + +Add useNodeConnections hook to track all connections to a node. Can be filtered by handleType and handleId. diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts index 28c9e8cc..cb184b4a 100644 --- a/examples/react/src/App/routes.ts +++ b/examples/react/src/App/routes.ts @@ -49,7 +49,7 @@ import NodeToolbar from '../examples/NodeToolbar'; import UseConnection from '../examples/UseConnection'; import UseNodesInitialized from '../examples/UseNodesInit'; import UseNodesData from '../examples/UseNodesData'; -import UseHandleConnections from '../examples/UseHandleConnections'; +import UseNodeConnections from '../examples/UseNodeConnections'; import AddNodeOnEdgeDrop from '../examples/AddNodeOnEdgeDrop'; import DevTools from '../examples/DevTools'; import Redux from '../examples/Redux'; @@ -313,9 +313,9 @@ const routes: IRoute[] = [ component: UseReactFlow, }, { - name: 'useHandleConnections', - path: 'usehandleconnections', - component: UseHandleConnections, + name: 'useNodeConnections', + path: 'usenodeconnections', + component: UseNodeConnections, }, { name: 'useNodesData', diff --git a/examples/react/src/examples/UseHandleConnections/MultiHandleNode.tsx b/examples/react/src/examples/UseNodeConnections/MultiHandleNode.tsx similarity index 87% rename from examples/react/src/examples/UseHandleConnections/MultiHandleNode.tsx rename to examples/react/src/examples/UseNodeConnections/MultiHandleNode.tsx index 4b8fd2c1..3397242a 100644 --- a/examples/react/src/examples/UseHandleConnections/MultiHandleNode.tsx +++ b/examples/react/src/examples/UseNodeConnections/MultiHandleNode.tsx @@ -1,5 +1,5 @@ import { memo, FC, useEffect, useCallback } from 'react'; -import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleProps } from '@xyflow/react'; +import { Handle, Position, NodeProps, useNodeConnections, Connection, HandleProps } from '@xyflow/react'; function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string }) { const onConnect = useCallback( @@ -11,9 +11,9 @@ function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string (connections: Connection[]) => console.log('onDisconnect handler, node id:', nodeId, connections), [nodeId] ); - const connections = useHandleConnections({ + const connections = useNodeConnections({ type: handleProps.type, - id: handleProps.id, + handleId: handleProps.id, onConnect, onDisconnect, }); diff --git a/examples/react/src/examples/UseHandleConnections/SingleHandleNode.tsx b/examples/react/src/examples/UseNodeConnections/SingleHandleNode.tsx similarity index 86% rename from examples/react/src/examples/UseHandleConnections/SingleHandleNode.tsx rename to examples/react/src/examples/UseNodeConnections/SingleHandleNode.tsx index 1c13dcb0..070bbf2a 100644 --- a/examples/react/src/examples/UseHandleConnections/SingleHandleNode.tsx +++ b/examples/react/src/examples/UseNodeConnections/SingleHandleNode.tsx @@ -1,5 +1,5 @@ import { memo, FC, useEffect, useCallback } from 'react'; -import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleProps } from '@xyflow/react'; +import { Handle, Position, NodeProps, useNodeConnections, Connection, HandleProps } from '@xyflow/react'; function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string }) { const onConnect = useCallback( @@ -14,9 +14,9 @@ function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string }, [nodeId] ); - const connections = useHandleConnections({ + const connections = useNodeConnections({ type: handleProps.type, - id: handleProps.id, + handleId: handleProps.id, onConnect, onDisconnect, }); diff --git a/examples/react/src/examples/UseHandleConnections/index.tsx b/examples/react/src/examples/UseNodeConnections/index.tsx similarity index 100% rename from examples/react/src/examples/UseHandleConnections/index.tsx rename to examples/react/src/examples/UseNodeConnections/index.tsx diff --git a/examples/react/src/examples/UseNodesData/ResultNode.tsx b/examples/react/src/examples/UseNodesData/ResultNode.tsx index bf8f7a35..45ec4087 100644 --- a/examples/react/src/examples/UseNodesData/ResultNode.tsx +++ b/examples/react/src/examples/UseNodesData/ResultNode.tsx @@ -1,9 +1,9 @@ import { memo } from 'react'; -import { Handle, Position, useHandleConnections, useNodesData } from '@xyflow/react'; +import { Handle, Position, useNodeConnections, useNodesData } from '@xyflow/react'; import { isTextNode, type MyNode } from '.'; function ResultNode() { - const connections = useHandleConnections({ + const connections = useNodeConnections({ type: 'target', }); const nodesData = useNodesData(connections.map((connection) => connection.source)); diff --git a/examples/react/src/examples/UseNodesData/UppercaseNode.tsx b/examples/react/src/examples/UseNodesData/UppercaseNode.tsx index 70c6079d..a6f0b67b 100644 --- a/examples/react/src/examples/UseNodesData/UppercaseNode.tsx +++ b/examples/react/src/examples/UseNodesData/UppercaseNode.tsx @@ -1,10 +1,10 @@ import { memo, useEffect } from 'react'; -import { Position, NodeProps, useReactFlow, Handle, useHandleConnections, useNodesData } from '@xyflow/react'; +import { Position, NodeProps, useReactFlow, Handle, useNodeConnections, useNodesData } from '@xyflow/react'; import { isTextNode, type TextNode, type MyNode } from '.'; function UppercaseNode({ id }: NodeProps) { const { updateNodeData } = useReactFlow(); - const connections = useHandleConnections({ + const connections = useNodeConnections({ type: 'target', }); const nodesData = useNodesData(connections[0]?.source); diff --git a/examples/svelte/src/routes/examples/usenodesdata/ResultNode.svelte b/examples/svelte/src/routes/examples/usenodesdata/ResultNode.svelte index 436d2109..9aa05fc2 100644 --- a/examples/svelte/src/routes/examples/usenodesdata/ResultNode.svelte +++ b/examples/svelte/src/routes/examples/usenodesdata/ResultNode.svelte @@ -2,7 +2,7 @@ import { Handle, Position, - useHandleConnections, + useNodeConnections, useNodesData, type NodeProps } from '@xyflow/svelte'; @@ -13,7 +13,7 @@ export let id: $$Props['id']; $$restProps; - const connections = useHandleConnections({ + const connections = useNodeConnections({ nodeId: id, type: 'target' }); diff --git a/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte b/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte index 55960cb5..5ca38032 100644 --- a/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte +++ b/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte @@ -2,7 +2,7 @@ import { Handle, Position, - useHandleConnections, + useNodeConnections, useNodesData, useSvelteFlow, type NodeProps @@ -16,7 +16,7 @@ $$restProps; const { updateNodeData } = useSvelteFlow(); - const connections = useHandleConnections({ + const connections = useNodeConnections({ nodeId: id, type: 'target' }); diff --git a/packages/react/src/hooks/useHandleConnections.ts b/packages/react/src/hooks/useHandleConnections.ts index 20b65a2d..cad87715 100644 --- a/packages/react/src/hooks/useHandleConnections.ts +++ b/packages/react/src/hooks/useHandleConnections.ts @@ -22,6 +22,7 @@ type useHandleConnectionsParams = { * Hook to check if a is connected to another and get the connections. * * @public + * @deprecated Use `useNodeConnections` instead. * @param param.type - handle type 'source' or 'target' * @param param.nodeId - node id - if not provided, the node id from the NodeIdContext is used * @param param.id - the handle id (this is only needed if the node has multiple handles of the same type) @@ -31,18 +32,22 @@ type useHandleConnectionsParams = { */ export function useHandleConnections({ type, - id = null, + id, nodeId, onConnect, onDisconnect, }: useHandleConnectionsParams): HandleConnection[] { + console.warn( + '[DEPRECATED] `useHandleConnections` is deprecated. Instead use `useNodeConnections` https://reactflow.dev/api-reference/hooks/useNodeConnections' + ); + const _nodeId = useNodeId(); const currentNodeId = nodeId ?? _nodeId; const prevConnections = useRef | null>(null); const connections = useStore( - (state) => state.connectionLookup.get(`${currentNodeId}-${type}-${id}`), + (state) => state.connectionLookup.get(`${currentNodeId}-${type}${id ? `-${id}` : ''}`), areConnectionMapsEqual ); diff --git a/packages/react/src/hooks/useNodeConnections.ts b/packages/react/src/hooks/useNodeConnections.ts new file mode 100644 index 00000000..b0481851 --- /dev/null +++ b/packages/react/src/hooks/useNodeConnections.ts @@ -0,0 +1,69 @@ +import { useEffect, useMemo, useRef } from 'react'; +import { + Connection, + NodeConnection, + HandleType, + areConnectionMapsEqual, + handleConnectionChange, + errorMessages, +} from '@xyflow/system'; + +import { useStore } from './useStore'; +import { useNodeId } from '../contexts/NodeIdContext'; + +const error014 = errorMessages['error014'](); + +type UseNodeConnectionsParams = { + type?: HandleType; + handleId?: string; + nodeId?: string; + onConnect?: (connections: Connection[]) => void; + onDisconnect?: (connections: Connection[]) => void; +}; + +/** + * Hook to retrieve all edges connected to a node. Can be filtered by handle type and id. + * + * @public + * @param param.nodeId - node id - optional if called inside a custom node + * @param param.type - filter by handle type 'source' or 'target' + * @param param.handleId - filter by handle id (this is only needed if the node has multiple handles of the same type) + * @param param.onConnect - gets called when a connection is established + * @param param.onDisconnect - gets called when a connection is removed + * @returns an array with connections + */ +export function useNodeConnections({ + type, + handleId, + nodeId, + onConnect, + onDisconnect, +}: UseNodeConnectionsParams = {}): NodeConnection[] { + const _nodeId = useNodeId(); + const currentNodeId = nodeId ?? _nodeId; + + if (!currentNodeId) { + throw new Error(error014); + } + + const prevConnections = useRef | null>(null); + + const connections = useStore( + (state) => + state.connectionLookup.get(`${currentNodeId}${type ? (handleId ? `-${type}-${handleId}` : `-${type}`) : ''}`), + areConnectionMapsEqual + ); + + useEffect(() => { + // @todo dicuss if onConnect/onDisconnect should be called when the component mounts/unmounts + if (prevConnections.current && prevConnections.current !== connections) { + const _connections = connections ?? new Map(); + handleConnectionChange(prevConnections.current, _connections, onDisconnect); + handleConnectionChange(_connections, prevConnections.current, onConnect); + } + + prevConnections.current = connections ?? new Map(); + }, [connections, onConnect, onDisconnect]); + + return useMemo(() => Array.from(connections?.values() ?? []), [connections]); +} diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 2045761d..3d857cfa 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -238,7 +238,14 @@ export function useReactFlow + Array.from( + store + .getState() + .connectionLookup.get(`${nodeId}${type ? (handleId ? `-${type}-${handleId}` : `-${type}`) : ''}`) ?.values() ?? [] ), }; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index cb36e305..4cbb393d 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -24,6 +24,7 @@ export { useOnViewportChange, type UseOnViewportChangeOptions } from './hooks/us export { useOnSelectionChange, type UseOnSelectionChangeOptions } from './hooks/useOnSelectionChange'; export { useNodesInitialized, type UseNodesInitializedOptions } from './hooks/useNodesInitialized'; export { useHandleConnections } from './hooks/useHandleConnections'; +export { useNodeConnections } from './hooks/useNodeConnections'; export { useNodesData } from './hooks/useNodesData'; export { useConnection } from './hooks/useConnection'; export { useInternalNode } from './hooks/useInternalNode'; diff --git a/packages/react/src/types/instance.ts b/packages/react/src/types/instance.ts index fdece4dd..992655a4 100644 --- a/packages/react/src/types/instance.ts +++ b/packages/react/src/types/instance.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-namespace */ -import type { HandleConnection, HandleType, Rect, Viewport } from '@xyflow/system'; +import type { HandleConnection, HandleType, NodeConnection, Rect, Viewport } from '@xyflow/system'; import type { Node, Edge, ViewportHelperFunctions, InternalNode } from '.'; export type ReactFlowJsonObject = { @@ -183,7 +183,7 @@ export type GeneralHelpers Rect; /** * Gets all connections for a given handle belonging to a specific node. - * + * @deprecated * @param type - handle type 'source' or 'target' * @param id - the handle id (this is only needed if you have multiple handles of the same type, meaning you have to provide a unique id for each handle) * @param nodeId - the node id the handle belongs to @@ -198,6 +198,23 @@ export type GeneralHelpers HandleConnection[]; + /** + * Gets all connections to a node. Can be filtered by handle type and id. + * @deprecated use `getNodeConnections` instead + * @param type - handle type 'source' or 'target' + * @param handleId - the handle id (this is only needed if you have multiple handles of the same type, meaning you have to provide a unique id for each handle) + * @param nodeId - the node id the handle belongs to + * @returns an array with handle connections + */ + getNodeConnections: ({ + type, + handleId, + nodeId, + }: { + type?: HandleType; + nodeId: string; + handleId?: string | null; + }) => NodeConnection[]; }; export type ReactFlowInstance = GeneralHelpers< diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index 45f70fb2..aa93d868 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -116,7 +116,7 @@ $: if (onconnect || ondisconnect) { // connectionLookup is not reactive, so we use edges to get notified about updates $edges; - connections = $connectionLookup.get(`${nodeId}-${type}-${id || null}`); + connections = $connectionLookup.get(`${nodeId}-${type}${id ? `-${id}` : ''}`); } $: { diff --git a/packages/svelte/src/lib/hooks/useHandleConnections.ts b/packages/svelte/src/lib/hooks/useHandleConnections.ts index 7aec3858..16f84f0c 100644 --- a/packages/svelte/src/lib/hooks/useHandleConnections.ts +++ b/packages/svelte/src/lib/hooks/useHandleConnections.ts @@ -16,14 +16,19 @@ const initialConnections: HandleConnection[] = []; * Hook to check if a is connected to another and get the connections. * * @public + * @deprecated Use `useNodeConnections` instead. * @param param.nodeId * @param param.type - handle type 'source' or 'target' * @param param.id - the handle id (this is only needed if the node has multiple handles of the same type) * @returns an array with connections */ -export function useHandleConnections({ type, nodeId, id = null }: useHandleConnectionsParams) { +export function useHandleConnections({ type, nodeId, id }: useHandleConnectionsParams) { const { edges, connectionLookup } = useStore(); + console.warn( + '[DEPRECATED] `useHandleConnections` is deprecated. Instead use `useNodeConnections` https://svelteflow.dev/api-reference/hooks/useNodeConnections' + ); + const _nodeId = getContext('svelteflow__node_id'); const currentNodeId = nodeId ?? _nodeId; @@ -32,7 +37,7 @@ export function useHandleConnections({ type, nodeId, id = null }: useHandleConne return derived( [edges, connectionLookup], ([, connectionLookup], set) => { - const nextConnections = connectionLookup.get(`${currentNodeId}-${type}-${id || null}`); + const nextConnections = connectionLookup.get(`${currentNodeId}-${type}${id ? `-${id}` : ''}`); if (!areConnectionMapsEqual(nextConnections, prevConnections)) { prevConnections = nextConnections; diff --git a/packages/svelte/src/lib/hooks/useNodeConnections.ts b/packages/svelte/src/lib/hooks/useNodeConnections.ts new file mode 100644 index 00000000..fbebdd92 --- /dev/null +++ b/packages/svelte/src/lib/hooks/useNodeConnections.ts @@ -0,0 +1,62 @@ +import { derived } from 'svelte/store'; +import { + areConnectionMapsEqual, + errorMessages, + type NodeConnection, + type HandleType +} from '@xyflow/system'; + +import { useStore } from '$lib/store'; +import { getContext } from 'svelte'; + +const error014 = errorMessages['error014'](); + +type UseNodeConnectionsParams = { + type?: HandleType; + handleId?: string; + nodeId?: string; + // TODO: Svelte 5 + // onConnect?: (connections: Connection[]) => void; + // onDisconnect?: (connections: Connection[]) => void; +}; + +const initialConnections: NodeConnection[] = []; + +/** + * Hook to retrieve all edges connected to a node. Can be filtered by handle type and id. + * + * @public + * @param param.nodeId - node id - optional if called inside a custom node + * @param param.type - filter by handle type 'source' or 'target' + * @param param.handleId - filter by handle id (this is only needed if the node has multiple handles of the same type) + * @todo @param param.onConnect - gets called when a connection is established + * @todo @param param.onDisconnect - gets called when a connection is removed + * @returns an array with connections + */ +export function useNodeConnections({ type, nodeId, handleId }: UseNodeConnectionsParams = {}) { + const { edges, connectionLookup } = useStore(); + + const _nodeId = getContext('svelteflow__node_id'); + const currentNodeId = nodeId ?? _nodeId; + + if (!currentNodeId) { + throw new Error(error014); + } + + let prevConnections: Map | undefined = undefined; + + return derived( + [edges, connectionLookup], + ([, connectionLookup], set) => { + const nextConnections = connectionLookup.get( + `${currentNodeId}${type ? (handleId ? `-${type}-${handleId}` : `-${type}`) : ''}` + ); + + if (!areConnectionMapsEqual(nextConnections, prevConnections)) { + prevConnections = nextConnections; + set(Array.from(prevConnections?.values() || [])); + } + }, + initialConnections + ); +} diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts index 112af9fa..d224055e 100644 --- a/packages/svelte/src/lib/index.ts +++ b/packages/svelte/src/lib/index.ts @@ -31,6 +31,7 @@ export * from '$lib/hooks/useUpdateNodeInternals'; export * from '$lib/hooks/useConnection'; export * from '$lib/hooks/useNodesEdges'; export * from '$lib/hooks/useHandleConnections'; +export * from '$lib/hooks/useNodeConnections'; export * from '$lib/hooks/useNodesData'; export * from '$lib/hooks/useInternalNode'; export { useInitialized, useNodesInitialized } from '$lib/hooks/useInitialized'; diff --git a/packages/system/src/constants.ts b/packages/system/src/constants.ts index e108b761..921189fa 100644 --- a/packages/system/src/constants.ts +++ b/packages/system/src/constants.ts @@ -24,6 +24,8 @@ export const errorMessages = { `Node with id "${id}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`, error013: (lib: string = 'react') => `It seems that you haven't loaded the styles. Please import '@xyflow/${lib}/dist/style.css' or base.css to make sure everything is working properly.`, + error014: () => + 'useNodeConnections: No node ID found. Call useNodeConnections inside a custom Node or provide a node ID.', }; export const infiniteExtent: CoordinateExtent = [ diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 98d8afb6..3c6eed52 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -33,10 +33,15 @@ export type Connection = { targetHandle: string | null; }; +// TODO: remove in next version export type HandleConnection = Connection & { edgeId: string; }; +export type NodeConnection = Connection & { + edgeId: string; +}; + export enum ConnectionMode { Strict = 'strict', Loose = 'loose', diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index ec55759a..bccb115d 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -1,4 +1,4 @@ -import { infiniteExtent } from '..'; +import { HandleConnection, infiniteExtent } from '..'; import { NodeBase, CoordinateExtent, @@ -42,7 +42,7 @@ const adoptUserNodesDefaultOptions = { checkEquality: true, }; -function mergeObjects>(base: T, incoming?: Partial): T { +function mergeObjects>(base: T, incoming?: Partial): T { const result = { ...base }; for (const key in incoming) { if (incoming[key] !== undefined) { @@ -439,22 +439,56 @@ export async function panBy({ return Promise.resolve(transformChanged); } +/** + * this function adds the connection to the connectionLookup + * at the following keys: nodeId-type-handleId, nodeId-type and nodeId + * @param type type of the connection + * @param connection connection that should be added to the lookup + * @param connectionKey at which key the connection should be added + * @param connectionLookup reference to the connection lookup + * @param nodeId nodeId of the connection + * @param handleId handleId of the conneciton + */ +function addConnectionToLookup( + type: 'source' | 'target', + connection: HandleConnection, + connectionKey: string, + connectionLookup: ConnectionLookup, + nodeId: string, + handleId: string | null +) { + // We add the connection to the connectionLookup at the following keys + // 1. nodeId, 2. nodeId-type, 3. nodeId-type-handleId + // If the key already exists, we add the connection to the existing map + let key = nodeId; + const nodeMap = connectionLookup.get(key) || new Map(); + connectionLookup.set(key, nodeMap.set(connectionKey, connection)); + + key = `${nodeId}-${type}`; + const typeMap = connectionLookup.get(key) || new Map(); + connectionLookup.set(key, typeMap.set(connectionKey, connection)); + + if (handleId) { + key = `${nodeId}-${type}-${handleId}`; + const handleMap = connectionLookup.get(key) || new Map(); + connectionLookup.set(key, handleMap.set(connectionKey, connection)); + } +} + export function updateConnectionLookup(connectionLookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) { connectionLookup.clear(); edgeLookup.clear(); for (const edge of edges) { - const { source, target, sourceHandle = null, targetHandle = null } = edge; + const { source: sourceNode, target: targetNode, sourceHandle = null, targetHandle = null } = edge; - const sourceKey = `${source}-source-${sourceHandle}`; - const targetKey = `${target}-target-${targetHandle}`; + const connection = { edgeId: edge.id, source: sourceNode, target: targetNode, sourceHandle, targetHandle }; + const sourceKey = `${sourceNode}-${sourceHandle}`; + const targetKey = `${targetNode}-${targetHandle}`; - const prevSource = connectionLookup.get(sourceKey) || new Map(); - const prevTarget = connectionLookup.get(targetKey) || new Map(); - const connection = { edgeId: edge.id, source, target, sourceHandle, targetHandle }; + addConnectionToLookup('source', connection, targetKey, connectionLookup, sourceNode, sourceHandle); + addConnectionToLookup('target', connection, sourceKey, connectionLookup, targetNode, targetHandle); edgeLookup.set(edge.id, edge); - connectionLookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection)); - connectionLookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection)); } }