feat(edges): add data and classname props, unify node/edge handling closes #357

This commit is contained in:
moklick
2020-07-27 12:40:39 +02:00
parent cbc41f244e
commit b3391860da
7 changed files with 127 additions and 90 deletions
+9 -6
View File
@@ -1,16 +1,19 @@
import React from 'react'; import React from 'react';
export default function CustomEdge({ export default function CustomEdge({ id, sourceX, sourceY, targetX, targetY, style = {}, data }) {
id, sourceX, sourceY, targetX, targetY, label, style = {}
}) {
return ( return (
<> <>
<path id={id} style={style} className="react-flow__edge-path" d={`M ${sourceX},${sourceY}L ${targetX},${targetY}`} /> <path
id={id}
style={style}
className="react-flow__edge-path"
d={`M ${sourceX},${sourceY}L ${targetX},${targetY}`}
/>
<text> <text>
<textPath href={`#${id}`} style={{ fontSize: '12px' }} startOffset="50%" textAnchor="middle"> <textPath href={`#${id}`} style={{ fontSize: '12px' }} startOffset="50%" textAnchor="middle">
{label} {data.text}
</textPath> </textPath>
</text> </text>
</> </>
); );
}; }
+3
View File
@@ -19,6 +19,7 @@ interface EdgeWrapperProps {
selected: boolean; selected: boolean;
elementsSelectable: boolean; elementsSelectable: boolean;
isHidden?: boolean; isHidden?: boolean;
data?: any;
} }
export default (EdgeComponent: ComponentType<EdgeCompProps>) => { export default (EdgeComponent: ComponentType<EdgeCompProps>) => {
@@ -38,6 +39,7 @@ export default (EdgeComponent: ComponentType<EdgeCompProps>) => {
labelBgStyle, labelBgStyle,
className, className,
isHidden, isHidden,
data,
...rest ...rest
}: EdgeWrapperProps) => { }: EdgeWrapperProps) => {
const setSelectedElements = useStoreActions((a) => a.setSelectedElements); const setSelectedElements = useStoreActions((a) => a.setSelectedElements);
@@ -76,6 +78,7 @@ export default (EdgeComponent: ComponentType<EdgeCompProps>) => {
labelStyle={labelStyle} labelStyle={labelStyle}
labelShowBg={labelShowBg} labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle} labelBgStyle={labelBgStyle}
data={data}
{...rest} {...rest}
/> />
</g> </g>
+2 -1
View File
@@ -166,6 +166,7 @@ function renderEdge(
key={edge.id} key={edge.id}
id={edge.id} id={edge.id}
type={edge.type} type={edge.type}
data={edge.data}
onClick={props.onElementClick} onClick={props.onElementClick}
selected={isSelected} selected={isSelected}
animated={edge.animated} animated={edge.animated}
@@ -216,7 +217,7 @@ const EdgeRenderer = memo((props: EdgeRendererProps) => {
<svg width={width} height={height} className="react-flow__edges"> <svg width={width} height={height} className="react-flow__edges">
<MarkerDefinitions color={arrowHeadColor} /> <MarkerDefinitions color={arrowHeadColor} />
<g transform={transformStyle}> <g transform={transformStyle}>
{edges.map((e: Edge) => renderEdge(e, props, nodes, selectedElements, elementsSelectable))} {edges.map((edge) => renderEdge(edge, props, nodes, selectedElements, elementsSelectable))}
{renderConnectionLine && ( {renderConnectionLine && (
<ConnectionLine <ConnectionLine
nodes={nodes} nodes={nodes}
+90 -62
View File
@@ -3,94 +3,122 @@ import isEqual from 'fast-deep-equal';
import { useStoreState, useStoreActions } from '../store/hooks'; import { useStoreState, useStoreActions } from '../store/hooks';
import { parseElement, isNode, isEdge } from '../utils/graph'; import { parseElement, isNode, isEdge } from '../utils/graph';
import { Elements, Node, Edge } from '../types'; import { Elements, Node, Edge, FlowElement } from '../types';
const useElementUpdater = (elements: Elements): void => { const useElementUpdater = (propElements: Elements): void => {
const stateNodes = useStoreState((s) => s.nodes); const stateElements = useStoreState((s) => s.elements);
const stateEdges = useStoreState((s) => s.edges); const setElements = useStoreActions((a) => a.setElements);
const setNodes = useStoreActions((a) => a.setNodes);
const setEdges = useStoreActions((a) => a.setEdges);
useEffect(() => { useEffect(() => {
const nextEdges: Edge[] = elements.filter(isEdge).map((e) => parseElement(e) as Edge); const nextElements: Elements = propElements.map((propElement) => {
const nextNodes: Node[] = elements.filter(isNode).map((propNode) => { const existingElement = stateElements.find((el) => el.id === propElement.id);
const existingNode = stateNodes.find((n) => n.id === propNode.id);
if (existingNode) { if (existingElement) {
const data = !isEqual(existingNode.data, propNode.data) const data = !isEqual(existingElement.data, propElement.data)
? { ...existingNode.data, ...propNode.data } ? { ...existingElement.data, ...propElement.data }
: existingNode.data; : existingElement.data;
const style = !isEqual(existingNode.style, propNode.style) const style = !isEqual(existingElement.style, propElement.style)
? { ...existingNode.style, ...propNode.style } ? { ...existingElement.style, ...propElement.style }
: existingNode.style; : existingElement.style;
const className = existingNode.className === propNode.className ? existingNode.className : propNode.className; const elementProps = {
const isHidden = existingNode.isHidden === propNode.isHidden ? existingNode.isHidden : propNode.isHidden; ...existingElement,
const draggable = existingNode.draggable === propNode.draggable ? existingNode.draggable : propNode.draggable;
const selectable =
existingNode.selectable === propNode.selectable ? existingNode.selectable : propNode.selectable;
const connectable =
existingNode.connectable === propNode.connectable ? existingNode.connectable : propNode.connectable;
const positionChanged =
existingNode.position.x !== propNode.position.x || existingNode.position.y !== propNode.position.y;
const nodeProps = {
...existingNode,
data,
}; };
if (positionChanged) { if (typeof data !== 'undefined') {
nodeProps.__rf = { elementProps.data = data;
...existingNode.__rf,
position: propNode.position,
};
nodeProps.position = propNode.position;
} }
if (typeof style !== 'undefined') { if (typeof style !== 'undefined') {
nodeProps.style = style; elementProps.style = style;
} }
if (typeof className !== 'undefined') { if (typeof propElement.className !== 'undefined') {
nodeProps.className = className; elementProps.className = propElement.className;
} }
if (typeof isHidden !== 'undefined') { if (typeof propElement.isHidden !== 'undefined') {
nodeProps.isHidden = isHidden; elementProps.isHidden = propElement.isHidden;
} }
if (typeof draggable !== 'undefined') { if (isNode(existingElement)) {
nodeProps.draggable = draggable; const propNode = propElement as Node;
} const nodeProps = elementProps as Node;
if (typeof selectable !== 'undefined') { const positionChanged =
nodeProps.selectable = selectable; existingElement.position.x !== propNode.position.x || existingElement.position.y !== propNode.position.y;
}
if (typeof connectable !== 'undefined') { if (positionChanged) {
nodeProps.connectable = connectable; nodeProps.__rf = {
} ...existingElement.__rf,
position: propNode.position,
};
nodeProps.position = propNode.position;
}
return nodeProps; if (typeof propNode.draggable !== 'undefined') {
nodeProps.draggable = propNode.draggable;
}
if (typeof propNode.selectable !== 'undefined') {
nodeProps.selectable = propNode.selectable;
}
if (typeof propNode.connectable !== 'undefined') {
nodeProps.connectable = propNode.connectable;
}
return nodeProps;
} else if (isEdge(existingElement)) {
const propEdge = propElement as Edge;
const edgeProps = elementProps as Edge;
const labelStyle = !isEqual(existingElement.labelStyle, propEdge.labelStyle)
? { ...existingElement.labelStyle, ...propEdge.labelStyle }
: existingElement.labelStyle;
const labelBgStyle = !isEqual(existingElement.labelBgStyle, propEdge.labelBgStyle)
? { ...existingElement.labelBgStyle, ...propEdge.labelBgStyle }
: existingElement.labelBgStyle;
if (typeof propEdge.label !== 'undefined') {
edgeProps.label = propEdge.label;
}
if (typeof labelStyle !== 'undefined') {
edgeProps.labelStyle = labelStyle;
}
if (typeof propEdge.labelShowBg !== 'undefined') {
edgeProps.labelShowBg = propEdge.labelShowBg;
}
if (typeof labelBgStyle !== 'undefined') {
edgeProps.labelBgStyle = labelBgStyle;
}
if (typeof propEdge.animated !== 'undefined') {
edgeProps.animated = propEdge.animated;
}
if (typeof propEdge.arrowHeadType !== 'undefined') {
edgeProps.arrowHeadType = propEdge.arrowHeadType;
}
return edgeProps;
}
} }
return parseElement(propNode) as Node; return parseElement(propElement) as FlowElement;
}); });
const nodesChanged: boolean = !isEqual(stateNodes, nextNodes); const elementsChanged: boolean = !isEqual(stateElements, nextElements);
const edgesChanged: boolean = !isEqual(stateEdges, nextEdges);
if (nodesChanged) { if (elementsChanged) {
setNodes(nextNodes); setElements(nextElements);
} }
}, [propElements, stateElements]);
if (edgesChanged) {
setEdges(nextEdges);
}
}, [elements, stateNodes, stateEdges]);
}; };
export default useElementUpdater; export default useElementUpdater;
+15 -19
View File
@@ -1,4 +1,4 @@
import { createStore, Action, action, Thunk, thunk } from 'easy-peasy'; import { createStore, Action, action, Thunk, thunk, computed, Computed } from 'easy-peasy';
import isEqual from 'fast-deep-equal'; import isEqual from 'fast-deep-equal';
import { Selection as D3Selection, ZoomBehavior } from 'd3'; import { Selection as D3Selection, ZoomBehavior } from 'd3';
import { zoom, zoomIdentity } from 'd3-zoom'; import { zoom, zoomIdentity } from 'd3-zoom';
@@ -7,7 +7,7 @@ import { select } from 'd3-selection';
import { getDimensions, clamp } from '../utils'; import { getDimensions, clamp } from '../utils';
import { getHandleBounds } from '../components/Nodes/utils'; import { getHandleBounds } from '../components/Nodes/utils';
import { getNodesInside, getConnectedEdges, getRectOfNodes } from '../utils/graph'; import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge } from '../utils/graph';
import { import {
ElementId, ElementId,
Elements, Elements,
@@ -55,8 +55,9 @@ export interface StoreModel {
width: number; width: number;
height: number; height: number;
transform: Transform; transform: Transform;
nodes: Node[]; elements: Elements;
edges: Edge[]; nodes: Computed<StoreModel, Node[]>;
edges: Computed<StoreModel, Edge[]>;
selectedElements: Elements | null; selectedElements: Elements | null;
selectedNodesBbox: Rect; selectedNodesBbox: Rect;
@@ -89,9 +90,7 @@ export interface StoreModel {
setOnConnect: Action<StoreModel, OnConnectFunc>; setOnConnect: Action<StoreModel, OnConnectFunc>;
setNodes: Action<StoreModel, Node[]>; setElements: Action<StoreModel, Elements>;
setEdges: Action<StoreModel, Edge[]>;
updateNodeDimensions: Action<StoreModel, NodeDimensionUpdate>; updateNodeDimensions: Action<StoreModel, NodeDimensionUpdate>;
@@ -138,8 +137,9 @@ export const storeModel: StoreModel = {
width: 0, width: 0,
height: 0, height: 0,
transform: [0, 0, 1], transform: [0, 0, 1],
nodes: [], elements: [],
edges: [], nodes: computed([(state) => state.elements], (elements) => elements.filter((el) => isNode(el)) as Node[]),
edges: computed([(state) => state.elements], (elements) => elements.filter((el) => isEdge(el)) as Edge[]),
selectedElements: null, selectedElements: null,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
@@ -181,12 +181,8 @@ export const storeModel: StoreModel = {
state.onConnect = onConnect; state.onConnect = onConnect;
}), }),
setNodes: action((state, nodes) => { setElements: action((state, elements) => {
state.nodes = nodes; state.elements = elements;
}),
setEdges: action((state, edges) => {
state.edges = edges;
}), }),
updateNodeDimensions: action((state, { id, nodeElement }) => { updateNodeDimensions: action((state, { id, nodeElement }) => {
@@ -207,8 +203,8 @@ export const storeModel: StoreModel = {
target: getHandleBounds('.target', nodeElement, bounds, state.transform[2]), target: getHandleBounds('.target', nodeElement, bounds, state.transform[2]),
}; };
state.nodes.forEach((n) => { state.elements.forEach((n) => {
if (n.id === id) { if (n.id === id && isNode(n)) {
n.__rf = { n.__rf = {
...n.__rf, ...n.__rf,
...dimensions, ...dimensions,
@@ -230,8 +226,8 @@ export const storeModel: StoreModel = {
}; };
} }
state.nodes.forEach((n) => { state.elements.forEach((n) => {
if (n.id === id) { if (n.id === id && isNode(n)) {
n.__rf = { n.__rf = {
...n.__rf, ...n.__rf,
position, position,
+7 -1
View File
@@ -2,7 +2,9 @@ import { CSSProperties, MouseEvent } from 'react';
export type ElementId = string; export type ElementId = string;
export type Elements = Array<Node | Edge>; export type FlowElement = Node | Edge;
export type Elements = Array<FlowElement>;
export type Transform = [number, number, number]; export type Transform = [number, number, number];
@@ -64,6 +66,8 @@ export interface Edge {
animated?: boolean; animated?: boolean;
arrowHeadType?: ArrowHeadType; arrowHeadType?: ArrowHeadType;
isHidden?: boolean; isHidden?: boolean;
data?: any;
className?: string;
} }
export enum BackgroundVariant { export enum BackgroundVariant {
@@ -95,6 +99,7 @@ export interface EdgeProps {
style?: CSSProperties; style?: CSSProperties;
arrowHeadType?: ArrowHeadType; arrowHeadType?: ArrowHeadType;
markerEndId?: string; markerEndId?: string;
data?: any;
} }
export interface EdgeBezierProps extends EdgeProps { export interface EdgeBezierProps extends EdgeProps {
@@ -227,6 +232,7 @@ export interface EdgeCompProps {
onClick?: (edge: Edge) => void; onClick?: (edge: Edge) => void;
animated?: boolean; animated?: boolean;
selected?: boolean; selected?: boolean;
data?: any;
} }
export interface EdgeTextProps { export interface EdgeTextProps {
+1 -1
View File
@@ -85,7 +85,7 @@ export const project = (position: XYPosition): XYPosition => {
export const parseElement = (element: Node | Edge): Node | Edge => { export const parseElement = (element: Node | Edge): Node | Edge => {
if (!element.id) { if (!element.id) {
throw new Error('All elements (nodes and edges) need to have an id.'); throw new Error('All nodes and edges need to have an id.');
} }
if (isEdge(element)) { if (isEdge(element)) {