diff --git a/README.md b/README.md index 90d52434..4c967925 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ React Flow is a library for building node-based graphs. You can easily implement - [Background](#background) - [Minimap](#minimap) - [Controls](#controls) + - [ReactFlowProvider](#reactflowprovider) - [Styling](#styling) - [Helper Functions](#helper-functions) - [Access Internal State](#access-internal-state) @@ -132,7 +133,7 @@ Fits view port so that all nodes are visible # Nodes -There are three different [node types](#node-types--custom-nodes) (`default`, `input`, `output`) you can use. The node types differ in the number and types of handles. An input node has only a source handle, a default node has a source and a target and an output node has only a target handle. You create nodes by adding them to the `elements` array of the React Flow component. +There are three different [node types](#node-types--custom-nodes) (`default`, `input`, `output`) you can use. The node types differ in the number and types of handles. An input node has only a source handle, a default node has a source and a target and an output node has only a target handle. You create nodes by adding them to the `elements` array of the `ReactFlow` component. Node example: `{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }` @@ -160,7 +161,7 @@ The standard node types are `input`, `default` and `output`. The default node ty ``` The keys represent the type names and the values are the components that get rendered. -If you want to introduce a new type you can pass a `nodeTypes` object to the React Flow component: +If you want to introduce a new type you can pass a `nodeTypes` object to the `ReactFlow` component: ```javascript nodeTypes={{ @@ -222,7 +223,7 @@ You can find an example of how to implement a custom node with multiple handles # Edges -React Flow comes with three [edge types](#edge-types--custom-edges) (`straight`, `default`, `step`). As the names indicate, the edges differ in the representation. The default type is a bezier edge. You create edges by adding them to your `elements` array of the React Flow component. +React Flow comes with three [edge types](#edge-types--custom-edges) (`straight`, `default`, `step`). As the names indicate, the edges differ in the representation. The default type is a bezier edge. You create edges by adding them to your `elements` array of the `ReactFlow` component. Edge example: `{ id: 'e1-2', type: 'straight', source: '1', target: '2', animated: true, label: 'edge label' }` @@ -256,7 +257,7 @@ The basic edge types are `straight`, `default` and `step`. The default `edgeType ``` The keys represent the type names and the values are the edge components. -If you want to introduce a new edge type you can pass an `edgeTypes` object to the React Flow component: +If you want to introduce a new edge type you can pass an `edgeTypes` object to the `ReactFlow` component: ```javascript edgeTypes={{ @@ -272,7 +273,7 @@ There is an implementation of a custom edge in the [edges example](/example/src/ ## Background -React Flow comes with two background variants: **dots** and **lines**. You can use it by passing it as a children to the React Flow component: +React Flow comes with two background variants: **dots** and **lines**. You can use it by passing it as a children to the `ReactFlow` component: ```javascript import ReactFlow, { Background } from 'react-flow-renderer'; @@ -299,7 +300,7 @@ const FlowWithBackground = () => ( ## MiniMap -You can use the mini map plugin by passing it as a children to the React Flow component: +You can use the mini map plugin by passing it as a children to the `ReactFlow` component: ```javascript import ReactFlow, { MiniMap } from 'react-flow-renderer'; @@ -330,7 +331,7 @@ const FlowWithMiniMap = () => ( ## Controls -The control panel contains a zoom-in, zoom-out, fit-view and a lock/unlock button. You can use it by passing it as a children to the React Flow component: +The control panel contains a zoom-in, zoom-out, fit-view and a lock/unlock button. You can use it by passing it as a children to the `ReactFlow` component: ```javascript import ReactFlow, { Controls } from 'react-flow-renderer'; @@ -350,6 +351,25 @@ const FlowWithControls = () => ( - `style`: css properties - `className`: additional class name +## ReactFlowProvider + +If you need access to the internal state and action of React Flow outside of the `ReactFlow` component you can it with the `ReactFlowProvider` component: + +```javascript +import ReactFlow, { ReactFlowProvider } from 'react-flow-renderer'; + +const FlowWithOwnProvider = () => ( + + + +); +``` + + # Styling There are two ways how you can style the graph pane and the elements. @@ -401,7 +421,7 @@ The React Flow wrapper has the className `react-flow`. If you want to change the ## Using Properties -You could achieve the same effect by passing a style prop to the React Flow component: +You could achieve the same effect by passing a style prop to the `ReactFlow` component: ```javascript const FlowWithRedBg = ( @@ -450,7 +470,7 @@ You can use these function as seen in [this example](/example/src/Overview/index # Access Internal State Under the hood React Flow uses [Easy Peasy](https://easy-peasy.now.sh/) for state handling. -If you need to access the internal state you can use the `useStoreState` hook inside a child component of the React Flow component: +If you need to access the internal state you can use the `useStoreState` hook inside a child component of the `ReactFlow` component: ```javascript import ReactFlow, { useStoreState } from 'react-flow-renderer'; @@ -470,6 +490,8 @@ const Flow = () => ( ); ``` +If you need more control you can wrap the `ReactFlow` component with the `ReactFlowProvider` component in order to be able to call `useStoreState` outside of the component. + # Examples You can find all examples in the [example](example) folder or check out the live versions: diff --git a/example/src/Provider/index.js b/example/src/Provider/index.js new file mode 100644 index 00000000..be1c2054 --- /dev/null +++ b/example/src/Provider/index.js @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; + +import ReactFlow, { addEdge, ReactFlowProvider } from 'react-flow-renderer'; + +const onElementClick = element => console.log('click', element); + +const initialElements = [ + { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, + { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, + { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, + { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, + { id: 'e1-2', source: '1', target: '2', animated: true }, + { id: 'e1-3', source: '1', target: '3' } +]; + +const ProviderFlow = () => { + const [elements, setElements] = useState(initialElements); + const onConnect = (params) => setElements(els => addEdge(params, els)); + + return ( + + + + ); +} + +export default ProviderFlow; diff --git a/example/src/index.js b/example/src/index.js index 831a7b10..ff0f52a8 100644 --- a/example/src/index.js +++ b/example/src/index.js @@ -11,6 +11,7 @@ import Empty from './Empty'; import Edges from './Edges'; import Validation from './Validation'; import Horizontal from './Horizontal'; +import Provider from './Provider'; import './index.css'; @@ -19,9 +20,9 @@ const routes = [{ component: Overview, label: 'Overview' }, { - path: '/basic', - component: Basic, - label: 'Basic' + path: '/provider', + component: Provider, + label: 'Provider' }, { path: '/edges', component: Edges, @@ -42,6 +43,9 @@ const routes = [{ path: '/stress', component: Stress, label: 'Stress' +}, { + path: '/basic', + component: Basic }, { path: '/empty', component: Empty diff --git a/rollup.config.js b/rollup.config.js index 32e8c51f..a7d13be4 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -45,6 +45,7 @@ export default [ }), replace({ 'process.env.NODE_ENV': JSON.stringify(processEnv), + __REACT_FLOW_VERSION__: JSON.stringify(pkg.version), }), svgr(), typescript({ diff --git a/src/additional-components/ReactFlowProvider/index.tsx b/src/additional-components/ReactFlowProvider/index.tsx new file mode 100644 index 00000000..400b4cbc --- /dev/null +++ b/src/additional-components/ReactFlowProvider/index.tsx @@ -0,0 +1,16 @@ +import React, { useMemo, FC } from 'react'; +import { StoreProvider, createStore } from 'easy-peasy'; + +import { storeModel } from '../../store'; + +const ReactFlowProvider: FC = ({ children }) => { + const store = useMemo(() => { + return createStore(storeModel); + }, []); + + return {children}; +}; + +ReactFlowProvider.displayName = 'ReactFlowProvider'; + +export default ReactFlowProvider; diff --git a/src/additional-components/index.ts b/src/additional-components/index.ts index 3322bfcf..b2a86074 100644 --- a/src/additional-components/index.ts +++ b/src/additional-components/index.ts @@ -4,3 +4,4 @@ export { default as MiniMap } from './MiniMap'; export { default as Controls } from './Controls'; export { default as Background } from './Background'; +export { default as ReactFlowProvider } from './ReactFlowProvider'; diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 45faf948..920350d4 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -2,10 +2,20 @@ import React, { useEffect, useRef, useState, memo, ComponentType, CSSProperties import { DraggableCore } from 'react-draggable'; import cx from 'classnames'; import { ResizeObserver } from 'resize-observer'; +import { useStoreActions } from '../../store/hooks'; import { Provider } from '../../contexts/NodeIdContext'; -import store from '../../store'; -import { Node, XYPosition, Transform, ElementId, NodeComponentProps, WrapNodeProps } from '../../types'; +import { + Node, + XYPosition, + Transform, + ElementId, + NodeComponentProps, + WrapNodeProps, + Elements, + Edge, + NodePosUpdate, +} from '../../types'; const getMouseEvent = (evt: MouseEvent | TouchEvent) => typeof TouchEvent !== 'undefined' && evt instanceof TouchEvent ? evt.touches[0] : (evt as MouseEvent); @@ -19,6 +29,7 @@ interface OnDragStartParams { setOffset: (pos: XYPosition) => void; transform: Transform; position: XYPosition; + setSelectedElements: (elms: Elements | Node | Edge) => void; onNodeDragStart?: (node: Node) => void; } @@ -32,6 +43,7 @@ const onStart = ({ setOffset, transform, position, + setSelectedElements, }: OnDragStartParams): false | void => { const startEvt = getMouseEvent(evt); @@ -51,7 +63,7 @@ const onStart = ({ } if (selectNodesOnDrag) { - store.dispatch.setSelectedElements({ id, type } as Node); + setSelectedElements({ id, type } as Node); } }; @@ -61,9 +73,10 @@ interface OnDragParams { id: ElementId; offset: XYPosition; transform: Transform; + updateNodePos: (params: NodePosUpdate) => void; } -const onDrag = ({ evt, setDragging, id, offset, transform }: OnDragParams): void => { +const onDrag = ({ evt, setDragging, id, offset, transform, updateNodePos }: OnDragParams): void => { const dragEvt = getMouseEvent(evt); const scaledClient = { @@ -72,7 +85,7 @@ const onDrag = ({ evt, setDragging, id, offset, transform }: OnDragParams): void }; setDragging(true); - store.dispatch.updateNodePos({ + updateNodePos({ id, pos: { x: scaledClient.x - transform[0] - offset.x, @@ -89,6 +102,7 @@ interface OnDragStopParams { position: XYPosition; data: any; selectNodesOnDrag: boolean; + setSelectedElements: (elms: Elements | Node | Edge) => void; onNodeDragStop?: (node: Node) => void; onClick?: (node: Node) => void; } @@ -103,6 +117,7 @@ const onStop = ({ selectNodesOnDrag, onNodeDragStop, onClick, + setSelectedElements, }: OnDragStopParams): void => { const node = { id, @@ -113,7 +128,7 @@ const onStop = ({ if (!isDragging) { if (!selectNodesOnDrag) { - store.dispatch.setSelectedElements({ id, type } as Node); + setSelectedElements({ id, type } as Node); } if (onClick) { @@ -148,6 +163,10 @@ export default (NodeComponent: ComponentType) => { sourcePosition, targetPosition, }: WrapNodeProps) => { + const updateNodeDimensions = useStoreActions((a) => a.updateNodeDimensions); + const setSelectedElements = useStoreActions((a) => a.setSelectedElements); + const updateNodePos = useStoreActions((a) => a.updateNodePos); + const nodeElement = useRef(null); const [offset, setOffset] = useState({ x: 0, y: 0 }); const [isDragging, setDragging] = useState(false); @@ -163,11 +182,11 @@ export default (NodeComponent: ComponentType) => { useEffect(() => { if (nodeElement.current) { - store.dispatch.updateNodeDimensions({ id, nodeElement: nodeElement.current }); + updateNodeDimensions({ id, nodeElement: nodeElement.current }); const resizeObserver = new ResizeObserver((entries) => { for (let _ of entries) { - store.dispatch.updateNodeDimensions({ id, nodeElement: nodeElement.current! }); + updateNodeDimensions({ id, nodeElement: nodeElement.current! }); } }); @@ -196,11 +215,23 @@ export default (NodeComponent: ComponentType) => { setOffset, transform, position, + setSelectedElements, }) } - onDrag={(evt) => onDrag({ evt: evt as MouseEvent, setDragging, id, offset, transform })} + onDrag={(evt) => onDrag({ evt: evt as MouseEvent, setDragging, id, offset, transform, updateNodePos })} onStop={() => - onStop({ onNodeDragStop, selectNodesOnDrag, onClick, isDragging, setDragging, id, type, position, data }) + onStop({ + onNodeDragStop, + selectNodesOnDrag, + onClick, + isDragging, + setDragging, + id, + type, + position, + data, + setSelectedElements, + }) } scale={transform[2]} disabled={!isInteractive} diff --git a/src/container/ReactFlow/Wrapper.tsx b/src/container/ReactFlow/Wrapper.tsx new file mode 100644 index 00000000..c5cc6de0 --- /dev/null +++ b/src/container/ReactFlow/Wrapper.tsx @@ -0,0 +1,21 @@ +import React, { FC } from 'react'; +import { StoreProvider, useStore } from 'easy-peasy'; + +import store, { StoreModel } from '../../store'; + +const Wrapper: FC = ({ children }) => { + const easyPeasyStore = useStore(); + const isWrapepdWithReactFlowProvider = easyPeasyStore?.getState()?.reactFlowVersion; + + if (isWrapepdWithReactFlowProvider) { + // we need to wrap it with a fragment because t's not allowed for children to be a ReactNode + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 + return <>{children}; + } + + return {children}; +}; + +Wrapper.displayName = 'ReactFlowWrapper'; + +export default Wrapper; diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index de742138..82baff23 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -1,6 +1,5 @@ import React, { useMemo, CSSProperties, HTMLAttributes } from 'react'; import cx from 'classnames'; -import { StoreProvider } from 'easy-peasy'; const nodeEnv: string = process.env.NODE_ENV as string; @@ -19,7 +18,7 @@ import BezierEdge from '../../components/Edges/BezierEdge'; import StraightEdge from '../../components/Edges/StraightEdge'; import StepEdge from '../../components/Edges/StepEdge'; import { createEdgeTypes } from '../EdgeRenderer/utils'; -import store from '../../store'; +import Wrapper from './Wrapper'; import { Elements, NodeTypesType, @@ -92,7 +91,7 @@ const ReactFlow = ({ return (
- + {onSelectionChange && } {children} - +
); }; diff --git a/src/custom.d.ts b/src/custom.d.ts index 52c9c30a..41d5c1c7 100644 --- a/src/custom.d.ts +++ b/src/custom.d.ts @@ -3,8 +3,7 @@ declare module '*.css' { export default content; } -interface SvgrComponent - extends React.StatelessComponent> {} +interface SvgrComponent extends React.StatelessComponent> {} declare module '*.svg' { const svgUrl: string; @@ -12,3 +11,5 @@ declare module '*.svg' { export default svgUrl; export { svgComponent as ReactComponent }; } + +declare var __REACT_FLOW_VERSION__: string; diff --git a/src/store/index.ts b/src/store/index.ts index b0d00dc8..ca68b3d0 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -19,6 +19,7 @@ import { SelectionRect, HandleType, SetConnectionId, + NodePosUpdate, } from '../types'; type TransformXYK = { @@ -27,11 +28,6 @@ type TransformXYK = { k: number; }; -type NodePosUpdate = { - id: ElementId; - pos: XYPosition; -}; - type NodeDimensionUpdate = { id: ElementId; nodeElement: HTMLDivElement; @@ -87,6 +83,8 @@ export interface StoreModel { isInteractive: boolean; + reactFlowVersion: string; + onConnect: OnConnectFunc; setOnConnect: Action; @@ -126,7 +124,7 @@ export interface StoreModel { unsetUserSelection: Action; } -const storeModel: StoreModel = { +export const storeModel: StoreModel = { width: 0, height: 0, transform: [0, 0, 1], @@ -163,6 +161,8 @@ const storeModel: StoreModel = { isInteractive: true, + reactFlowVersion: __REACT_FLOW_VERSION__, + onConnect: () => {}, setOnConnect: action((state, onConnect) => { diff --git a/src/types/index.ts b/src/types/index.ts index 046a0971..859d2727 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -203,3 +203,8 @@ export interface EdgeTextProps { labelShowBg?: boolean; labelBgStyle?: CSSProperties; } + +export type NodePosUpdate = { + id: ElementId; + pos: XYPosition; +};