流程组件React Flow节点拖拽添加与自定义节点
创建流程时,会涉及节点的拖拽,添加,删除 以及节点内容的修改
效果图
分步实现,每一步新增的功能代码用红色标记
实现节点拖拽
主要是增加两个函数onDragOver onDrop将选择的节点添加
index.tsx
import React, { useState, useRef, useCallback } from 'react'; import ReactFlow, { ReactFlowProvider, addEdge, useNodesState, useEdgesState, Controls, } from 'react-flow-renderer'; import Sidebar from './Sidebar'; import './index.css'; const initialNodes = [ { id: '1', type: 'input', data: { label: 'input node' }, position: { x: 250, y: 5 }, }, ]; let id = 0; const getId = () => `dndnode_${id++}`; const DnDFlow = () => { const reactFlowWrapper = useRef(null); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [reactFlowInstance, setReactFlowInstance] = useState(null); const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), []); // 节点拖拽时 const onDragOver = useCallback((event) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; }, []); // 添加新节点 const onDrop = useCallback( (event) => { event.preventDefault(); const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect(); const type = event.dataTransfer.getData('application/reactflow'); // check if the dropped element is valid if (typeof type === 'undefined' || !type) { return; } const position = reactFlowInstance.project({ x: event.clientX - reactFlowBounds.left, y: event.clientY - reactFlowBounds.top, }); const newNode = { id: getId(), type, position, data: { label: `${type} node` }, }; setNodes((nds) => nds.concat(newNode)); }, [reactFlowInstance] ); return ( <div className="dndflow"> <ReactFlowProvider> <div className="reactflow-wrapper" ref={reactFlowWrapper}> <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} onInit={setReactFlowInstance} onDrop={onDrop} onDragOver={onDragOver} fitView > <Controls /> </ReactFlow> </div> <Sidebar /> // 添加栏的内容 </ReactFlowProvider> </div> ); }; export default DnDFlow;
左侧拖拽节点内容
siderBar.tsx
import React from 'react'; export default () => { const onDragStart = (event, nodeType) => { event.dataTransfer.setData('application/reactflow', nodeType); event.dataTransfer.effectAllowed = 'move'; }; return ( <aside> <div className="description">You can drag these nodes to the pane on the right.</div> <div className="dndnode input" onDragStart={(event) => onDragStart(event, 'input')} draggable> Input Node </div> // 传回节点的类型,这里的类型可以使用自定义的类型 <div className="dndnode" onDragStart={(event) => onDragStart(event, 'default')} draggable> Default Node </div> <div className="dndnode output" onDragStart={(event) => onDragStart(event, 'output')} draggable> Output Node </div> </aside> ); };
效果:
实现自定义节点
步骤: 1.创建一个节点组件 2.增加一个定义节点类型名称,与节点组件对应 3.将节点类型传入ReactFlow的nodeTypes 4.节点拖拽新增时,将节点类型改为自定义类型名称
index.tsx
import React, { useState, useRef, useCallback } from 'react'; import ReactFlow, { ReactFlowProvider, addEdge, useNodesState, useEdgesState, Controls, } from 'react-flow-renderer'; import CustomNode from '../components/customNode'; import Sidebar from './Sidebar'; import './index.css'; // 自定义节点类型 const nodeTypes = { custom: CustomNode, }; const initialNodes = [ { id: '1', type: 'input', data: { label: 'input node' }, position: { x: 250, y: 5 }, }, ]; let id = 0; const getId = () => `dndnode_${id++}`; const DnDFlow = () => { const reactFlowWrapper = useRef(null); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [reactFlowInstance, setReactFlowInstance] = useState(null); const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), []); // 节点拖拽时 const onDragOver = useCallback((event) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; }, []); // 添加新节点 const onDrop = useCallback( (event) => { event.preventDefault(); const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect(); const type = event.dataTransfer.getData('application/reactflow'); // check if the dropped element is valid if (typeof type === 'undefined' || !type) { return; } const position = reactFlowInstance.project({ x: event.clientX - reactFlowBounds.left, y: event.clientY - reactFlowBounds.top, }); const newNode = { id: getId(), type, position, data: { label: `${type} node` }, }; setNodes((nds) => nds.concat(newNode)); }, [reactFlowInstance] ); return ( <div className="dndflow"> <ReactFlowProvider> <div className="reactflow-wrapper" ref={reactFlowWrapper}> <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} onInit={setReactFlowInstance} onDrop={onDrop} onDragOver={onDragOver} fitView > <Controls /> </ReactFlow> </div> <Sidebar /> // 添加栏的内容 </ReactFlowProvider> </div> ); }; export default DnDFlow;
Sidebar.tsx左侧添加栏内容
import React from 'react'; export default () => { const onDragStart = (event: any, nodeType: any) => { event.dataTransfer.setData('application/reactflow', nodeType); event.dataTransfer.effectAllowed = 'move'; }; return ( <aside> <div className="description">拖拽节点到画布添加</div> <div className="dndnode input" onDragStart={(event) => { event.stopPropagation(); onDragStart(event, 'input'); }} draggable > 开始节点 </div> <div className="dndnode" onDragStart={(event) => { event.stopPropagation(); onDragStart(event, 'custom'); // 类型为自定义节点类型名称 }} draggable > 中间节点 </div> <div className="dndnode output" onDragStart={(event) => { event.stopPropagation(); onDragStart(event, 'output'); }} draggable > 结束节点 </div> </aside> ); };
customNode.tsx自定义节点内容
import React, { memo } from 'react'; import { CloseOutlined } from '@ant-design/icons'; import { Handle } from 'react-flow-renderer'; export default memo(({ data, id, isConnectable }: any) => { // console.log(1, data) return ( <> <Handle type="target" position="top" className="my_handle" onConnect={(params) => console.log('handle onConnect', params)} isConnectable={isConnectable} /> // 上面的连接点 <div className="nodeContent" style={data.style}> <div className="nodelabel">{data.label}</div> <div className="close"> <CloseOutlined onClick={(e) => { e.stopPropagation(); data.onChange(id); }} className="icon-close" /> </div> </div> // 节点内容,可以自定义内容 <Handle type="source" position="bottom" id="a" className="my_handle" isConnectable={isConnectable} /> // 下面的连接点 </> ); });
效果:
修改节点内容
步骤: 1.创建一个修改内容的表单组件 2.点击节点将当前节点数据传入 3.将节点初始配置回显 4.修改表单,触发修改事件,回传修改后的数据 5.在修改函数中,修改数据,重新设置nodes
index.tsx
import React, { useState, useRef, useCallback } from 'react'; import ReactFlow, { ReactFlowProvider, addEdge, useNodesState, useEdgesState, Controls, } from 'react-flow-renderer'; import CustomNode from '../components/customNode'; import UpdateNode from '../components/nodeContent'; import Sidebar from './Sidebar'; import './index.css'; // 自定义节点类型 const nodeTypes = { custom: CustomNode, }; const initialNodes = [ { id: '1', type: 'input', data: { label: 'input node' }, position: { x: 250, y: 5 }, }, ]; let id = 0; const getId = () => `dndnode_${id++}`; const DnDFlow = () => { const reactFlowWrapper = useRef(null); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [reactFlowInstance, setReactFlowInstance] = useState(null);
const [nodeInfo, setNodeInfo] = useState<any>({});
const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), []); // 节点拖拽时 const onDragOver = useCallback((event) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; }, []); // 添加新节点 const onDrop = useCallback( (event) => { event.preventDefault(); const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect(); const type = event.dataTransfer.getData('application/reactflow'); // check if the dropped element is valid if (typeof type === 'undefined' || !type) { return; } const position = reactFlowInstance.project({ x: event.clientX - reactFlowBounds.left, y: event.clientY - reactFlowBounds.top, }); const newNode = { id: getId(), type, position, data: { label: `${type} node` }, }; setNodes((nds) => nds.concat(newNode)); }, [reactFlowInstance] ); // 点击节点,将节点初始配置传入 const onNodeClick = (e: any, node: any) => { console.log(node); setNodeInfo({ ...node.data, id: node.id, nodeBg: node.style && node.style.background ? node.style.background : '#ffffff', }); setNodeShow(true); }; // 修改完成,setNodes改变节点内容 const changeNode = (val: any) => { setNodes((nds) => nds.map((item) => { if (item.id === val.id) { item.data = val; item.hidden = val.isHidden; item.style = { background: val.nodeBg }; } return item; }), ); // console.log(val, nodes) }; return ( <div className="dndflow"> <ReactFlowProvider> <div className="reactflow-wrapper" ref={reactFlowWrapper}> <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes}
onNodeClick={onNodeClick} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} onInit={setReactFlowInstance} onDrop={onDrop} onDragOver={onDragOver} fitView > <UpdateNode info={nodeInfo} onChange={changeNode} /> // 修改节点内容组件 <Controls /> </ReactFlow> </div> <Sidebar /> // 添加栏的内容 </ReactFlowProvider> </div> ); }; export default DnDFlow;
nodeContent.tsx节点修改组件
import React, { useState, useEffect } from 'react'; import { Input, Switch } from 'antd'; export type nodeProps = { info: any; onChange: (val: any) => void; }; export default ({ info, onChange }: nodeProps) => { const [nodeInfo, setNodeInfo] = useState<any>({}); useEffect(() => { if (info.id) { // console.log(info) if (!info.isHidden) { info.isHidden = false; } setNodeInfo(info); } }, [info.id]); // 改变名称 const setNodeName = (value: string) => { setNodeInfo({ ...nodeInfo, label: value, }); onChange({ ...nodeInfo, label: value, }); }; // 改变背景色 const setNodeBg = (value: string) => { // console.log(value); setNodeInfo({ ...nodeInfo, nodeBg: value, }); onChange({ ...nodeInfo, nodeBg: value, }); }; // 是否隐藏 const setNodeHidden = (value: boolean) => { setNodeInfo({ ...nodeInfo, isHidden: value, }); onChange({ ...nodeInfo, isHidden: value, }); }; return nodeInfo.id ? ( <div className="updatenode__controls"> <label>名称:</label> {/* <input value={nodeInfo.label} onChange={(evt) => setNodeName(evt.target.value)} /> */} <Input placeholder="" value={nodeInfo.label} onChange={(evt) => setNodeName(evt.target.value)} /> <label className="updatenode__bglabel">背景色:</label> <Input type="color" value={nodeInfo.nodeBg} onChange={(evt) => setNodeBg(evt.target.value)} /> <div className="updatenode__checkboxwrapper"> <label>是否隐藏:</label> <Switch checked={nodeInfo.isHidden} onChange={setNodeHidden} /> </div> </div> ) : ( <></> ); };
连接线自定义与内容修改也是一样的操作步骤,
连接线效果图:
githup上传了完整的代码,有节点内容修改.