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 = {