diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts index 18307c79..4bab00f5 100644 --- a/examples/react/src/App/routes.ts +++ b/examples/react/src/App/routes.ts @@ -44,8 +44,9 @@ import CancelConnection from '../examples/CancelConnection'; import InteractiveMinimap from '../examples/InteractiveMinimap'; import UseOnSelectionChange from '../examples/UseOnSelectionChange'; import NodeToolbar from '../examples/NodeToolbar'; -import useNodesInitialized from '../examples/UseNodesInit'; -import useHandleConnectionStatus from '../examples/UseHandleConnectionStatus'; +import UseNodesInitialized from '../examples/UseNodesInit'; +import UseNodesData from '../examples/UseNodesData'; +import UseHandleConnections from '../examples/UseHandleConnections'; export interface IRoute { name: string; @@ -262,7 +263,7 @@ const routes: IRoute[] = [ { name: 'useNodesInitialized', path: 'use-nodes-initialized', - component: useNodesInitialized, + component: UseNodesInitialized, }, { name: 'useOnSelectionChange', @@ -275,9 +276,14 @@ const routes: IRoute[] = [ component: UseReactFlow, }, { - name: 'useHandleConnectionStatus', - path: 'usehandleconnectionstatus', - component: useHandleConnectionStatus, + name: 'useHandleConnections', + path: 'usehandleconnections', + component: UseHandleConnections, + }, + { + name: 'useNodesData', + path: 'usenodesdata', + component: UseNodesData, }, { name: 'useUpdateNodeInternals', diff --git a/examples/react/src/examples/UseHandleConnectionStatus/MultiHandleNode.tsx b/examples/react/src/examples/UseHandleConnections/MultiHandleNode.tsx similarity index 82% rename from examples/react/src/examples/UseHandleConnectionStatus/MultiHandleNode.tsx rename to examples/react/src/examples/UseHandleConnections/MultiHandleNode.tsx index f8a31143..e04f3448 100644 --- a/examples/react/src/examples/UseHandleConnectionStatus/MultiHandleNode.tsx +++ b/examples/react/src/examples/UseHandleConnections/MultiHandleNode.tsx @@ -1,6 +1,5 @@ import { memo, FC, useEffect, useCallback } from 'react'; -import { Handle, Position, NodeProps, useHandleConnectionStatus, Connection } from '@xyflow/react'; -import { HandleComponentProps } from '@xyflow/react/dist/esm/components/Handle'; +import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleComponentProps } from '@xyflow/react'; function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeId: string }) { const onConnect = useCallback( @@ -12,7 +11,7 @@ function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeI (connections: Connection[]) => console.log('onDisconnect handler, node id:', nodeId, connections), [nodeId] ); - const status = useHandleConnectionStatus({ + const connections = useHandleConnections({ handleType: handleProps.type, handleId: handleProps.id, onConnect, @@ -20,8 +19,8 @@ function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeI }); useEffect(() => { - console.log('useEffect, node id:', nodeId, handleProps.type, status); - }, [status]); + console.log('useEffect, node id:', nodeId, handleProps.type, connections); + }, [connections]); return ; } diff --git a/examples/react/src/examples/UseHandleConnectionStatus/SingleHandleNode.tsx b/examples/react/src/examples/UseHandleConnections/SingleHandleNode.tsx similarity index 81% rename from examples/react/src/examples/UseHandleConnectionStatus/SingleHandleNode.tsx rename to examples/react/src/examples/UseHandleConnections/SingleHandleNode.tsx index ebbe584c..444499fb 100644 --- a/examples/react/src/examples/UseHandleConnectionStatus/SingleHandleNode.tsx +++ b/examples/react/src/examples/UseHandleConnections/SingleHandleNode.tsx @@ -1,6 +1,5 @@ import { memo, FC, useEffect, useCallback } from 'react'; -import { Handle, Position, NodeProps, useHandleConnectionStatus, Connection } from '@xyflow/react'; -import { HandleComponentProps } from '@xyflow/react/dist/esm/components/Handle'; +import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleComponentProps } from '@xyflow/react'; function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeId: string }) { const onConnect = useCallback( @@ -15,7 +14,7 @@ function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeI }, [nodeId] ); - const status = useHandleConnectionStatus({ + const connections = useHandleConnections({ handleType: handleProps.type, handleId: handleProps.id, onConnect, @@ -23,8 +22,8 @@ function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeI }); useEffect(() => { - console.log('useEffect, node id:', nodeId, handleProps.type, status); - }, [status]); + console.log('useEffect, node id:', nodeId, handleProps.type, connections); + }, [connections]); return ; } diff --git a/examples/react/src/examples/UseHandleConnectionStatus/index.tsx b/examples/react/src/examples/UseHandleConnections/index.tsx similarity index 100% rename from examples/react/src/examples/UseHandleConnectionStatus/index.tsx rename to examples/react/src/examples/UseHandleConnections/index.tsx diff --git a/examples/react/src/examples/UseNodesData/ResultNode.tsx b/examples/react/src/examples/UseNodesData/ResultNode.tsx new file mode 100644 index 00000000..09603eac --- /dev/null +++ b/examples/react/src/examples/UseNodesData/ResultNode.tsx @@ -0,0 +1,25 @@ +import { memo, useEffect } from 'react'; +import { Handle, Position, useHandleConnections, useNodesData } from '@xyflow/react'; + +function ResultNode() { + const connections = useHandleConnections({ + handleType: 'target', + }); + const nodesData = useNodesData<{ text: string }>(connections.map((connection) => connection.source)); + + useEffect(() => { + console.log('incoming data changed', nodesData); + }, [nodesData]); + + return ( +
+ +
+ incoming texts:{' '} + {nodesData?.filter((nodeData) => nodeData.text).map(({ text }, i) =>
{text}
) || 'none'} +
+
+ ); +} + +export default memo(ResultNode); diff --git a/examples/react/src/examples/UseNodesData/TextNode.tsx b/examples/react/src/examples/UseNodesData/TextNode.tsx new file mode 100644 index 00000000..a7388cb0 --- /dev/null +++ b/examples/react/src/examples/UseNodesData/TextNode.tsx @@ -0,0 +1,20 @@ +import { memo, ChangeEventHandler } from 'react'; +import { Position, NodeProps, useUpdateNodeData, Handle } from '@xyflow/react'; + +function TextNode({ id, data }: NodeProps) { + const updateNodeData = useUpdateNodeData(); + + const onChange: ChangeEventHandler = (evt) => updateNodeData(id, { text: evt.target.value }); + + return ( +
+
node {id}
+
+ +
+ +
+ ); +} + +export default memo(TextNode); diff --git a/examples/react/src/examples/UseNodesData/index.tsx b/examples/react/src/examples/UseNodesData/index.tsx new file mode 100644 index 00000000..67e68a2f --- /dev/null +++ b/examples/react/src/examples/UseNodesData/index.tsx @@ -0,0 +1,83 @@ +import { useCallback } from 'react'; +import { + ReactFlow, + Controls, + addEdge, + Connection, + useNodesState, + useEdgesState, + Background, + Node, + Edge, +} from '@xyflow/react'; + +import TextNode from './TextNode'; +import ResultNode from './ResultNode'; + +const nodeTypes = { + text: TextNode, + result: ResultNode, +}; + +const initNodes: Node[] = [ + { + id: '1', + type: 'text', + data: { + text: 'hello', + }, + position: { x: 0, y: 0 }, + }, + { + id: '2', + type: 'text', + data: { + text: 'world', + }, + position: { x: 0, y: 100 }, + }, + + { + id: '3', + type: 'result', + data: {}, + position: { x: 300, y: 50 }, + }, +]; + +const initEdges: Edge[] = [ + { + id: 'e1-3', + source: '1', + target: '3', + }, + { + id: 'e2-3', + source: '2', + target: '3', + }, +]; + +const CustomNodeFlow = () => { + const [nodes, setNodes, onNodesChange] = useNodesState(initNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges); + + const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]); + + return ( + + + + + ); +}; + +export default CustomNodeFlow; diff --git a/packages/react/src/hooks/useHandleConnectionStatus.ts b/packages/react/src/hooks/useHandleConnections.ts similarity index 86% rename from packages/react/src/hooks/useHandleConnectionStatus.ts rename to packages/react/src/hooks/useHandleConnections.ts index 470cc077..f692ba37 100644 --- a/packages/react/src/hooks/useHandleConnectionStatus.ts +++ b/packages/react/src/hooks/useHandleConnections.ts @@ -4,7 +4,7 @@ import { Connection, HandleType } from '@xyflow/system'; import { useStore } from './useStore'; import { useNodeId } from '../contexts/NodeIdContext'; -type useHandleConnectionStatusParams = { +type useHandleConnectionsParams = { handleType: HandleType; nodeId?: string; handleId?: string | null; @@ -12,6 +12,50 @@ type useHandleConnectionStatusParams = { onDisconnect?: (connections: Connection[]) => void; }; +/** + * Hook to check if a is connected to another and get the connections. + * + * @public + * @param param.handleType - 'source' or 'target' + * @param param.handleId - the handle id (this is only needed if the node has multiple handles of the same type) + * @param param.nodeId - node id - if not provided, the node id from the NodeIdContext is used + * @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 useHandleConnections({ + handleType, + handleId = null, + nodeId, + onConnect, + onDisconnect, +}: useHandleConnectionsParams): Connection[] { + const _nodeId = useNodeId(); + const prevConnections = useRef | null>(null); + const currentNodeId = nodeId || _nodeId; + + const connections = useStore( + (state) => state.connectionLookup.get(`${currentNodeId}-${handleType}-${handleId}`), + 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]); +} + +/** + * @internal + */ function areConnectionMapsEqual(a?: Map, b?: Map) { if (!a && !b) { return true; @@ -36,6 +80,7 @@ function areConnectionMapsEqual(a?: Map, b?: Map is connected to another and get the connections. - * - * @public - * @param param.handleType - 'source' or 'target' - * @param param.handleId - the handle id (this is only needed if the node has multiple handles of the same type) - * @param param.nodeId - node id - if not provided, the node id from the NodeIdContext is used - * @param param.onConnect - gets called when a connection is established - * @param param.onDisconnect - gets called when a connection is removed - * @returns a `connected` boolean and a connections array - */ -export function useHandleConnectionStatus({ - handleType, - handleId = null, - nodeId, - onConnect, - onDisconnect, -}: useHandleConnectionStatusParams): { - connected: boolean; - connections: Connection[] | null; -} { - const _nodeId = useNodeId(); - const prevConnections = useRef | null>(null); - const currentNodeId = nodeId || _nodeId; - - const connections = useStore( - (state) => state.connectionLookup.get(`${currentNodeId}-${handleType}-${handleId}`), - 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( - () => ({ - connected: !!connections, - connections: Array.from(connections?.values() ?? []), - }), - [connections] - ); -} diff --git a/packages/react/src/hooks/useNodesData.ts b/packages/react/src/hooks/useNodesData.ts new file mode 100644 index 00000000..69927642 --- /dev/null +++ b/packages/react/src/hooks/useNodesData.ts @@ -0,0 +1,33 @@ +import { useCallback } from 'react'; +import { shallow } from 'zustand/shallow'; + +import { useStore } from '../hooks/useStore'; + +export function useNodesData(nodeId: string): NodeData | null; +export function useNodesData(nodeIds: string[]): NodeData[]; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function useNodesData(nodeIds: any): any { + const nodesData = useStore( + useCallback( + (s) => { + if (!Array.isArray(nodeIds)) { + return s.nodeLookup.get(nodeIds)?.data || null; + } + + return nodeIds.reduce((res, id) => { + const node = s.nodeLookup.get(id); + + if (node) { + res.push(node.data); + } + + return res; + }, []); + }, + [nodeIds] + ), + shallow + ); + + return nodesData; +} diff --git a/packages/react/src/hooks/useUpdateNodeData.ts b/packages/react/src/hooks/useUpdateNodeData.ts new file mode 100644 index 00000000..10cd71e2 --- /dev/null +++ b/packages/react/src/hooks/useUpdateNodeData.ts @@ -0,0 +1,13 @@ +import { useCallback } from 'react'; + +import useReactFlow from './useReactFlow'; + +export function useUpdateNodeData() { + const { setNodes } = useReactFlow(); + + const updateNodeData = useCallback((id: string, data: unknown) => { + setNodes((prevNodes) => prevNodes.map((node) => (node.id === id ? { ...node, data } : node))); + }, []); + + return updateNodeData; +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index d9013b7c..63b70c98 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,5 +1,5 @@ export { default as ReactFlow } from './container/ReactFlow'; -export { default as Handle } from './components/Handle'; +export { default as Handle, type HandleComponentProps } from './components/Handle'; export { default as EdgeText } from './components/Edges/EdgeText'; export { StraightEdge } from './components/Edges/StraightEdge'; export { StepEdge } from './components/Edges/StepEdge'; @@ -22,7 +22,9 @@ export { useStore, useStoreApi } from './hooks/useStore'; export { default as useOnViewportChange, type UseOnViewportChangeOptions } from './hooks/useOnViewportChange'; export { default as useOnSelectionChange, type UseOnSelectionChangeOptions } from './hooks/useOnSelectionChange'; export { default as useNodesInitialized, type UseNodesInitializedOptions } from './hooks/useNodesInitialized'; -export { useHandleConnectionStatus } from './hooks/useHandleConnectionStatus'; +export { useHandleConnections } from './hooks/useHandleConnections'; +export { useUpdateNodeData } from './hooks/useUpdateNodeData'; +export { useNodesData } from './hooks/useNodesData'; export { useNodeId } from './contexts/NodeIdContext'; export { applyNodeChanges, applyEdgeChanges, handleParentExpand } from './utils/changes'; diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 22ad889e..e601ee08 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -22,8 +22,8 @@ export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => vo export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => void; export type Connection = { - source: string | null; - target: string | null; + source: string; + target: string; sourceHandle: string | null; targetHandle: string | null; };