feat(hooks): add useNodesData, useUpdateNodeData, simplify useHandleConnections
This commit is contained in:
@@ -44,8 +44,9 @@ import CancelConnection from '../examples/CancelConnection';
|
|||||||
import InteractiveMinimap from '../examples/InteractiveMinimap';
|
import InteractiveMinimap from '../examples/InteractiveMinimap';
|
||||||
import UseOnSelectionChange from '../examples/UseOnSelectionChange';
|
import UseOnSelectionChange from '../examples/UseOnSelectionChange';
|
||||||
import NodeToolbar from '../examples/NodeToolbar';
|
import NodeToolbar from '../examples/NodeToolbar';
|
||||||
import useNodesInitialized from '../examples/UseNodesInit';
|
import UseNodesInitialized from '../examples/UseNodesInit';
|
||||||
import useHandleConnectionStatus from '../examples/UseHandleConnectionStatus';
|
import UseNodesData from '../examples/UseNodesData';
|
||||||
|
import UseHandleConnections from '../examples/UseHandleConnections';
|
||||||
|
|
||||||
export interface IRoute {
|
export interface IRoute {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -262,7 +263,7 @@ const routes: IRoute[] = [
|
|||||||
{
|
{
|
||||||
name: 'useNodesInitialized',
|
name: 'useNodesInitialized',
|
||||||
path: 'use-nodes-initialized',
|
path: 'use-nodes-initialized',
|
||||||
component: useNodesInitialized,
|
component: UseNodesInitialized,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'useOnSelectionChange',
|
name: 'useOnSelectionChange',
|
||||||
@@ -275,9 +276,14 @@ const routes: IRoute[] = [
|
|||||||
component: UseReactFlow,
|
component: UseReactFlow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'useHandleConnectionStatus',
|
name: 'useHandleConnections',
|
||||||
path: 'usehandleconnectionstatus',
|
path: 'usehandleconnections',
|
||||||
component: useHandleConnectionStatus,
|
component: UseHandleConnections,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'useNodesData',
|
||||||
|
path: 'usenodesdata',
|
||||||
|
component: UseNodesData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'useUpdateNodeInternals',
|
name: 'useUpdateNodeInternals',
|
||||||
|
|||||||
+4
-5
@@ -1,6 +1,5 @@
|
|||||||
import { memo, FC, useEffect, useCallback } from 'react';
|
import { memo, FC, useEffect, useCallback } from 'react';
|
||||||
import { Handle, Position, NodeProps, useHandleConnectionStatus, Connection } from '@xyflow/react';
|
import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleComponentProps } from '@xyflow/react';
|
||||||
import { HandleComponentProps } from '@xyflow/react/dist/esm/components/Handle';
|
|
||||||
|
|
||||||
function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeId: string }) {
|
function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeId: string }) {
|
||||||
const onConnect = useCallback(
|
const onConnect = useCallback(
|
||||||
@@ -12,7 +11,7 @@ function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeI
|
|||||||
(connections: Connection[]) => console.log('onDisconnect handler, node id:', nodeId, connections),
|
(connections: Connection[]) => console.log('onDisconnect handler, node id:', nodeId, connections),
|
||||||
[nodeId]
|
[nodeId]
|
||||||
);
|
);
|
||||||
const status = useHandleConnectionStatus({
|
const connections = useHandleConnections({
|
||||||
handleType: handleProps.type,
|
handleType: handleProps.type,
|
||||||
handleId: handleProps.id,
|
handleId: handleProps.id,
|
||||||
onConnect,
|
onConnect,
|
||||||
@@ -20,8 +19,8 @@ function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeI
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('useEffect, node id:', nodeId, handleProps.type, status);
|
console.log('useEffect, node id:', nodeId, handleProps.type, connections);
|
||||||
}, [status]);
|
}, [connections]);
|
||||||
|
|
||||||
return <Handle {...handleProps} />;
|
return <Handle {...handleProps} />;
|
||||||
}
|
}
|
||||||
+4
-5
@@ -1,6 +1,5 @@
|
|||||||
import { memo, FC, useEffect, useCallback } from 'react';
|
import { memo, FC, useEffect, useCallback } from 'react';
|
||||||
import { Handle, Position, NodeProps, useHandleConnectionStatus, Connection } from '@xyflow/react';
|
import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleComponentProps } from '@xyflow/react';
|
||||||
import { HandleComponentProps } from '@xyflow/react/dist/esm/components/Handle';
|
|
||||||
|
|
||||||
function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeId: string }) {
|
function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeId: string }) {
|
||||||
const onConnect = useCallback(
|
const onConnect = useCallback(
|
||||||
@@ -15,7 +14,7 @@ function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeI
|
|||||||
},
|
},
|
||||||
[nodeId]
|
[nodeId]
|
||||||
);
|
);
|
||||||
const status = useHandleConnectionStatus({
|
const connections = useHandleConnections({
|
||||||
handleType: handleProps.type,
|
handleType: handleProps.type,
|
||||||
handleId: handleProps.id,
|
handleId: handleProps.id,
|
||||||
onConnect,
|
onConnect,
|
||||||
@@ -23,8 +22,8 @@ function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeI
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('useEffect, node id:', nodeId, handleProps.type, status);
|
console.log('useEffect, node id:', nodeId, handleProps.type, connections);
|
||||||
}, [status]);
|
}, [connections]);
|
||||||
|
|
||||||
return <Handle {...handleProps} />;
|
return <Handle {...handleProps} />;
|
||||||
}
|
}
|
||||||
@@ -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 (
|
||||||
|
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||||
|
<Handle type="target" position={Position.Left} />
|
||||||
|
<div>
|
||||||
|
incoming texts:{' '}
|
||||||
|
{nodesData?.filter((nodeData) => nodeData.text).map(({ text }, i) => <div key={i}>{text}</div>) || 'none'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ResultNode);
|
||||||
@@ -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<HTMLInputElement> = (evt) => updateNodeData(id, { text: evt.target.value });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||||
|
<div>node {id}</div>
|
||||||
|
<div>
|
||||||
|
<input onChange={onChange} value={data.text} />
|
||||||
|
</div>
|
||||||
|
<Handle type="source" position={Position.Right} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(TextNode);
|
||||||
@@ -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 (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
fitView
|
||||||
|
>
|
||||||
|
<Controls />
|
||||||
|
<Background />
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomNodeFlow;
|
||||||
+46
-51
@@ -4,7 +4,7 @@ import { Connection, HandleType } from '@xyflow/system';
|
|||||||
import { useStore } from './useStore';
|
import { useStore } from './useStore';
|
||||||
import { useNodeId } from '../contexts/NodeIdContext';
|
import { useNodeId } from '../contexts/NodeIdContext';
|
||||||
|
|
||||||
type useHandleConnectionStatusParams = {
|
type useHandleConnectionsParams = {
|
||||||
handleType: HandleType;
|
handleType: HandleType;
|
||||||
nodeId?: string;
|
nodeId?: string;
|
||||||
handleId?: string | null;
|
handleId?: string | null;
|
||||||
@@ -12,6 +12,50 @@ type useHandleConnectionStatusParams = {
|
|||||||
onDisconnect?: (connections: Connection[]) => void;
|
onDisconnect?: (connections: Connection[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to check if a <Handle /> is connected to another <Handle /> 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<Map<string, Connection> | 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<string, Connection>, b?: Map<string, Connection>) {
|
function areConnectionMapsEqual(a?: Map<string, Connection>, b?: Map<string, Connection>) {
|
||||||
if (!a && !b) {
|
if (!a && !b) {
|
||||||
return true;
|
return true;
|
||||||
@@ -36,6 +80,7 @@ function areConnectionMapsEqual(a?: Map<string, Connection>, b?: Map<string, Con
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* We call the callback for all connections in a that are not in b
|
* We call the callback for all connections in a that are not in b
|
||||||
|
*
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
function handleConnectionChange(
|
function handleConnectionChange(
|
||||||
@@ -59,53 +104,3 @@ function handleConnectionChange(
|
|||||||
cb(diff);
|
cb(diff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to check if a <Handle /> is connected to another <Handle /> 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<Map<string, Connection> | 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]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { shallow } from 'zustand/shallow';
|
||||||
|
|
||||||
|
import { useStore } from '../hooks/useStore';
|
||||||
|
|
||||||
|
export function useNodesData<NodeData = unknown>(nodeId: string): NodeData | null;
|
||||||
|
export function useNodesData<NodeData = unknown>(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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
export { default as ReactFlow } from './container/ReactFlow';
|
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 { default as EdgeText } from './components/Edges/EdgeText';
|
||||||
export { StraightEdge } from './components/Edges/StraightEdge';
|
export { StraightEdge } from './components/Edges/StraightEdge';
|
||||||
export { StepEdge } from './components/Edges/StepEdge';
|
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 useOnViewportChange, type UseOnViewportChangeOptions } from './hooks/useOnViewportChange';
|
||||||
export { default as useOnSelectionChange, type UseOnSelectionChangeOptions } from './hooks/useOnSelectionChange';
|
export { default as useOnSelectionChange, type UseOnSelectionChangeOptions } from './hooks/useOnSelectionChange';
|
||||||
export { default as useNodesInitialized, type UseNodesInitializedOptions } from './hooks/useNodesInitialized';
|
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 { useNodeId } from './contexts/NodeIdContext';
|
||||||
|
|
||||||
export { applyNodeChanges, applyEdgeChanges, handleParentExpand } from './utils/changes';
|
export { applyNodeChanges, applyEdgeChanges, handleParentExpand } from './utils/changes';
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => vo
|
|||||||
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => void;
|
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => void;
|
||||||
|
|
||||||
export type Connection = {
|
export type Connection = {
|
||||||
source: string | null;
|
source: string;
|
||||||
target: string | null;
|
target: string;
|
||||||
sourceHandle: string | null;
|
sourceHandle: string | null;
|
||||||
targetHandle: string | null;
|
targetHandle: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user