Merge pull request #278 from wbkd/develop

Develop
This commit is contained in:
Moritz
2020-06-02 18:36:48 +02:00
committed by GitHub
12 changed files with 166 additions and 34 deletions
+31 -9
View File
@@ -19,6 +19,7 @@ React Flow is a library for building node-based graphs. You can easily implement
- [Background](#background) - [Background](#background)
- [Minimap](#minimap) - [Minimap](#minimap)
- [Controls](#controls) - [Controls](#controls)
- [ReactFlowProvider](#reactflowprovider)
- [Styling](#styling) - [Styling](#styling)
- [Helper Functions](#helper-functions) - [Helper Functions](#helper-functions)
- [Access Internal State](#access-internal-state) - [Access Internal State](#access-internal-state)
@@ -132,7 +133,7 @@ Fits view port so that all nodes are visible
# Nodes # 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 } }` 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. 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 ```javascript
nodeTypes={{ nodeTypes={{
@@ -222,7 +223,7 @@ You can find an example of how to implement a custom node with multiple handles
# Edges # 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' }` 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. 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 ```javascript
edgeTypes={{ edgeTypes={{
@@ -272,7 +273,7 @@ There is an implementation of a custom edge in the [edges example](/example/src/
## Background ## 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 ```javascript
import ReactFlow, { Background } from 'react-flow-renderer'; import ReactFlow, { Background } from 'react-flow-renderer';
@@ -299,7 +300,7 @@ const FlowWithBackground = () => (
## MiniMap ## 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 ```javascript
import ReactFlow, { MiniMap } from 'react-flow-renderer'; import ReactFlow, { MiniMap } from 'react-flow-renderer';
@@ -330,7 +331,7 @@ const FlowWithMiniMap = () => (
## Controls ## 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 ```javascript
import ReactFlow, { Controls } from 'react-flow-renderer'; import ReactFlow, { Controls } from 'react-flow-renderer';
@@ -350,6 +351,25 @@ const FlowWithControls = () => (
- `style`: css properties - `style`: css properties
- `className`: additional class name - `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 wrap it with the `ReactFlowProvider` component:
```javascript
import ReactFlow, { ReactFlowProvider } from 'react-flow-renderer';
const FlowWithOwnProvider = () => (
<ReactFlowProvider>
<ReactFlow
elements={elements}
onElementClick={onElementClick}
onConnect={onConnect}
/>
</ReactFlowProvider>
);
```
# Styling # Styling
There are two ways how you can style the graph pane and the elements. 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 ## 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 ```javascript
const FlowWithRedBg = ( const FlowWithRedBg = (
@@ -450,7 +470,7 @@ You can use these function as seen in [this example](/example/src/Overview/index
# Access Internal State # Access Internal State
Under the hood React Flow uses [Easy Peasy](https://easy-peasy.now.sh/) for state handling. 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 ```javascript
import ReactFlow, { useStoreState } from 'react-flow-renderer'; 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 `ReactFlow` component.
# Examples # Examples
You can find all examples in the [example](example) folder or check out the live versions: You can find all examples in the [example](example) folder or check out the live versions:
+31
View File
@@ -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 (
<ReactFlowProvider>
<ReactFlow
elements={elements}
onElementClick={onElementClick}
onConnect={onConnect}
/>
</ReactFlowProvider>
);
}
export default ProviderFlow;
+7 -3
View File
@@ -11,6 +11,7 @@ import Empty from './Empty';
import Edges from './Edges'; import Edges from './Edges';
import Validation from './Validation'; import Validation from './Validation';
import Horizontal from './Horizontal'; import Horizontal from './Horizontal';
import Provider from './Provider';
import './index.css'; import './index.css';
@@ -19,9 +20,9 @@ const routes = [{
component: Overview, component: Overview,
label: 'Overview' label: 'Overview'
}, { }, {
path: '/basic', path: '/provider',
component: Basic, component: Provider,
label: 'Basic' label: 'Provider'
}, { }, {
path: '/edges', path: '/edges',
component: Edges, component: Edges,
@@ -42,6 +43,9 @@ const routes = [{
path: '/stress', path: '/stress',
component: Stress, component: Stress,
label: 'Stress' label: 'Stress'
}, {
path: '/basic',
component: Basic
}, { }, {
path: '/empty', path: '/empty',
component: Empty component: Empty
+1
View File
@@ -45,6 +45,7 @@ export default [
}), }),
replace({ replace({
'process.env.NODE_ENV': JSON.stringify(processEnv), 'process.env.NODE_ENV': JSON.stringify(processEnv),
__REACT_FLOW_VERSION__: JSON.stringify(pkg.version),
}), }),
svgr(), svgr(),
typescript({ typescript({
@@ -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 <StoreProvider store={store}>{children}</StoreProvider>;
};
ReactFlowProvider.displayName = 'ReactFlowProvider';
export default ReactFlowProvider;
+1
View File
@@ -4,3 +4,4 @@
export { default as MiniMap } from './MiniMap'; export { default as MiniMap } from './MiniMap';
export { default as Controls } from './Controls'; export { default as Controls } from './Controls';
export { default as Background } from './Background'; export { default as Background } from './Background';
export { default as ReactFlowProvider } from './ReactFlowProvider';
+41 -10
View File
@@ -2,10 +2,20 @@ import React, { useEffect, useRef, useState, memo, ComponentType, CSSProperties
import { DraggableCore } from 'react-draggable'; import { DraggableCore } from 'react-draggable';
import cx from 'classnames'; import cx from 'classnames';
import { ResizeObserver } from 'resize-observer'; import { ResizeObserver } from 'resize-observer';
import { useStoreActions } from '../../store/hooks';
import { Provider } from '../../contexts/NodeIdContext'; import { Provider } from '../../contexts/NodeIdContext';
import store from '../../store'; import {
import { Node, XYPosition, Transform, ElementId, NodeComponentProps, WrapNodeProps } from '../../types'; Node,
XYPosition,
Transform,
ElementId,
NodeComponentProps,
WrapNodeProps,
Elements,
Edge,
NodePosUpdate,
} from '../../types';
const getMouseEvent = (evt: MouseEvent | TouchEvent) => const getMouseEvent = (evt: MouseEvent | TouchEvent) =>
typeof TouchEvent !== 'undefined' && evt instanceof TouchEvent ? evt.touches[0] : (evt as MouseEvent); typeof TouchEvent !== 'undefined' && evt instanceof TouchEvent ? evt.touches[0] : (evt as MouseEvent);
@@ -19,6 +29,7 @@ interface OnDragStartParams {
setOffset: (pos: XYPosition) => void; setOffset: (pos: XYPosition) => void;
transform: Transform; transform: Transform;
position: XYPosition; position: XYPosition;
setSelectedElements: (elms: Elements | Node | Edge) => void;
onNodeDragStart?: (node: Node) => void; onNodeDragStart?: (node: Node) => void;
} }
@@ -32,6 +43,7 @@ const onStart = ({
setOffset, setOffset,
transform, transform,
position, position,
setSelectedElements,
}: OnDragStartParams): false | void => { }: OnDragStartParams): false | void => {
const startEvt = getMouseEvent(evt); const startEvt = getMouseEvent(evt);
@@ -51,7 +63,7 @@ const onStart = ({
} }
if (selectNodesOnDrag) { if (selectNodesOnDrag) {
store.dispatch.setSelectedElements({ id, type } as Node); setSelectedElements({ id, type } as Node);
} }
}; };
@@ -61,9 +73,10 @@ interface OnDragParams {
id: ElementId; id: ElementId;
offset: XYPosition; offset: XYPosition;
transform: Transform; 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 dragEvt = getMouseEvent(evt);
const scaledClient = { const scaledClient = {
@@ -72,7 +85,7 @@ const onDrag = ({ evt, setDragging, id, offset, transform }: OnDragParams): void
}; };
setDragging(true); setDragging(true);
store.dispatch.updateNodePos({ updateNodePos({
id, id,
pos: { pos: {
x: scaledClient.x - transform[0] - offset.x, x: scaledClient.x - transform[0] - offset.x,
@@ -89,6 +102,7 @@ interface OnDragStopParams {
position: XYPosition; position: XYPosition;
data: any; data: any;
selectNodesOnDrag: boolean; selectNodesOnDrag: boolean;
setSelectedElements: (elms: Elements | Node | Edge) => void;
onNodeDragStop?: (node: Node) => void; onNodeDragStop?: (node: Node) => void;
onClick?: (node: Node) => void; onClick?: (node: Node) => void;
} }
@@ -103,6 +117,7 @@ const onStop = ({
selectNodesOnDrag, selectNodesOnDrag,
onNodeDragStop, onNodeDragStop,
onClick, onClick,
setSelectedElements,
}: OnDragStopParams): void => { }: OnDragStopParams): void => {
const node = { const node = {
id, id,
@@ -113,7 +128,7 @@ const onStop = ({
if (!isDragging) { if (!isDragging) {
if (!selectNodesOnDrag) { if (!selectNodesOnDrag) {
store.dispatch.setSelectedElements({ id, type } as Node); setSelectedElements({ id, type } as Node);
} }
if (onClick) { if (onClick) {
@@ -148,6 +163,10 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
sourcePosition, sourcePosition,
targetPosition, targetPosition,
}: WrapNodeProps) => { }: WrapNodeProps) => {
const updateNodeDimensions = useStoreActions((a) => a.updateNodeDimensions);
const setSelectedElements = useStoreActions((a) => a.setSelectedElements);
const updateNodePos = useStoreActions((a) => a.updateNodePos);
const nodeElement = useRef<HTMLDivElement>(null); const nodeElement = useRef<HTMLDivElement>(null);
const [offset, setOffset] = useState({ x: 0, y: 0 }); const [offset, setOffset] = useState({ x: 0, y: 0 });
const [isDragging, setDragging] = useState(false); const [isDragging, setDragging] = useState(false);
@@ -163,11 +182,11 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
useEffect(() => { useEffect(() => {
if (nodeElement.current) { if (nodeElement.current) {
store.dispatch.updateNodeDimensions({ id, nodeElement: nodeElement.current }); updateNodeDimensions({ id, nodeElement: nodeElement.current });
const resizeObserver = new ResizeObserver((entries) => { const resizeObserver = new ResizeObserver((entries) => {
for (let _ of 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<NodeComponentProps>) => {
setOffset, setOffset,
transform, transform,
position, 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={() =>
onStop({ onNodeDragStop, selectNodesOnDrag, onClick, isDragging, setDragging, id, type, position, data }) onStop({
onNodeDragStop,
selectNodesOnDrag,
onClick,
isDragging,
setDragging,
id,
type,
position,
data,
setSelectedElements,
})
} }
scale={transform[2]} scale={transform[2]}
disabled={!isInteractive} disabled={!isInteractive}
+21
View File
@@ -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<StoreModel>();
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 <StoreProvider store={store}>{children}</StoreProvider>;
};
Wrapper.displayName = 'ReactFlowWrapper';
export default Wrapper;
+3 -4
View File
@@ -1,6 +1,5 @@
import React, { useMemo, CSSProperties, HTMLAttributes } from 'react'; import React, { useMemo, CSSProperties, HTMLAttributes } from 'react';
import cx from 'classnames'; import cx from 'classnames';
import { StoreProvider } from 'easy-peasy';
const nodeEnv: string = process.env.NODE_ENV as string; 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 StraightEdge from '../../components/Edges/StraightEdge';
import StepEdge from '../../components/Edges/StepEdge'; import StepEdge from '../../components/Edges/StepEdge';
import { createEdgeTypes } from '../EdgeRenderer/utils'; import { createEdgeTypes } from '../EdgeRenderer/utils';
import store from '../../store'; import Wrapper from './Wrapper';
import { import {
Elements, Elements,
NodeTypesType, NodeTypesType,
@@ -92,7 +91,7 @@ const ReactFlow = ({
return ( return (
<div style={style} className={cx('react-flow', className)}> <div style={style} className={cx('react-flow', className)}>
<StoreProvider store={store}> <Wrapper>
<GraphView <GraphView
onLoad={onLoad} onLoad={onLoad}
onMove={onMove} onMove={onMove}
@@ -119,7 +118,7 @@ const ReactFlow = ({
/> />
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />} {onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
{children} {children}
</StoreProvider> </Wrapper>
</div> </div>
); );
}; };
+3 -2
View File
@@ -3,8 +3,7 @@ declare module '*.css' {
export default content; export default content;
} }
interface SvgrComponent interface SvgrComponent extends React.StatelessComponent<React.SVGAttributes<SVGElement>> {}
extends React.StatelessComponent<React.SVGAttributes<SVGElement>> {}
declare module '*.svg' { declare module '*.svg' {
const svgUrl: string; const svgUrl: string;
@@ -12,3 +11,5 @@ declare module '*.svg' {
export default svgUrl; export default svgUrl;
export { svgComponent as ReactComponent }; export { svgComponent as ReactComponent };
} }
declare var __REACT_FLOW_VERSION__: string;
+6 -6
View File
@@ -19,6 +19,7 @@ import {
SelectionRect, SelectionRect,
HandleType, HandleType,
SetConnectionId, SetConnectionId,
NodePosUpdate,
} from '../types'; } from '../types';
type TransformXYK = { type TransformXYK = {
@@ -27,11 +28,6 @@ type TransformXYK = {
k: number; k: number;
}; };
type NodePosUpdate = {
id: ElementId;
pos: XYPosition;
};
type NodeDimensionUpdate = { type NodeDimensionUpdate = {
id: ElementId; id: ElementId;
nodeElement: HTMLDivElement; nodeElement: HTMLDivElement;
@@ -87,6 +83,8 @@ export interface StoreModel {
isInteractive: boolean; isInteractive: boolean;
reactFlowVersion: string;
onConnect: OnConnectFunc; onConnect: OnConnectFunc;
setOnConnect: Action<StoreModel, OnConnectFunc>; setOnConnect: Action<StoreModel, OnConnectFunc>;
@@ -126,7 +124,7 @@ export interface StoreModel {
unsetUserSelection: Action<StoreModel>; unsetUserSelection: Action<StoreModel>;
} }
const storeModel: StoreModel = { export const storeModel: StoreModel = {
width: 0, width: 0,
height: 0, height: 0,
transform: [0, 0, 1], transform: [0, 0, 1],
@@ -163,6 +161,8 @@ const storeModel: StoreModel = {
isInteractive: true, isInteractive: true,
reactFlowVersion: __REACT_FLOW_VERSION__,
onConnect: () => {}, onConnect: () => {},
setOnConnect: action((state, onConnect) => { setOnConnect: action((state, onConnect) => {
+5
View File
@@ -203,3 +203,8 @@ export interface EdgeTextProps {
labelShowBg?: boolean; labelShowBg?: boolean;
labelBgStyle?: CSSProperties; labelBgStyle?: CSSProperties;
} }
export type NodePosUpdate = {
id: ElementId;
pos: XYPosition;
};