From cec7da54dfff818c18cbf3267bcf383888260ad7 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Thu, 28 Aug 2025 10:44:54 +0200 Subject: [PATCH] initial version of middlewares --- .../react/src/examples/Overview/index.tsx | 36 ++++++- .../src/components/BatchProvider/index.tsx | 24 ++++- packages/react/src/hooks/useMiddleware.ts | 97 +++++++++++++++++++ packages/react/src/index.ts | 2 + packages/react/src/store/index.ts | 8 +- packages/react/src/store/initialState.ts | 5 + packages/react/src/types/store.ts | 5 + 7 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 packages/react/src/hooks/useMiddleware.ts diff --git a/examples/react/src/examples/Overview/index.tsx b/examples/react/src/examples/Overview/index.tsx index 8dddbb0d..8e6a3441 100644 --- a/examples/react/src/examples/Overview/index.tsx +++ b/examples/react/src/examples/Overview/index.tsx @@ -17,6 +17,8 @@ import { ConnectionMode, OnBeforeDelete, OnDelete, + useNodeChangeMiddleware, + NodeChange, } from '@xyflow/react'; const onNodeDragStart = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes); @@ -191,6 +193,37 @@ const nodeColor = (n: Node): string => { return '#fff'; }; +function RestrictExtent({ + minX = -Infinity, + minY = -Infinity, + maxX = Infinity, + maxY = Infinity, +}: { + minX?: number; + minY?: number; + maxX?: number; + maxY?: number; +}) { + useNodeChangeMiddleware( + useCallback( + (changes: NodeChange[]) => { + return changes.map((change) => { + if (change.type === 'position' && change.position) { + const { position } = change; + position.x = Math.min(Math.max(position.x, minX), maxX); + position.y = Math.min(Math.max(position.y, minY), maxY); + change.position = position; + } + return change; + }); + }, + [minX, minY, maxX, maxY] + ) + ); + + return null; +} + const OverviewFlow = () => { const [nodes, , onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -234,8 +267,9 @@ const OverviewFlow = () => { maxZoom={Infinity} onBeforeDelete={onBeforeDelete} onDelete={onDelete} - onPaneMouseMove={onPaneMouseMove} + // onPaneMouseMove={onPaneMouseMove} > + diff --git a/packages/react/src/components/BatchProvider/index.tsx b/packages/react/src/components/BatchProvider/index.tsx index b2ed270f..d70d6fa8 100644 --- a/packages/react/src/components/BatchProvider/index.tsx +++ b/packages/react/src/components/BatchProvider/index.tsx @@ -28,7 +28,15 @@ export function BatchProvider(); const nodeQueueHandler = useCallback((queueItems: QueueItem[]) => { - const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup, fitViewQueued } = store.getState(); + const { + nodes = [], + setNodes, + hasDefaultNodes, + onNodesChange, + nodeLookup, + fitViewQueued, + nodeChangeMiddleware, + } = store.getState(); /* * This is essentially an `Array.reduce` in imperative clothing. Processing @@ -40,11 +48,23 @@ export function BatchProvider[]; + console.log('this workds'); + + for (const middleware of nodeChangeMiddleware.values()) { + changes = middleware(changes); + } + + // for (const middleware of nodeSyncMiddleware.values()) { + // changes = middleware(changes); + // } + + // Do we always want to setNodes(next) or only when there are changes? + if (hasDefaultNodes) { setNodes(next); } diff --git a/packages/react/src/hooks/useMiddleware.ts b/packages/react/src/hooks/useMiddleware.ts new file mode 100644 index 00000000..b2192564 --- /dev/null +++ b/packages/react/src/hooks/useMiddleware.ts @@ -0,0 +1,97 @@ +import { NodeChange } from '@xyflow/system'; +import { useEffect, useRef } from 'react'; +import { useStoreApi } from './useStore'; +import { Edge, Node } from '../types'; + +/** + * We identify each middleware with a symbol that is unique for each hook instance. + */ + +function useSymbol() { + const symbol = useRef(); + if (!symbol.current) { + symbol.current = Symbol(); + } + return symbol; +} + +export function useNodeChangeMiddleware( + fn: (changes: NodeChange[]) => NodeChange[] +) { + const store = useStoreApi(); + const symbol = useSymbol(); + + useEffect(() => { + const { nodeChangeMiddleware } = store.getState(); + nodeChangeMiddleware.set(symbol.current!, fn); + }, [fn]); + + useEffect(() => { + const { nodeChangeMiddleware } = store.getState(); + return () => { + nodeChangeMiddleware.delete(symbol.current!); + }; + }, []); +} + +export function useNodeChangeMiddlewareDynamic( + fn: (changes: NodeChange[]) => NodeChange[] +) { + const store = useStoreApi(); + const symbol = useSymbol(); + + const { nodeChangeMiddleware } = store.getState(); + nodeChangeMiddleware.delete(symbol.current!); + nodeChangeMiddleware.set(symbol.current!, fn); +} + +// export function useEdgeChangeMiddleware(fn: (changes: EdgeChange[]) => EdgeChange[]) { +// const store = useStoreApi(); +// const symbol = useSymbol(); + +// useEffect(() => { +// const { edgeChangeMiddleware } = store.getState(); +// edgeChangeMiddleware.set(symbol.current!, fn); +// }, [fn]); + +// useEffect(() => { +// const { edgeChangeMiddleware } = store.getState(); +// return () => { +// edgeChangeMiddleware.delete(symbol.current!); +// }; +// }, []); +// } + +// export function useNodeSyncMiddleware(fn: (changes: NodeChange[]) => NodeChange[]) { +// const store = useStoreApi(); +// const symbol = useSymbol(); + +// useEffect(() => { +// const { nodeSyncMiddleware } = store.getState(); +// nodeSyncMiddleware.set(symbol.current!, fn); +// }, [fn]); + +// useEffect(() => { +// const { nodeSyncMiddleware } = store.getState(); +// return () => { +// nodeSyncMiddleware.delete(symbol.current!); +// }; +// }, []); +// } + +// export function useEdgeSyncMiddleware(fn: (changes: EdgeChange[]) => EdgeChange[]) { +// const store = useStoreApi(); +// const symbol = useSymbol(); + +// useEffect(() => { +// const { edgeSyncMiddleware } = store.getState(); +// edgeSyncMiddleware.set(symbol.current!, fn); +// }, [fn]); + +// useEffect(() => { +// const { edgeSyncMiddleware } = store.getState(); +// return () => { +// edgeSyncMiddleware.delete(symbol.current!); +// }; +// }, []); +// } diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index b4a67f2e..859e51e8 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -30,6 +30,8 @@ export { useConnection } from './hooks/useConnection'; export { useInternalNode } from './hooks/useInternalNode'; export { useNodeId } from './contexts/NodeIdContext'; +export { useNodeChangeMiddleware, useNodeChangeMiddlewareDynamic } from './hooks/useMiddleware'; + export { applyNodeChanges, applyEdgeChanges } from './utils/changes'; export { isNode, isEdge } from './utils/general'; diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 085f8b1e..0391b041 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -175,8 +175,8 @@ const createStore = ({ }, updateNodePositions: (nodeDragItems, dragging = false) => { const parentExpandChildren: ParentExpandChild[] = []; - const changes = []; - const { nodeLookup, triggerNodeChanges } = get(); + let changes = []; + const { nodeLookup, triggerNodeChanges, nodeChangeMiddleware } = get(); for (const [id, dragItem] of nodeDragItems) { // we are using the nodelookup to be sure to use the current expandParent and parentId value @@ -216,6 +216,10 @@ const createStore = ({ changes.push(...parentExpandChanges); } + for (const middleware of nodeChangeMiddleware.values()) { + changes = middleware(changes); + } + triggerNodeChanges(changes); }, triggerNodeChanges: (changes) => { diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index bfc9baaa..917ef3e7 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -146,6 +146,11 @@ const getInitialState = ({ lib: 'react', debug: false, ariaLabelConfig: defaultAriaLabelConfig, + + nodeChangeMiddleware: new Map(), + edgeChangeMiddleware: new Map(), + nodeSyncMiddleware: new Map(), + edgeSyncMiddleware: new Map(), }; }; diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 1fe0d308..a1ab5d64 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -152,6 +152,11 @@ export type ReactFlowStore[]) => NodeChange[]>; + edgeChangeMiddleware: Map[]) => EdgeChange[]>; + nodeSyncMiddleware: Map[]) => NodeChange[]>; + edgeSyncMiddleware: Map[]) => EdgeChange[]>; }; export type ReactFlowActions = {