refactor(background): export background as component closes #235

This commit is contained in:
moklick
2020-05-25 15:53:56 +02:00
parent 10cd70c6ee
commit 7324c79fd0
24 changed files with 229 additions and 227 deletions
+67 -39
View File
@@ -16,10 +16,11 @@ React Flow is a library for building node-based graphs. You can easily implement
- [Edges](#nodes)
- [Options](#options-2)
- [Edge Types / Custom Edges](#edge-types--custom-edges)
- [Helper Functions](#helper-functions)
- [Plugins](#plugins)
- [Components](#plugins)
- [Background](#background)
- [Minimap](#minimap)
- [Controls](#controls)
- [Helper Functions](#helper-functions)
- [Examples](#examples)
- [Development](#development)
- [Testing](#testing)
@@ -30,7 +31,7 @@ React Flow is a library for building node-based graphs. You can easily implement
* **Customizable:** Different [node](#node-types--custom-nodes) and [edge types](#edge-types--custom-edges) and support for custom nodes with multiple handles and edges
* **Fast rendering:** Only nodes that have changed are re-rendered and only those that are in the viewport are displayed
* **Utils:** Snap-to-grid, background styles and graph [helper functions](#helper-functions)
* **Plugin system:** [Mini map and graph controls](#plugins)
* **Components:** [Background, Minimap and graph controls](#components)
* **Reliable**: Written in [Typescript](https://www.typescriptlang.org/) and tested with [cypress](https://www.cypress.io/)
In order to make this library as flexible as possible we dont do any state updates besides the positions. This means that you need to pass the functions to remove an element or connect nodes by yourself. You can implement your own ones or use the helper functions that come with the library.
@@ -242,47 +243,34 @@ Now you could use the new type `special` for an edge.
The `straight`, `default` and `step` types would still be available unless you overwrote one of them.
There is an implementation of a custom edge in the [edges example](/example/src/Edges/index.js).
## Helper Functions
## Components
If you want to remove a node or connect two nodes with each other you need to pass a function to `onElementsRemove` and `onConnect`. In order to simplify this process we export some helper functions so that you don't need to implement them:
### Background
React Flow comes with two background variants: **dots** and **lines**.
```javascript
import ReactFlow, { isNode, isEdge, removeElements, addEdge } from 'react-flow-renderer';
import ReactFlow, { Background } from 'react-flow-renderer';
const FlowWithBackground = () => (
<ReactFlow elements={elements}>
<Background
variant="dots"
gap={12}
size={4}
/>
</ReactFlow>
);
```
#### isEdge
#### Prop Types
Returns true if element is an edge
`isEdge = (element: Node | Edge): element is Edge`
#### isNode
Returns true if element is a node
`isNode = (element: Node | Edge): element is Node`
#### removeElements
Returns elements without the elements from `elementsToRemove`
`removeElements = (elementsToRemove: Elements, elements: Elements): Elements`
#### addEdge
Returns elements array with added edge
`addEdge = (edgeParams: Edge, elements: Elements): Elements`
#### project
Transforms pixel coordinates to the internal React Flow coordinate system
`project = (position: XYPosition): XYPosition`
You can use these function as seen in [this example](/example/src/Overview/index.js#L40-L41) or use your own ones.
## Plugins
- `variant`: string - has to be 'dots' or 'lines' - default: `dots`
- `gap`: number - the gap between the dots or lines - default: `16`
- `size`: number - the radius of the dots or the stroke width of the lines - default: 0.5
- `color`: string - the color of the dots or lines - default: `#999`
- `style`: css properties
- `className`: class name
### MiniMap
@@ -291,7 +279,7 @@ You can use the mini map plugin by passing it as a children to the React Flow co
```javascript
import ReactFlow, { MiniMap } from 'react-flow-renderer';
const GraphWithMiniMap = () => (
const FlowWithMiniMap = () => (
<ReactFlow elements={elements}>
<MiniMap
nodeColor={(node) => {
@@ -337,6 +325,46 @@ const FlowWithControls = () => (
- `showFitView`: boolean - default: true
- `showInteractive`: boolean - default: true
## Helper Functions
If you want to remove a node or connect two nodes with each other you need to pass a function to `onElementsRemove` and `onConnect`. In order to simplify this process we export some helper functions so that you don't need to implement them:
```javascript
import ReactFlow, { isNode, isEdge, removeElements, addEdge } from 'react-flow-renderer';
```
#### isEdge
Returns true if element is an edge
`isEdge = (element: Node | Edge): element is Edge`
#### isNode
Returns true if element is a node
`isNode = (element: Node | Edge): element is Node`
#### removeElements
Returns elements without the elements from `elementsToRemove`
`removeElements = (elementsToRemove: Elements, elements: Elements): Elements`
#### addEdge
Returns elements array with added edge
`addEdge = (edgeParams: Edge, elements: Elements): Elements`
#### project
Transforms pixel coordinates to the internal React Flow coordinate system
`project = (position: XYPosition): XYPosition`
You can use these function as seen in [this example](/example/src/Overview/index.js#L40-L41) or use your own ones.
## Examples
You can find all examples in the [example](example) folder or check out the live versions:
+2 -2
View File
@@ -10,9 +10,9 @@ describe('Basic Graph Rendering', () => {
});
it('renders a grid', () => {
cy.get('.react-flow__grid');
cy.get('.react-flow__background');
const gridStroke = Cypress.$('.react-flow__grid path').attr('stroke');
const gridStroke = Cypress.$('.react-flow__background path').attr('stroke');
expect(gridStroke).to.equal('#eee');
});
@@ -1,4 +1,4 @@
describe('Rich Graph Rendering', () => {
describe('Overview Graph Rendering', () => {
it('renders a graph', () => {
cy.visit('/');
@@ -9,9 +9,9 @@ describe('Rich Graph Rendering', () => {
});
it('renders a grid', () => {
cy.get('.react-flow__grid');
cy.get('.react-flow__background');
const gridStroke = Cypress.$('.react-flow__grid path').attr('fill');
const gridStroke = Cypress.$('.react-flow__background path').attr('fill');
expect(gridStroke).to.equal('#888');
});
});
+3 -3
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import ReactFlow, { removeElements, addEdge, isNode } from 'react-flow-renderer';
import ReactFlow, { removeElements, addEdge, isNode, Background } from 'react-flow-renderer';
const onNodeDragStop = node => console.log('drag stop', node);
const onLoad = graphInstance => console.log('graph loaded:', graphInstance);
@@ -47,10 +47,10 @@ const BasicFlow = () => {
onElementsRemove={onElementsRemove}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
style={{ width: '100%', height: '100%' }}
backgroundType="lines"
className="react-flow-basic-example"
>
<Background variant="lines" />
<button
onClick={updatePos}
style={{ position: 'absolute', right: 10, top: 30, zIndex: 4 }}
-2
View File
@@ -64,8 +64,6 @@ const CustomNodeFlow = () => {
}}
connectionLineStyle={{ stroke: '#ddd', strokeWidth: 2 }}
connectionLineType="bezier"
backgroundColor="#888"
backgroundGap={16}
snapToGrid={true}
snapGrid={[16, 16]}
>
+2 -1
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import ReactFlow, { removeElements, addEdge, MiniMap, Controls } from 'react-flow-renderer';
import ReactFlow, { removeElements, addEdge, MiniMap, Controls, Background } from 'react-flow-renderer';
import CustomEdge from './CustomEdge';
@@ -50,6 +50,7 @@ const EdgesFlow = () => {
>
<MiniMap />
<Controls />
<Background />
</ReactFlow>
);
}
+12 -12
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import ReactFlow, { removeElements, addEdge, MiniMap, Controls } from 'react-flow-renderer';
import ReactFlow, { removeElements, addEdge, MiniMap, Controls, Background } from 'react-flow-renderer';
const onNodeDragStop = node => console.log('drag stop', node);
const onLoad = graphInstance => console.log('graph loaded:', graphInstance);
@@ -29,18 +29,18 @@ const EmptyFlow = () => {
onElementsRemove={onElementsRemove}
onConnect={p => onConnect(p)}
onNodeDragStop={onNodeDragStop}
style={{ width: '100%', height: '100%' }}
backgroundType="lines"
>
<MiniMap />
<Controls />
<button
type="button"
onClick={addRandomNode}
style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}
>
add node
</button>
<MiniMap />
<Controls />
<Background variant="lines" />
<button
type="button"
onClick={addRandomNode}
style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}
>
add node
</button>
</ReactFlow>
);
}
+1 -2
View File
@@ -20,12 +20,11 @@ const InactiveFlow = () => {
return (
<ReactFlow
elements={initialElements}
style={{ width: '100%', height: '100%' }}
backgroundType="lines"
isInteractive={isInteractive}
>
<MiniMap />
<Controls />
<div
style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}
>
+7 -4
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import ReactFlow, { removeElements, addEdge, MiniMap, Controls } from 'react-flow-renderer';
import ReactFlow, { removeElements, addEdge, MiniMap, Controls, Background } from 'react-flow-renderer';
const onNodeDragStart = node => console.log('drag start', node);
const onNodeDragStop = node => console.log('drag stop', node);
@@ -59,8 +59,6 @@ const OverviewFlow = () => {
onLoad={onLoad}
connectionLineStyle={{ stroke: '#ddd', strokeWidth: 2 }}
connectionLineType="bezier"
backgroundColor="#888"
backgroundGap={16}
snapToGrid={true}
snapGrid={[16, 16]}
>
@@ -74,11 +72,16 @@ const OverviewFlow = () => {
}}
/>
<Controls />
<Background
color="#888"
gap={16}
/>
<button
type="button"
onClick={addRandomNode}
style={{ position: 'absolute', right: 10, top: 30, zIndex: 4 }}
className="richexample__add"
className="overview-example__add"
>
add node
</button>
+5 -5
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import ReactFlow, { removeElements, addEdge, MiniMap, isNode, Controls } from 'react-flow-renderer';
import ReactFlow, { removeElements, addEdge, MiniMap, isNode, Controls, Background } from 'react-flow-renderer';
import { getElements } from './utils';
const onLoad = graph => {
@@ -40,17 +40,17 @@ const StressGraph = () => {
onLoad={onLoad}
onElementsRemove={onElementsRemove}
onConnect={onConnect}
style={{ width: '100%', height: '100%' }}
backgroundType="lines"
>
<MiniMap />
<Controls />
<Background />
<button
onClick={updatePos}
style={{ position: 'absolute', right: 10, top: 30, zIndex: 4 }}
>
change pos
</button>
<MiniMap />
<Controls />
</ReactFlow>
);
}
+24 -12
View File
@@ -5,11 +5,14 @@ body {
color: #111;
}
html, body {
html,
body {
margin: 0;
}
html, body, #root {
html,
body,
#root {
height: 100%;
}
@@ -32,7 +35,10 @@ header {
line-height: 1;
}
header a, header a:focus, header a:active, header a:visited {
header a,
header a:focus,
header a:active,
header a:visited {
color: #111;
}
@@ -58,7 +64,7 @@ nav {
z-index: 10;
left: 0;
padding: 3%;
background: rgba(255,255,255,0.9);
background: rgba(255, 255, 255, 0.9);
}
nav.is-open {
@@ -77,7 +83,10 @@ nav a {
margin-bottom: 5px;
}
nav a, nav a:focus, nav a:active, nav a:visited {
nav a,
nav a:focus,
nav a:active,
nav a:visited {
color: #fff;
}
@@ -93,8 +102,8 @@ nav a.active:before {
height: 6px;
background: white;
opacity: 0.5;
left:0;
top:50%;
left: 0;
top: 50%;
transform: translateY(-50%);
}
@@ -107,7 +116,10 @@ nav a.active:before {
font-size: 12px;
}
.sourcedisplay, .sourcedisplay:focus, .sourcedisplay:active, .sourcedisplay:visited {
.sourcedisplay,
.sourcedisplay:focus,
.sourcedisplay:active,
.sourcedisplay:visited {
color: #777;
}
@@ -115,12 +127,12 @@ nav a.active:before {
color: #111;
}
.richexample__add {
.overview-example__add {
display: none;
}
.react-flow__node a {
font-weight :700;
font-weight: 700;
color: #111;
}
@@ -146,7 +158,7 @@ nav a.active:before {
display: none;
}
.richexample__add {
.overview-example__add {
display: block;
}
}
}
@@ -0,0 +1,56 @@
import React, { memo, HTMLAttributes, CSSProperties } from 'react';
import classnames from 'classnames';
import { useStoreState } from '../../store/hooks';
import { BackgroundVariant } from '../../types';
import { createGridLinesPath, createGridDotsPath } from './utils';
interface BackgroundProps extends HTMLAttributes<SVGElement> {
variant?: BackgroundVariant;
gap?: number;
color?: string;
size?: number;
}
const baseStyles: CSSProperties = {
position: 'absolute',
top: 0,
left: 0,
};
const defaultColors = {
[BackgroundVariant.Dots]: '#999',
[BackgroundVariant.Lines]: '#eee',
};
const Background = memo(
({ variant = BackgroundVariant.Dots, gap = 24, size = 0.5, color, style = {}, className = '' }: BackgroundProps) => {
const {
width,
height,
transform: [x, y, scale],
} = useStoreState((s) => s);
const bgClasses = classnames('react-flow__background', className);
const bgColor = color ? color : defaultColors[variant];
const scaledGap = gap * scale;
const xOffset = x % scaledGap;
const yOffset = y % scaledGap;
const isLines = variant === BackgroundVariant.Lines;
const path = isLines
? createGridLinesPath(width, height, xOffset, yOffset, scaledGap)
: createGridDotsPath(width, height, xOffset, yOffset, scaledGap, size);
const fill = isLines ? 'none' : bgColor;
const stroke = isLines ? bgColor : 'none';
return (
<svg width={width} height={height} style={{ ...baseStyles, ...style }} className={bgClasses}>
<path fill={fill} stroke={stroke} strokeWidth={size} d={path} />
</svg>
);
}
);
Background.displayName = 'Background';
export default Background;
@@ -0,0 +1,37 @@
export const createGridLinesPath = (
width: number,
height: number,
xOffset: number,
yOffset: number,
gap: number
): string => {
const lineCountX = Math.ceil(width / gap) + 1;
const lineCountY = Math.ceil(height / gap) + 1;
const xValues = Array.from({ length: lineCountX }, (_, i) => `M${i * gap + xOffset} 0 V${height}`);
const yValues = Array.from({ length: lineCountY }, (_, i) => `M0 ${i * gap + yOffset} H${width}`);
return [...xValues, ...yValues].join(' ');
};
export const createGridDotsPath = (
width: number,
height: number,
xOffset: number,
yOffset: number,
gap: number,
size: number
): string => {
const lineCountX = Math.ceil(width / gap) + 1;
const lineCountY = Math.ceil(height / gap) + 1;
const values = Array.from({ length: lineCountX }, (_, col) => {
const x = col * gap + xOffset;
return Array.from({ length: lineCountY }, (_, row) => {
const y = row * gap + yOffset;
return `M${x} ${y - size} l${size} ${size} l${-size} ${size} l${-size} ${-size}z`;
}).join(' ');
});
return values.join(' ');
};
+6
View File
@@ -0,0 +1,6 @@
// These components are not used by React Flow directly
// but the user can add them as a child of a React Flow component
export { default as MiniMap } from './MiniMap';
export { default as Controls } from './Controls';
export { default as Background } from './Background';
-108
View File
@@ -1,108 +0,0 @@
import React, { memo, HTMLAttributes, CSSProperties } from 'react';
import classnames from 'classnames';
import { useStoreState } from '../../store/hooks';
import { GridType } from '../../types';
interface GridProps extends HTMLAttributes<SVGElement> {
backgroundType?: GridType;
gap?: number;
color?: string;
size?: number;
}
const baseStyles: CSSProperties = {
position: 'absolute',
top: 0,
left: 0,
};
const createGridLines = (
width: number,
height: number,
xOffset: number,
yOffset: number,
gap: number
): string => {
const lineCountX = Math.ceil(width / gap) + 1;
const lineCountY = Math.ceil(height / gap) + 1;
const xValues = Array.from(
{ length: lineCountX },
(_, i) => `M${i * gap + xOffset} 0 V${height}`
);
const yValues = Array.from(
{ length: lineCountY },
(_, i) => `M0 ${i * gap + yOffset} H${width}`
);
return [...xValues, ...yValues].join(' ');
};
const createGridDots = (
width: number,
height: number,
xOffset: number,
yOffset: number,
gap: number,
size: number
): string => {
const lineCountX = Math.ceil(width / gap) + 1;
const lineCountY = Math.ceil(height / gap) + 1;
const values = Array.from({ length: lineCountX }, (_, col) => {
const x = col * gap + xOffset;
return Array.from({ length: lineCountY }, (_, row) => {
const y = row * gap + yOffset;
return `M${x} ${y -
size} l${size} ${size} l${-size} ${size} l${-size} ${-size}z`;
}).join(' ');
});
return values.join(' ');
};
const Grid = memo(
({
gap = 24,
color = '#aaa',
size = 0.5,
style = {},
className = '',
backgroundType = GridType.Dots,
}: GridProps) => {
const {
width,
height,
transform: [x, y, scale],
} = useStoreState(s => s);
const gridClasses = classnames('react-flow__grid', className);
const scaledGap = gap * scale;
const xOffset = x % scaledGap;
const yOffset = y % scaledGap;
const isLines = backgroundType === 'lines';
const path = isLines
? createGridLines(width, height, xOffset, yOffset, scaledGap)
: createGridDots(width, height, xOffset, yOffset, scaledGap, size);
const fill = isLines ? 'none' : color;
const stroke = isLines ? color : 'none';
return (
<svg
width={width}
height={height}
style={{ ...baseStyles, ...style }}
className={gridClasses}
>
<path fill={fill} stroke={stroke} strokeWidth={size} d={path} />
</svg>
);
}
);
Grid.displayName = 'Grid';
export default Grid;
+1 -13
View File
@@ -6,14 +6,13 @@ import NodeRenderer from '../NodeRenderer';
import EdgeRenderer from '../EdgeRenderer';
import UserSelection from '../../components/UserSelection';
import NodesSelection from '../../components/NodesSelection';
import BackgroundGrid from '../../components/BackgroundGrid';
import useKeyPress from '../../hooks/useKeyPress';
import useD3Zoom from '../../hooks/useD3Zoom';
import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler';
import useElementUpdater from '../../hooks/useElementUpdater';
import { getDimensions } from '../../utils';
import { fitView, zoomIn, zoomOut, project } from '../../utils/graph';
import { Elements, NodeTypesType, EdgeTypesType, GridType, OnLoadFunc, Node, Edge, Connection } from '../../types';
import { Elements, NodeTypesType, EdgeTypesType, OnLoadFunc, Node, Edge, Connection } from '../../types';
export interface GraphViewProps {
elements: Elements;
@@ -30,10 +29,6 @@ export interface GraphViewProps {
connectionLineType: string;
connectionLineStyle: CSSProperties;
deleteKeyCode: number;
showBackground: boolean;
backgroundGap: number;
backgroundColor: string;
backgroundType: GridType;
snapToGrid: boolean;
snapGrid: [number, number];
onlyRenderVisibleNodes: boolean;
@@ -55,10 +50,6 @@ const GraphView = memo(
onElementsRemove,
deleteKeyCode,
elements,
showBackground,
backgroundGap,
backgroundColor,
backgroundType,
onConnect,
snapToGrid,
snapGrid,
@@ -135,9 +126,6 @@ const GraphView = memo(
return (
<div className={rendererClasses} ref={rendererNode}>
{showBackground && (
<BackgroundGrid gap={backgroundGap} color={backgroundColor} backgroundType={backgroundType} />
)}
<NodeRenderer
nodeTypes={nodeTypes}
onElementClick={onElementClick}
+1 -17
View File
@@ -20,7 +20,7 @@ import StraightEdge from '../../components/Edges/StraightEdge';
import StepEdge from '../../components/Edges/StepEdge';
import { createEdgeTypes } from '../EdgeRenderer/utils';
import store from '../../store';
import { Elements, NodeTypesType, EdgeTypesType, GridType, OnLoadFunc, Node, Edge, Connection } from '../../types';
import { Elements, NodeTypesType, EdgeTypesType, OnLoadFunc, Node, Edge, Connection } from '../../types';
import '../../style.css';
@@ -40,10 +40,6 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
connectionLineStyle: CSSProperties;
deleteKeyCode: number;
selectionKeyCode: number;
showBackground: boolean;
backgroundGap: number;
backgroundColor: string;
backgroundType: GridType;
snapToGrid: boolean;
snapGrid: [number, number];
onlyRenderVisibleNodes: boolean;
@@ -69,10 +65,6 @@ const ReactFlow = ({
connectionLineStyle,
deleteKeyCode,
selectionKeyCode,
showBackground,
backgroundGap,
backgroundType,
backgroundColor,
snapToGrid,
snapGrid,
onlyRenderVisibleNodes,
@@ -99,10 +91,6 @@ const ReactFlow = ({
deleteKeyCode={deleteKeyCode}
elements={elements}
onConnect={onConnect}
backgroundColor={backgroundColor}
backgroundGap={backgroundGap}
showBackground={showBackground}
backgroundType={backgroundType}
snapToGrid={snapToGrid}
snapGrid={snapGrid}
onlyRenderVisibleNodes={onlyRenderVisibleNodes}
@@ -139,10 +127,6 @@ ReactFlow.defaultProps = {
connectionLineStyle: {},
deleteKeyCode: 8,
selectionKeyCode: 16,
backgroundColor: '#eee',
backgroundGap: 24,
showBackground: true,
backgroundType: GridType.Dots,
snapToGrid: false,
snapGrid: [16, 16],
onlyRenderVisibleNodes: true,
+1 -1
View File
@@ -4,8 +4,8 @@ export default ReactFlow;
export { default as Handle } from './components/Handle';
export { default as EdgeText } from './components/Edges/EdgeText';
export { MiniMap, Controls } from './plugins';
export { isNode, isEdge, removeElements, addEdge, getOutgoers } from './utils/graph';
export * from './additional-components';
export * from './types';
-2
View File
@@ -1,2 +0,0 @@
export { default as MiniMap } from './MiniMap';
export { default as Controls } from './Controls';
+1 -1
View File
@@ -54,7 +54,7 @@ export interface Edge {
animated?: boolean;
}
export enum GridType {
export enum BackgroundVariant {
Lines = 'lines',
Dots = 'dots',
}