diff --git a/example/src/ControlledUncontrolled/index.tsx b/example/src/ControlledUncontrolled/index.tsx new file mode 100644 index 00000000..2cb8a0de --- /dev/null +++ b/example/src/ControlledUncontrolled/index.tsx @@ -0,0 +1,98 @@ +import ReactFlow, { + useReactFlow, + Background, + BackgroundVariant, + Node, + Edge, + ReactFlowProvider, + useNodesState, + useEdgesState, +} from 'react-flow-renderer'; + +const defaultNodes: Node[] = [ + { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, + { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, + { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, + { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' }, +]; + +const defaultEdges: Edge[] = [ + { id: 'e1-2', source: '1', target: '2' }, + { id: 'e1-3', source: '1', target: '3' }, +]; + +const defaultEdgeOptions = { + animated: true, +}; + +// This is bad practise. You should either use a controlled or an uncontrolled component. +// This is just an example for testing the API. +const ControlledUncontrolled = () => { + const [nodes, , onNodesChange] = useNodesState(defaultNodes); + const [edges, , onEdgesChange] = useEdgesState(defaultEdges); + const instance = useReactFlow(); + + const logToObject = () => console.log(instance.toObject()); + const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 }); + + const updateNodePositions = () => { + instance.setNodes((nodes) => + nodes.map((node) => { + node.position = { + x: Math.random() * 400, + y: Math.random() * 400, + }; + + return node; + }) + ); + }; + + const updateEdgeColors = () => { + instance.setEdges((edges) => + edges.map((edge) => { + edge.style = { + stroke: '#ff5050', + }; + + return edge; + }) + ); + }; + + return ( + + + +
+ + + + +
+
+ ); +}; + +export default function App() { + return ( + + + + ); +} diff --git a/example/src/index.tsx b/example/src/index.tsx index 84d583fe..b30e9086 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -32,6 +32,7 @@ import SaveRestore from './SaveRestore'; import SwitchFlow from './Switch'; import Validation from './Validation'; import DefaultNodes from './DefaultNodes'; +import ControlledUncontrolled from './ControlledUncontrolled'; import './index.css'; @@ -156,6 +157,10 @@ const routes = [ path: '/validation', component: Validation, }, + { + path: '/controlled-uncontrolled', + component: ControlledUncontrolled, + }, ]; const Header = withRouter(({ history, location }) => { diff --git a/src/components/Handle/index.tsx b/src/components/Handle/index.tsx index 1529ea95..88acd1d5 100644 --- a/src/components/Handle/index.tsx +++ b/src/components/Handle/index.tsx @@ -21,7 +21,7 @@ const selector = (s: ReactFlowState) => ({ connectionMode: s.connectionMode, connectionStartHandle: s.connectionStartHandle, connectOnClick: s.connectOnClick, - isControlled: s.isControlled, + hasDefaultEdges: s.hasDefaultEdges, }); const Handle = forwardRef( @@ -49,7 +49,7 @@ const Handle = forwardRef( connectionMode, connectionStartHandle, connectOnClick, - isControlled, + hasDefaultEdges, } = useStore(selector, shallow); const handleId = id || null; @@ -63,15 +63,15 @@ const Handle = forwardRef( ...defaultEdgeOptions, ...params, }; - if (isControlled) { + if (hasDefaultEdges) { const { edges } = store.getState(); store.setState({ edges: addEdge(edgeParams, edges) }); - } else { - onConnectAction?.(edgeParams); } + + onConnectAction?.(edgeParams); onConnect?.(edgeParams); }, - [isControlled, onConnectAction, onConnect] + [hasDefaultEdges, onConnectAction, onConnect] ); const onMouseDownHandler = useCallback( diff --git a/src/components/StoreUpdater/index.tsx b/src/components/StoreUpdater/index.tsx index 519e102f..49707d53 100644 --- a/src/components/StoreUpdater/index.tsx +++ b/src/components/StoreUpdater/index.tsx @@ -111,9 +111,7 @@ const StoreUpdater = ({ const store = useStoreApi(); useEffect(() => { - if (defaultNodes) { - setDefaultNodesAndEdges(defaultNodes, defaultEdges); - } + setDefaultNodesAndEdges(defaultNodes, defaultEdges); return () => { reset(); diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index cd05cab8..d134fa59 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -25,7 +25,7 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode); useEffect(() => { - const { nodeInternals, edges, isControlled } = store.getState(); + const { nodeInternals, edges, hasDefaultNodes, hasDefaultEdges } = store.getState(); // @TODO: work with nodeInternals instead of converting it to an array const nodes = Array.from(nodeInternals).map(([_, node]) => node); const selectedNodes = nodes.filter((n) => n.selected); @@ -35,24 +35,31 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { const connectedEdges = getConnectedEdges(selectedNodes, edges); const edgeIdsToRemove = [...selectedEdges, ...connectedEdges].map((e) => e.id); - if (isControlled) { + if (hasDefaultNodes) { selectedNodes.forEach((node) => { nodeInternals.delete(node.id); }); + } + if (hasDefaultEdges) { store.setState({ nodeInternals: new Map(nodeInternals), edges: edges.filter((e) => !edgeIdsToRemove.includes(e.id)), }); - } else { + } + + if (onNodesChange) { const nodeChanges: NodeChange[] = selectedNodes.map((n) => ({ id: n.id, type: 'remove' })); + onNodesChange(nodeChanges); + } + + if (onEdgesChange) { const edgeChanges: EdgeChange[] = edgeIdsToRemove.map((id) => ({ id, type: 'remove', })); - onNodesChange?.(nodeChanges); - onEdgesChange?.(edgeChanges); + onEdgesChange(edgeChanges); } store.setState({ nodesSelectionActive: false }); diff --git a/src/store/index.ts b/src/store/index.ts index 2c4f20e6..d0b0840e 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -43,9 +43,14 @@ const createStore = () => set({ edges }); } }, - setDefaultNodesAndEdges: (nodes: Node[], edges: Edge[] = []) => { - const nodeInternals = createNodeInternals(nodes, get().nodeInternals); - set({ nodeInternals, edges, isControlled: true }); + setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => { + const hasDefaultNodes = typeof nodes !== 'undefined'; + const hasDefaultEdges = typeof edges !== 'undefined'; + + const nodeInternals = hasDefaultNodes ? createNodeInternals(nodes, new Map()) : new Map(); + const nextEdges = hasDefaultEdges ? edges : []; + + set({ nodeInternals, edges: nextEdges, hasDefaultNodes, hasDefaultEdges }); }, updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { const { onNodesChange, transform, nodeInternals, fitViewOnInit } = get(); @@ -88,9 +93,9 @@ const createStore = () => } }, updateNodePosition: ({ id, diff, dragging }: NodeDiffUpdate) => { - const { onNodesChange, nodeExtent, nodeInternals, isControlled } = get(); + const { onNodesChange, nodeExtent, nodeInternals, hasDefaultNodes } = get(); - if (isControlled || onNodesChange) { + if (hasDefaultNodes || onNodesChange) { const changes: NodeDimensionChange[] = []; nodeInternals.forEach((node) => { @@ -104,18 +109,18 @@ const createStore = () => }); if (changes?.length) { - if (isControlled) { + if (hasDefaultNodes) { const nodes = applyNodeChanges(changes, Array.from(nodeInternals.values())); const nextNodeInternals = createNodeInternals(nodes, nodeInternals); set({ nodeInternals: nextNodeInternals }); - } else { - onNodesChange?.(changes); } + + onNodesChange?.(changes); } } }, addSelectedNodes: (selectedNodeIds: string[]) => { - const { multiSelectionActive, onNodesChange, nodeInternals, isControlled } = get(); + const { multiSelectionActive, onNodesChange, nodeInternals, hasDefaultNodes } = get(); // @TODO: work with nodeInternals instead of converting it to an array const nodes = Array.from(nodeInternals).map(([_, node]) => node); let changedNodes: NodeSelectionChange[]; @@ -127,15 +132,15 @@ const createStore = () => } if (changedNodes.length) { - if (isControlled) { + if (hasDefaultNodes) { set({ nodeInternals: handleControlledNodeSelectionChange(changedNodes, nodeInternals) }); - } else if (onNodesChange) { - onNodesChange(changedNodes); } + + onNodesChange?.(changedNodes); } }, addSelectedEdges: (selectedEdgeIds: string[]) => { - const { multiSelectionActive, onEdgesChange, edges, isControlled } = get(); + const { multiSelectionActive, onEdgesChange, edges, hasDefaultEdges } = get(); let changedEdges: EdgeSelectionChange[]; @@ -146,17 +151,16 @@ const createStore = () => } if (changedEdges.length) { - if (isControlled) { + if (hasDefaultEdges) { set({ edges: handleControlledEdgeSelectionChange(changedEdges, edges), }); - } else if (onEdgesChange) { - onEdgesChange(changedEdges); } + onEdgesChange?.(changedEdges); } }, unselectNodesAndEdges: () => { - const { nodeInternals, edges, onNodesChange, onEdgesChange, isControlled } = get(); + const { nodeInternals, edges, onNodesChange, onEdgesChange, hasDefaultNodes, hasDefaultEdges } = get(); // @TODO: work with nodeInternals instead of converting it to an array const nodes = Array.from(nodeInternals).map(([_, node]) => node); @@ -167,20 +171,18 @@ const createStore = () => const edgesToUnselect = edges.map((edge) => createSelectionChange(edge.id, false)) as EdgeSelectionChange[]; if (nodesToUnselect.length) { - if (isControlled) { + if (hasDefaultNodes) { set({ nodeInternals: handleControlledNodeSelectionChange(nodesToUnselect, nodeInternals) }); - } else if (onNodesChange) { - onNodesChange(nodesToUnselect); } + onNodesChange?.(nodesToUnselect); } if (edgesToUnselect.length) { - if (isControlled) { + if (hasDefaultEdges) { set({ edges: handleControlledEdgeSelectionChange(edgesToUnselect, edges), }); - } else if (onEdgesChange) { - onEdgesChange(edgesToUnselect); } + onEdgesChange?.(edgesToUnselect); } }, setMinZoom: (minZoom: number) => { @@ -202,8 +204,7 @@ const createStore = () => set({ translateExtent }); }, resetSelectedElements: () => { - const { nodeInternals, edges, onNodesChange, onEdgesChange, isControlled } = get(); - // @TODO: work with nodeInternals instead of converting it to an array + const { nodeInternals, edges, onNodesChange, onEdgesChange, hasDefaultNodes, hasDefaultEdges } = get(); const nodes = Array.from(nodeInternals.values()); const nodesToUnselect = nodes @@ -214,22 +215,20 @@ const createStore = () => .map((e) => createSelectionChange(e.id, false)) as EdgeSelectionChange[]; if (nodesToUnselect.length) { - if (isControlled) { + if (hasDefaultNodes) { set({ nodeInternals: handleControlledNodeSelectionChange(nodesToUnselect, nodeInternals), }); - } else if (onNodesChange) { - onNodesChange(nodesToUnselect); } + onNodesChange?.(nodesToUnselect); } if (edgesToUnselect.length) { - if (isControlled) { + if (hasDefaultEdges) { set({ edges: handleControlledEdgeSelectionChange(edgesToUnselect, edges), }); - } else if (onEdgesChange) { - onEdgesChange(edgesToUnselect); } + onEdgesChange?.(edgesToUnselect); } }, setNodeExtent: (nodeExtent: CoordinateExtent) => { diff --git a/src/store/initialState.ts b/src/store/initialState.ts index ef826882..04a358e9 100644 --- a/src/store/initialState.ts +++ b/src/store/initialState.ts @@ -13,7 +13,8 @@ const initialState: ReactFlowStore = { edges: [], onNodesChange: null, onEdgesChange: null, - isControlled: false, + hasDefaultNodes: false, + hasDefaultEdges: false, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, d3Zoom: null, d3Selection: null, diff --git a/src/types/general.ts b/src/types/general.ts index 47b366b0..fd45b6ef 100644 --- a/src/types/general.ts +++ b/src/types/general.ts @@ -122,7 +122,8 @@ export type ReactFlowStore = { selectedNodesBbox: Rect; onNodesChange: OnNodesChange | null; onEdgesChange: OnEdgesChange | null; - isControlled: boolean; + hasDefaultNodes: boolean; + hasDefaultEdges: boolean; d3Zoom: ZoomBehavior | null; d3Selection: D3Selection | null; @@ -169,7 +170,7 @@ export type ReactFlowStore = { export type ReactFlowActions = { setNodes: (nodes: Node[]) => void; setEdges: (edges: Edge[]) => void; - setDefaultNodesAndEdges: (nodes: Node[], edges?: Edge[]) => void; + setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void; updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; updateNodePosition: (update: NodeDiffUpdate) => void; resetSelectedElements: () => void;