From 7324c79fd043fcdd1879330931326ee09037b4fe Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 25 May 2020 15:53:56 +0200 Subject: [PATCH 1/8] refactor(background): export background as component closes #235 --- README.md | 106 ++++++++++------- cypress/integration/flow/basic.spec.js | 4 +- .../flow/{rich.spec.js => overview.spec.js} | 6 +- example/src/Basic/index.js | 6 +- example/src/CustomNode/index.js | 2 - example/src/Edges/index.js | 3 +- example/src/Empty/index.js | 24 ++-- example/src/Inactive/index.js | 3 +- example/src/Overview/index.js | 11 +- example/src/Stress/index.js | 10 +- example/src/index.css | 36 ++++-- .../Background/index.tsx | 56 +++++++++ src/additional-components/Background/utils.ts | 37 ++++++ .../Controls/index.tsx | 0 .../MiniMap/MiniMapNode.tsx | 0 .../MiniMap/index.tsx | 0 src/additional-components/index.ts | 6 + src/components/BackgroundGrid/index.tsx | 108 ------------------ .../SelectionListener/{index.ts => index.tsx} | 0 src/container/GraphView/index.tsx | 14 +-- src/container/ReactFlow/index.tsx | 18 +-- src/index.ts | 2 +- src/plugins/index.ts | 2 - src/types/index.ts | 2 +- 24 files changed, 229 insertions(+), 227 deletions(-) rename cypress/integration/flow/{rich.spec.js => overview.spec.js} (64%) create mode 100644 src/additional-components/Background/index.tsx create mode 100644 src/additional-components/Background/utils.ts rename src/{plugins => additional-components}/Controls/index.tsx (100%) rename src/{plugins => additional-components}/MiniMap/MiniMapNode.tsx (100%) rename src/{plugins => additional-components}/MiniMap/index.tsx (100%) create mode 100644 src/additional-components/index.ts delete mode 100644 src/components/BackgroundGrid/index.tsx rename src/components/SelectionListener/{index.ts => index.tsx} (100%) delete mode 100644 src/plugins/index.ts diff --git a/README.md b/README.md index a3c66eb7..bd73aabe 100644 --- a/README.md +++ b/README.md @@ -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 don’t 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 = () => ( + + + +); ``` -#### 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 = () => ( { @@ -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: diff --git a/cypress/integration/flow/basic.spec.js b/cypress/integration/flow/basic.spec.js index 46e2aed7..dccabd28 100644 --- a/cypress/integration/flow/basic.spec.js +++ b/cypress/integration/flow/basic.spec.js @@ -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'); }); diff --git a/cypress/integration/flow/rich.spec.js b/cypress/integration/flow/overview.spec.js similarity index 64% rename from cypress/integration/flow/rich.spec.js rename to cypress/integration/flow/overview.spec.js index d58c3c4c..e072b834 100644 --- a/cypress/integration/flow/rich.spec.js +++ b/cypress/integration/flow/overview.spec.js @@ -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'); }); }); diff --git a/example/src/Basic/index.js b/example/src/Basic/index.js index 002ef268..56fb9029 100644 --- a/example/src/Basic/index.js +++ b/example/src/Basic/index.js @@ -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" > + + + + + + + ); } diff --git a/example/src/Inactive/index.js b/example/src/Inactive/index.js index 3f6490b6..45980b3f 100644 --- a/example/src/Inactive/index.js +++ b/example/src/Inactive/index.js @@ -20,12 +20,11 @@ const InactiveFlow = () => { return ( +
diff --git a/example/src/Overview/index.js b/example/src/Overview/index.js index 7df9ed28..a36b5122 100644 --- a/example/src/Overview/index.js +++ b/example/src/Overview/index.js @@ -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 = () => { }} /> + + diff --git a/example/src/Stress/index.js b/example/src/Stress/index.js index 74f344ed..d5382de7 100644 --- a/example/src/Stress/index.js +++ b/example/src/Stress/index.js @@ -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" > + + + + - - ); } diff --git a/example/src/index.css b/example/src/index.css index ffe80e6c..87da1ef6 100644 --- a/example/src/index.css +++ b/example/src/index.css @@ -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; } -} +} \ No newline at end of file diff --git a/src/additional-components/Background/index.tsx b/src/additional-components/Background/index.tsx new file mode 100644 index 00000000..93262819 --- /dev/null +++ b/src/additional-components/Background/index.tsx @@ -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 { + 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 ( + + + + ); + } +); + +Background.displayName = 'Background'; + +export default Background; diff --git a/src/additional-components/Background/utils.ts b/src/additional-components/Background/utils.ts new file mode 100644 index 00000000..cf938870 --- /dev/null +++ b/src/additional-components/Background/utils.ts @@ -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(' '); +}; diff --git a/src/plugins/Controls/index.tsx b/src/additional-components/Controls/index.tsx similarity index 100% rename from src/plugins/Controls/index.tsx rename to src/additional-components/Controls/index.tsx diff --git a/src/plugins/MiniMap/MiniMapNode.tsx b/src/additional-components/MiniMap/MiniMapNode.tsx similarity index 100% rename from src/plugins/MiniMap/MiniMapNode.tsx rename to src/additional-components/MiniMap/MiniMapNode.tsx diff --git a/src/plugins/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx similarity index 100% rename from src/plugins/MiniMap/index.tsx rename to src/additional-components/MiniMap/index.tsx diff --git a/src/additional-components/index.ts b/src/additional-components/index.ts new file mode 100644 index 00000000..3322bfcf --- /dev/null +++ b/src/additional-components/index.ts @@ -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'; diff --git a/src/components/BackgroundGrid/index.tsx b/src/components/BackgroundGrid/index.tsx deleted file mode 100644 index 6b670363..00000000 --- a/src/components/BackgroundGrid/index.tsx +++ /dev/null @@ -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 { - 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 ( - - - - ); - } -); - -Grid.displayName = 'Grid'; - -export default Grid; diff --git a/src/components/SelectionListener/index.ts b/src/components/SelectionListener/index.tsx similarity index 100% rename from src/components/SelectionListener/index.ts rename to src/components/SelectionListener/index.tsx diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index cb22c7f1..c9c42de4 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -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 (
- {showBackground && ( - - )} , '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, diff --git a/src/index.ts b/src/index.ts index 782d7a8e..6c2f0444 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/plugins/index.ts b/src/plugins/index.ts deleted file mode 100644 index bd8ac3db..00000000 --- a/src/plugins/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as MiniMap } from './MiniMap'; -export { default as Controls } from './Controls'; diff --git a/src/types/index.ts b/src/types/index.ts index 4773d6fa..20868fcf 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -54,7 +54,7 @@ export interface Edge { animated?: boolean; } -export enum GridType { +export enum BackgroundVariant { Lines = 'lines', Dots = 'dots', } From 17614bda6b5df1cad8cabd048431f5031955729f Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 25 May 2020 16:24:50 +0200 Subject: [PATCH 2/8] refactor(callbacks): dont provide noops as defaults, but make cbs optional --- src/components/Edges/wrapEdge.tsx | 7 +- src/components/Nodes/wrapNode.tsx | 98 ++++++++++++++++++---------- src/container/GraphView/index.tsx | 25 +++---- src/container/NodeRenderer/index.tsx | 8 +-- src/container/ReactFlow/index.tsx | 23 +++---- src/hooks/useD3Zoom.ts | 22 +++++-- src/hooks/useGlobalKeyHandler.ts | 4 +- src/types/index.ts | 9 +-- 8 files changed, 118 insertions(+), 78 deletions(-) diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx index cb845358..bf27894f 100644 --- a/src/components/Edges/wrapEdge.tsx +++ b/src/components/Edges/wrapEdge.tsx @@ -13,7 +13,7 @@ interface EdgeWrapperProps { labelStyle?: CSSProperties; labelShowBg?: boolean; labelBgStyle: CSSProperties; - onClick: (edge: Edge) => void; + onClick?: (edge: Edge) => void; animated: boolean; selected: boolean; isInteractive: boolean; @@ -46,7 +46,10 @@ export default (EdgeComponent: ComponentType) => { } store.dispatch.setSelectedElements({ id, source, target }); - onClick({ id, source, target, type }); + + if (onClick) { + onClick({ id, source, target, type }); + } }; return ( diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 0c8b1376..7d310ff9 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -10,16 +10,27 @@ import { Node, XYPosition, Transform, ElementId, NodeComponentProps, WrapNodePro const getMouseEvent = (evt: MouseEvent | TouchEvent) => typeof TouchEvent !== 'undefined' && evt instanceof TouchEvent ? evt.touches[0] : (evt as MouseEvent); -const onStart = ( - evt: MouseEvent | TouchEvent, - onDragStart: (node: Node) => void, - id: ElementId, - type: string, - data: any, - setOffset: (pos: XYPosition) => void, - transform: Transform, - position: XYPosition -): false | void => { +interface OnDragStartParams { + evt: MouseEvent | TouchEvent; + id: ElementId; + type: string; + data: any; + setOffset: (pos: XYPosition) => void; + transform: Transform; + position: XYPosition; + onNodeDragStart?: (node: Node) => void; +} + +const onStart = ({ + evt, + onNodeDragStart, + id, + type, + data, + setOffset, + transform, + position, +}: OnDragStartParams): false | void => { const startEvt = getMouseEvent(evt); const scaledClient: XYPosition = { @@ -33,16 +44,21 @@ const onStart = ( store.dispatch.setSelectedElements({ id, type } as Node); setOffset({ x: offsetX, y: offsetY }); - onDragStart(node); + + if (onNodeDragStart) { + onNodeDragStart(node); + } }; -const onDrag = ( - evt: MouseEvent | TouchEvent, - setDragging: (isDragging: boolean) => void, - id: ElementId, - offset: XYPosition, - transform: Transform -): void => { +interface OnDragParams { + evt: MouseEvent | TouchEvent; + setDragging: (isDragging: boolean) => void; + id: ElementId; + offset: XYPosition; + transform: Transform; +} + +const onDrag = ({ evt, setDragging, id, offset, transform }: OnDragParams): void => { const dragEvt = getMouseEvent(evt); const scaledClient = { @@ -60,16 +76,27 @@ const onDrag = ( }); }; -const onStop = ( - onDragStop: (node: Node) => void, - onClick: (node: Node) => void, - isDragging: boolean, - setDragging: (isDragging: boolean) => void, - id: ElementId, - type: string, - position: XYPosition, - data: any -): void => { +interface OnDragStopParams { + isDragging: boolean; + setDragging: (isDragging: boolean) => void; + id: ElementId; + type: string; + position: XYPosition; + data: any; + onNodeDragStop?: (node: Node) => void; + onClick?: (node: Node) => void; +} + +const onStop = ({ + onNodeDragStop, + onClick, + isDragging, + setDragging, + id, + type, + position, + data, +}: OnDragStopParams): void => { const node = { id, type, @@ -77,12 +104,15 @@ const onStop = ( data, } as Node; - if (!isDragging) { + if (!isDragging && onClick) { return onClick(node); } setDragging(false); - onDragStop(node); + + if (onNodeDragStop) { + onNodeDragStop(node); + } }; export default (NodeComponent: ComponentType) => { @@ -146,9 +176,11 @@ export default (NodeComponent: ComponentType) => { return ( onStart(evt as MouseEvent, onNodeDragStart, id, type, data, setOffset, transform, position)} - onDrag={(evt) => onDrag(evt as MouseEvent, setDragging, id, offset, transform)} - onStop={() => onStop(onNodeDragStop, onClick, isDragging, setDragging, id, type, position, data)} + onStart={(evt) => + onStart({ evt: evt as MouseEvent, onNodeDragStart, id, type, data, setOffset, transform, position }) + } + onDrag={(evt) => onDrag({ evt: evt as MouseEvent, setDragging, id, offset, transform })} + onStop={() => onStop({ onNodeDragStop, onClick, isDragging, setDragging, id, type, position, data })} scale={transform[2]} disabled={!isInteractive} cancel=".nodrag" diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index c9c42de4..d9ae264f 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -16,13 +16,13 @@ import { Elements, NodeTypesType, EdgeTypesType, OnLoadFunc, Node, Edge, Connect export interface GraphViewProps { elements: Elements; - onElementClick: (element: Node | Edge) => void; - onElementsRemove: (elements: Elements) => void; - onNodeDragStart: (node: Node) => void; - onNodeDragStop: (node: Node) => void; - onConnect: (connection: Connection | Edge) => void; - onLoad: OnLoadFunc; - onMove: () => void; + onElementClick?: (element: Node | Edge) => void; + onElementsRemove?: (elements: Elements) => void; + onNodeDragStart?: (node: Node) => void; + onNodeDragStop?: (node: Node) => void; + onConnect?: (connection: Connection | Edge) => void; + onLoad?: OnLoadFunc; + onMove?: () => void; selectionKeyCode: number; nodeTypes: NodeTypesType; edgeTypes: EdgeTypesType; @@ -92,18 +92,21 @@ const GraphView = memo( useEffect(() => { updateDimensions(); - setOnConnect(onConnect); window.onresize = updateDimensions; + if (onConnect) { + setOnConnect(onConnect); + } + return () => { window.onresize = null; }; }, []); - useD3Zoom(zoomPane, onMove, selectionKeyPressed); + useD3Zoom({ zoomPane, onMove, selectionKeyPressed }); useEffect(() => { - if (state.d3Initialised) { + if (state.d3Initialised && onLoad) { onLoad({ fitView, zoomIn, @@ -111,7 +114,7 @@ const GraphView = memo( project, }); } - }, [state.d3Initialised]); + }, [state.d3Initialised, onLoad]); useEffect(() => { setSnapGrid({ snapToGrid, snapGrid }); diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index db410d66..05f1e32c 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -6,9 +6,9 @@ import { Node, Transform, NodeTypesType, WrapNodeProps, Elements, Edge } from '. interface NodeRendererProps { nodeTypes: NodeTypesType; - onElementClick: (element: Node | Edge) => void; - onNodeDragStart: (node: Node) => void; - onNodeDragStop: (node: Node) => void; + onElementClick?: (element: Node | Edge) => void; + onNodeDragStart?: (node: Node) => void; + onNodeDragStop?: (node: Node) => void; onlyRenderVisibleNodes?: boolean; } @@ -22,7 +22,7 @@ function renderNode( const nodeType = node.type || 'default'; const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; if (!props.nodeTypes[nodeType]) { - console.warn(`No node type found for type "${nodeType}". Using fallback type "default".`); + console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); } const isSelected = selectedElements ? selectedElements.some(({ id }) => id === node.id) : false; diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index 32d4c749..5d46899a 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -26,14 +26,14 @@ import '../../style.css'; export interface ReactFlowProps extends Omit, 'onLoad'> { elements: Elements; - onElementClick: (element: Node | Edge) => void; - onElementsRemove: (elements: Elements) => void; - onNodeDragStart: (node: Node) => void; - onNodeDragStop: (node: Node) => void; - onConnect: (connection: Edge | Connection) => void; - onLoad: OnLoadFunc; - onMove: () => void; - onSelectionChange: (elements: Elements | null) => void; + onElementClick?: (element: Node | Edge) => void; + onElementsRemove?: (elements: Elements) => void; + onNodeDragStart?: (node: Node) => void; + onNodeDragStop?: (node: Node) => void; + onConnect?: (connection: Edge | Connection) => void; + onLoad?: OnLoadFunc; + onMove?: () => void; + onSelectionChange?: (elements: Elements | null) => void; nodeTypes: NodeTypesType; edgeTypes: EdgeTypesType; connectionLineType: string; @@ -106,13 +106,6 @@ const ReactFlow = ({ ReactFlow.displayName = 'ReactFlow'; ReactFlow.defaultProps = { - onElementClick: () => {}, - onElementsRemove: () => {}, - onNodeDragStart: () => {}, - onNodeDragStop: () => {}, - onConnect: () => {}, - onLoad: () => {}, - onMove: () => {}, nodeTypes: { input: InputNode, default: DefaultNode, diff --git a/src/hooks/useD3Zoom.ts b/src/hooks/useD3Zoom.ts index cc9726c8..fc0527e3 100644 --- a/src/hooks/useD3Zoom.ts +++ b/src/hooks/useD3Zoom.ts @@ -4,20 +4,26 @@ import { select, event } from 'd3-selection'; import { useStoreState, useStoreActions } from '../store/hooks'; +interface UseD3ZoomParams { + zoomPane: MutableRefObject; + selectionKeyPressed: boolean; + onMove?: () => void; +} + const d3ZoomInstance = d3Zoom .zoom() .scaleExtent([0.5, 2]) .filter(() => !event.button); -export default (zoomPane: MutableRefObject, onMove: () => void, shiftPressed: boolean): void => { - const state = useStoreState(s => ({ +export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): void => { + const state = useStoreState((s) => ({ transform: s.transform, d3Selection: s.d3Selection, d3Zoom: s.d3Zoom, })); - const initD3 = useStoreActions(actions => actions.initD3); - const updateTransform = useStoreActions(actions => actions.updateTransform); + const initD3 = useStoreActions((actions) => actions.initD3); + const updateTransform = useStoreActions((actions) => actions.updateTransform); useEffect(() => { if (zoomPane.current) { @@ -27,7 +33,7 @@ export default (zoomPane: MutableRefObject, onMove: () => void, }, []); useEffect(() => { - if (shiftPressed) { + if (selectionKeyPressed) { d3ZoomInstance.on('zoom', null); } else { d3ZoomInstance.on('zoom', () => { @@ -37,7 +43,9 @@ export default (zoomPane: MutableRefObject, onMove: () => void, updateTransform(event.transform); - onMove(); + if (onMove) { + onMove(); + } }); if (state.d3Selection && state.d3Zoom) { @@ -53,5 +61,5 @@ export default (zoomPane: MutableRefObject, onMove: () => void, return () => { d3ZoomInstance.on('zoom', null); }; - }, [shiftPressed]); + }, [selectionKeyPressed]); }; diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index 48ca0e8d..c30c1f2f 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -7,7 +7,7 @@ import { Elements, Node } from '../types'; interface HookParams { deleteKeyCode: number; - onElementsRemove: (elements: Elements) => void; + onElementsRemove?: (elements: Elements) => void; } export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => { @@ -19,7 +19,7 @@ export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => { const deleteKeyPressed = useKeyPress(deleteKeyCode); useEffect(() => { - if (deleteKeyPressed && state.selectedElements) { + if (onElementsRemove && deleteKeyPressed && state.selectedElements) { let elementsToRemove = state.selectedElements; // we also want to remove the edges if only one node is selected diff --git a/src/types/index.ts b/src/types/index.ts index 20868fcf..a368ab77 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -108,7 +108,8 @@ export interface NodeComponentProps { yPos?: number; targetPosition?: Position; sourcePosition?: Position; - onClick?: (node: Node) => void | undefined; + onClick?: (node: Node) => void; + onNodeDragStart?: (node: Node) => void; onNodeDragStop?: (node: Node) => void; style?: CSSProperties; } @@ -122,9 +123,9 @@ export interface WrapNodeProps { xPos: number; yPos: number; isInteractive: boolean; - onClick: (node: Node) => void | undefined; - onNodeDragStart: (node: Node) => void; - onNodeDragStop: (node: Node) => void; + onClick?: (node: Node) => void; + onNodeDragStart?: (node: Node) => void; + onNodeDragStop?: (node: Node) => void; style?: CSSProperties; sourcePosition?: Position; targetPosition?: Position; From 56cfc8d731b8f4cb06a8c6332e4cd281eaf5021b Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 25 May 2020 17:12:11 +0200 Subject: [PATCH 3/8] refactor(easy-peasy): return single values from useStoreState --- .../Background/index.tsx | 8 ++- src/additional-components/Controls/index.tsx | 2 +- src/additional-components/MiniMap/index.tsx | 26 ++++------ src/components/NodesSelection/index.tsx | 50 +++++++++---------- src/container/EdgeRenderer/index.tsx | 33 ++++-------- src/container/GraphView/index.tsx | 23 ++++----- src/container/NodeRenderer/index.tsx | 19 +++---- src/hooks/useD3Zoom.ts | 22 +++----- src/hooks/useElementUpdater.ts | 17 +++---- src/hooks/useGlobalKeyHandler.ts | 23 ++++----- 10 files changed, 93 insertions(+), 130 deletions(-) diff --git a/src/additional-components/Background/index.tsx b/src/additional-components/Background/index.tsx index 93262819..782dd1b4 100644 --- a/src/additional-components/Background/index.tsx +++ b/src/additional-components/Background/index.tsx @@ -25,11 +25,9 @@ const defaultColors = { 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 width = useStoreState((s) => s.width); + const height = useStoreState((s) => s.height); + const [x, y, scale] = useStoreState((s) => s.transform); const bgClasses = classnames('react-flow__background', className); const bgColor = color ? color : defaultColors[variant]; diff --git a/src/additional-components/Controls/index.tsx b/src/additional-components/Controls/index.tsx index a26da2c0..44bf2f7f 100644 --- a/src/additional-components/Controls/index.tsx +++ b/src/additional-components/Controls/index.tsx @@ -25,7 +25,7 @@ interface ControlProps extends React.HTMLAttributes { const Controls = ({ style, showZoom = true, showFitView = true, showInteractive = true, className }: ControlProps) => { const setInteractive = useStoreActions((actions) => actions.setInteractive); - const { isInteractive } = useStoreState(({ isInteractive }) => ({ isInteractive })); + const isInteractive = useStoreState((s) => s.isInteractive); const mapClasses: string = classnames('react-flow__controls', className); return ( diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index 45b1486f..bb58ab20 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -30,27 +30,23 @@ const MiniMap = ({ nodeBorderRadius = 5, maskColor = 'rgba(10, 10, 10, .25)', }: MiniMapProps) => { - const state = useStoreState(({ width, height, nodes, transform: [tX, tY, tScale] }) => ({ - width, - height, - nodes, - tX, - tY, - tScale, - })); + const containerWidth = useStoreState((s) => s.width); + const containerHeight = useStoreState((s) => s.height); + const [tX, tY, tScale] = useStoreState((s) => s.transform); + const nodes = useStoreState((s) => s.nodes); const mapClasses = classnames('react-flow__minimap', className); const elementWidth = (style.width || baseStyle.width)! as number; const elementHeight = (style.height || baseStyle.height)! as number; const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc; - const hasNodes = state.nodes && state.nodes.length; + const hasNodes = nodes && nodes.length; - const bb = getRectOfNodes(state.nodes); + const bb = getRectOfNodes(nodes); const viewBB: Rect = { - x: -state.tX / state.tScale, - y: -state.tY / state.tScale, - width: state.width / state.tScale, - height: state.height / state.tScale, + x: -tX / tScale, + y: -tY / tScale, + width: containerWidth / tScale, + height: containerHeight / tScale, }; const boundingRect = hasNodes ? getBoundsofRects(bb, viewBB) : viewBB; @@ -79,7 +75,7 @@ const MiniMap = ({ }} className={mapClasses} > - {state.nodes.map((node) => ( + {nodes.map((node) => ( ))} { const [offset, setOffset] = useState({ x: 0, y: 0 }); const [startPositions, setStartPositions] = useState({}); - const state = useStoreState((s) => ({ - transform: s.transform, - selectedNodesBbox: s.selectedNodesBbox, - selectedElements: s.selectedElements, - snapToGrid: s.snapToGrid, - snapGrid: s.snapGrid, - nodes: s.nodes, - })); + const [tX, tY, tScale] = useStoreState((s) => s.transform); + const selectedNodesBbox = useStoreState((s) => s.selectedNodesBbox); + const selectedElements = useStoreState((s) => s.selectedElements); + const snapToGrid = useStoreState((s) => s.snapToGrid); + const snapGrid = useStoreState((s) => s.snapGrid); + const nodes = useStoreState((s) => s.nodes); + const updateNodePos = useStoreActions((a) => a.updateNodePos); - const [tx, ty, tScale] = state.transform; - const position = state.selectedNodesBbox; - const grid = (state.snapToGrid ? state.snapGrid : [1, 1])! as [number, number]; + const position = selectedNodesBbox; + const grid = (snapToGrid ? snapGrid : [1, 1])! as [number, number]; const onStart = (evt: MouseEvent) => { const scaledClient: XYPosition = { x: evt.clientX / tScale, y: evt.clientY / tScale, }; - const offsetX: number = scaledClient.x - position.x - tx; - const offsetY: number = scaledClient.y - position.y - ty; - const selectedNodes = state.selectedElements - ? (state.selectedElements.filter(isNode) as Node[]).map( - (selectedNode) => state.nodes.find((node) => node.id === selectedNode.id)! as Node - ) + const offsetX: number = scaledClient.x - position.x - tX; + const offsetY: number = scaledClient.y - position.y - tY; + const selectedNodes = selectedElements + ? selectedElements + .filter(isNode) + .map((selectedNode) => nodes.find((node) => node.id === selectedNode.id)! as Node) : []; const nextStartPositions = getStartPositions(selectedNodes); @@ -70,11 +68,11 @@ export default memo(() => { y: evt.clientY / tScale, }; - if (state.selectedElements) { - (state.selectedElements.filter(isNode) as Node[]).forEach((node) => { + if (selectedElements) { + selectedElements.filter(isNode).forEach((node) => { const pos: XYPosition = { - x: startPositions[node.id].x + scaledClient.x - position.x - offset.x - tx, - y: startPositions[node.id].y + scaledClient.y - position.y - offset.y - ty, + x: startPositions[node.id].x + scaledClient.x - position.x - offset.x - tX, + y: startPositions[node.id].y + scaledClient.y - position.y - offset.y - tY, }; updateNodePos({ id: node.id, pos }); @@ -86,7 +84,7 @@ export default memo(() => {
{
diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 351b3b34..a25b0167 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -187,23 +187,13 @@ function renderEdge( } const EdgeRenderer = memo((props: EdgeRendererProps) => { - const { - transform, - edges, - nodes, - connectionSourceId, - connectionPosition: { x, y }, - selectedElements, - isInteractive, - } = useStoreState((s) => ({ - transform: s.transform, - edges: s.edges, - nodes: s.nodes, - connectionSourceId: s.connectionSourceId, - connectionPosition: s.connectionPosition, - selectedElements: s.selectedElements, - isInteractive: s.isInteractive, - })); + const [tX, tY, tScale] = useStoreState((s) => s.transform); + const edges = useStoreState((s) => s.edges); + const nodes = useStoreState((s) => s.nodes); + const connectionSourceId = useStoreState((s) => s.connectionSourceId); + const connectionPosition = useStoreState((s) => s.connectionPosition); + const selectedElements = useStoreState((s) => s.selectedElements); + const isInteractive = useStoreState((s) => s.isInteractive); const { width, height, connectionLineStyle, connectionLineType } = props; @@ -211,8 +201,7 @@ const EdgeRenderer = memo((props: EdgeRendererProps) => { return null; } - const [tx, ty, tScale] = transform; - const transformStyle = `translate(${tx},${ty}) scale(${tScale})`; + const transformStyle = `translate(${tX},${tY}) scale(${tScale})`; return ( @@ -222,9 +211,9 @@ const EdgeRenderer = memo((props: EdgeRendererProps) => { { const zoomPane = useRef(null); const rendererNode = useRef(null); - const state = useStoreState((s) => ({ - width: s.width, - height: s.height, - nodes: s.nodes, - edges: s.edges, - d3Initialised: s.d3Initialised, - nodesSelectionActive: s.nodesSelectionActive, - })); + const width = useStoreState((s) => s.width); + const height = useStoreState((s) => s.height); + const d3Initialised = useStoreState((s) => s.d3Initialised); + const nodesSelectionActive = useStoreState((s) => s.nodesSelectionActive); + const updateSize = useStoreActions((actions) => actions.updateSize); const setNodesSelection = useStoreActions((actions) => actions.setNodesSelection); const setOnConnect = useStoreActions((a) => a.setOnConnect); @@ -106,7 +103,7 @@ const GraphView = memo( useD3Zoom({ zoomPane, onMove, selectionKeyPressed }); useEffect(() => { - if (state.d3Initialised && onLoad) { + if (d3Initialised && onLoad) { onLoad({ fitView, zoomIn, @@ -114,7 +111,7 @@ const GraphView = memo( project, }); } - }, [state.d3Initialised, onLoad]); + }, [d3Initialised, onLoad]); useEffect(() => { setSnapGrid({ snapToGrid, snapGrid }); @@ -137,15 +134,15 @@ const GraphView = memo( onlyRenderVisibleNodes={onlyRenderVisibleNodes} /> {selectionKeyPressed && } - {state.nodesSelectionActive && } + {nodesSelectionActive && }
); diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 05f1e32c..8f8ffe65 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -49,18 +49,15 @@ function renderNode( } const NodeRenderer = memo(({ onlyRenderVisibleNodes = true, ...props }: NodeRendererProps) => { - const { nodes, transform, selectedElements, width, height, isInteractive } = useStoreState((s) => ({ - nodes: s.nodes, - transform: s.transform, - selectedElements: s.selectedElements, - width: s.width, - height: s.height, - isInteractive: s.isInteractive, - })); - - const [tx, ty, tScale] = transform; + const nodes = useStoreState((s) => s.nodes); + const transform = useStoreState((s) => s.transform); + const selectedElements = useStoreState((s) => s.selectedElements); + const width = useStoreState((s) => s.width); + const height = useStoreState((s) => s.height); + const isInteractive = useStoreState((s) => s.isInteractive); + const [tX, tY, tScale] = transform; const transformStyle = { - transform: `translate(${tx}px,${ty}px) scale(${tScale})`, + transform: `translate(${tX}px,${tY}px) scale(${tScale})`, }; const renderNodes = onlyRenderVisibleNodes diff --git a/src/hooks/useD3Zoom.ts b/src/hooks/useD3Zoom.ts index fc0527e3..0cd64333 100644 --- a/src/hooks/useD3Zoom.ts +++ b/src/hooks/useD3Zoom.ts @@ -1,5 +1,5 @@ import { useEffect, MutableRefObject } from 'react'; -import * as d3Zoom from 'd3-zoom'; +import { zoom, zoomIdentity } from 'd3-zoom'; import { select, event } from 'd3-selection'; import { useStoreState, useStoreActions } from '../store/hooks'; @@ -10,17 +10,14 @@ interface UseD3ZoomParams { onMove?: () => void; } -const d3ZoomInstance = d3Zoom - .zoom() +const d3ZoomInstance = zoom() .scaleExtent([0.5, 2]) .filter(() => !event.button); export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): void => { - const state = useStoreState((s) => ({ - transform: s.transform, - d3Selection: s.d3Selection, - d3Zoom: s.d3Zoom, - })); + const transform = useStoreState((s) => s.transform); + const d3Selection = useStoreState((s) => s.d3Selection); + const d3Zoom = useStoreState((s) => s.d3Zoom); const initD3 = useStoreActions((actions) => actions.initD3); const updateTransform = useStoreActions((actions) => actions.updateTransform); @@ -48,13 +45,10 @@ export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): voi } }); - if (state.d3Selection && state.d3Zoom) { + if (d3Selection && d3Zoom) { // we need to restore the graph transform otherwise d3 zoom transform and graph transform are not synced - const graphTransform = d3Zoom.zoomIdentity - .translate(state.transform[0], state.transform[1]) - .scale(state.transform[2]); - - state.d3Selection.call(state.d3Zoom.transform, graphTransform); + const graphTransform = zoomIdentity.translate(transform[0], transform[1]).scale(transform[2]); + d3Selection.call(d3Zoom.transform, graphTransform); } } diff --git a/src/hooks/useElementUpdater.ts b/src/hooks/useElementUpdater.ts index 17bdcdfa..79b09444 100644 --- a/src/hooks/useElementUpdater.ts +++ b/src/hooks/useElementUpdater.ts @@ -6,13 +6,8 @@ import { parseElement, isNode, isEdge } from '../utils/graph'; import { Elements, Node, Edge } from '../types'; const useElementUpdater = (elements: Elements): void => { - const state = useStoreState((s) => ({ - nodes: s.nodes, - edges: s.edges, - transform: s.transform, - snapToGrid: s.snapToGrid, - snapGrid: s.snapGrid, - })); + const stateNodes = useStoreState((s) => s.nodes); + const stateEdges = useStoreState((s) => s.edges); const setNodes = useStoreActions((a) => a.setNodes); const setEdges = useStoreActions((a) => a.setEdges); @@ -22,7 +17,7 @@ const useElementUpdater = (elements: Elements): void => { const edges: Edge[] = elements.filter(isEdge).map((e) => parseElement(e) as Edge); const nextNodes: Node[] = nodes.map((propNode) => { - const existingNode = state.nodes.find((n) => n.id === propNode.id); + const existingNode = stateNodes.find((n) => n.id === propNode.id); if (existingNode) { const data = !isEqual(existingNode.data, propNode.data) @@ -59,8 +54,8 @@ const useElementUpdater = (elements: Elements): void => { return parseElement(propNode) as Node; }); - const nodesChanged: boolean = !isEqual(state.nodes, nextNodes); - const edgesChanged: boolean = !isEqual(state.edges, edges); + const nodesChanged: boolean = !isEqual(stateNodes, nextNodes); + const edgesChanged: boolean = !isEqual(stateEdges, edges); if (nodesChanged) { setNodes(nextNodes); @@ -69,7 +64,7 @@ const useElementUpdater = (elements: Elements): void => { if (edgesChanged) { setEdges(edges); } - }, [elements, state.nodes, state.edges]); + }, [elements, stateNodes, stateEdges]); }; export default useElementUpdater; diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index c30c1f2f..cce41486 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -2,8 +2,8 @@ import { useEffect } from 'react'; import { useStoreState, useStoreActions } from '../store/hooks'; import useKeyPress from './useKeyPress'; -import { isEdge, getConnectedEdges } from '../utils/graph'; -import { Elements, Node } from '../types'; +import { isNode, getConnectedEdges } from '../utils/graph'; +import { Elements } from '../types'; interface HookParams { deleteKeyCode: number; @@ -11,22 +11,21 @@ interface HookParams { } export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => { - const state = useStoreState((s) => ({ - selectedElements: s.selectedElements, - edges: s.edges, - })); + const selectedElements = useStoreState((s) => s.selectedElements); + const edges = useStoreState((s) => s.edges); + const setNodesSelection = useStoreActions((a) => a.setNodesSelection); const deleteKeyPressed = useKeyPress(deleteKeyCode); useEffect(() => { - if (onElementsRemove && deleteKeyPressed && state.selectedElements) { - let elementsToRemove = state.selectedElements; + if (onElementsRemove && deleteKeyPressed && selectedElements) { + let elementsToRemove = selectedElements; // we also want to remove the edges if only one node is selected - if (state.selectedElements.length === 1 && !isEdge(state.selectedElements[0])) { - const node = (state.selectedElements[0] as unknown) as Node; - const connectedEdges = getConnectedEdges([node], state.edges); - elementsToRemove = [...state.selectedElements, ...connectedEdges]; + if (selectedElements.length === 1 && isNode(selectedElements[0])) { + const node = selectedElements[0]; + const connectedEdges = getConnectedEdges([node], edges); + elementsToRemove = [...selectedElements, ...connectedEdges]; } onElementsRemove(elementsToRemove); From 714cecaba946bbee21946116d09dbbc87a70ba83 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 25 May 2020 17:14:09 +0200 Subject: [PATCH 4/8] refactor(easy-peasy): return single values from useStoreActions --- src/components/Handle/index.tsx | 6 ++---- src/components/UserSelection/index.tsx | 8 +++----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/Handle/index.tsx b/src/components/Handle/index.tsx index 61b245f8..44d686d5 100644 --- a/src/components/Handle/index.tsx +++ b/src/components/Handle/index.tsx @@ -15,10 +15,8 @@ const Handle = memo( ...rest }: HandleProps) => { const nodeId = useContext(NodeIdContext) as ElementId; - const { setPosition, setSourceId } = useStoreActions((a) => ({ - setPosition: a.setConnectionPosition, - setSourceId: a.setConnectionSourceId, - })); + const setPosition = useStoreActions((a) => a.setConnectionPosition); + const setSourceId = useStoreActions((a) => a.setConnectionSourceId); const onConnectAction = useStoreState((s) => s.onConnect); const onConnectExtended = (params: Connection) => { onConnectAction(params); diff --git a/src/components/UserSelection/index.tsx b/src/components/UserSelection/index.tsx index 94b32daa..1922e200 100644 --- a/src/components/UserSelection/index.tsx +++ b/src/components/UserSelection/index.tsx @@ -45,11 +45,9 @@ const SelectionRect = () => { }; export default memo(({ isInteractive }: UserSelectionProps) => { - const { setUserSelection, updateUserSelection, unsetUserSelection } = useStoreActions((a) => ({ - setUserSelection: a.setUserSelection, - updateUserSelection: a.updateUserSelection, - unsetUserSelection: a.unsetUserSelection, - })); + const setUserSelection = useStoreActions((a) => a.setUserSelection); + const updateUserSelection = useStoreActions((a) => a.updateUserSelection); + const unsetUserSelection = useStoreActions((a) => a.unsetUserSelection); if (!isInteractive) { return null; From c46d962ea1a9939861d9c2a08bbb7e9eee5735b0 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 25 May 2020 17:17:32 +0200 Subject: [PATCH 5/8] chore(readme): components anchor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bd73aabe..bbc02283 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ 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) -- [Components](#plugins) +- [Components](#components) - [Background](#background) - [Minimap](#minimap) - [Controls](#controls) From d567d00749aa0ea1904806bd5787f0115c7194ee Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 25 May 2020 17:19:32 +0200 Subject: [PATCH 6/8] chore(readme): background prop types --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index bbc02283..54b58d02 100644 --- a/README.md +++ b/README.md @@ -82,10 +82,6 @@ const BasicFlow = () => ( - `connectionLineStyle`: connection style as svg attributes - `deleteKeyCode`: default: `8` (delete) - `selectionKeyCode`: default: `16` (shift) -- `showBackground`: default: `true` -- `backgroundGap`: gap size - default: `16` -- `backgroundColor`: color of dots or lines - default: `#eee` -- `backgroundType`: background type = `dots` or `lines` - default: `dots` - `snapToGrid`: default: `false` - `snapGrid`: [x, y] array - default: `[16, 16]` - `onlyRenderVisibleNodes`: default: `true` @@ -268,7 +264,7 @@ const FlowWithBackground = () => ( - `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` +- `color`: string - the color of the dots or lines - default: `#999` for dots, '#eee' for lines - `style`: css properties - `className`: class name From 0a140404dcca0c2e25dedb78012f8ff4d1a8fd65 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 25 May 2020 17:21:28 +0200 Subject: [PATCH 7/8] chore(readme): background prop types --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 54b58d02..41a42990 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,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**. +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: ```javascript import ReactFlow, { Background } from 'react-flow-renderer'; @@ -263,8 +263,8 @@ const FlowWithBackground = () => ( - `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` for dots, '#eee' for lines +- `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` for dots, `#eee` for lines - `style`: css properties - `className`: class name From b6b3fded77f8522bdc0156466e7b161200996c83 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 25 May 2020 17:22:26 +0200 Subject: [PATCH 8/8] chore(readme): intro --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 41a42990..b37e7445 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # :ocean: React Flow -React Flow is a library for building node-based graphs. You can easily implement custom node types and it comes with plugins like a mini-map and graph controls. Feel free to check out the [examples](https://react-flow.netlify.com/) or read the [blog post](https://webkid.io/blog/react-flow-node-based-graph-library/) to get started. +React Flow is a library for building node-based graphs. You can easily implement custom node types and it comes with components like a mini-map and graph controls. Feel free to check out the [examples](https://react-flow.netlify.com/) or read the [blog post](https://webkid.io/blog/react-flow-node-based-graph-library/) to get started. - [Key Features](#key-features) - [Installation](#installation)