深入React Flow Renderer(三):创建和定制节点(附代码)

本文详细讲解了如何在ReactFlowRenderer中创建和定制节点,包括Redux集成、状态管理、节点组件开发以及模态框的使用,以实现工作流界面的扩展和个性化。
摘要由CSDN通过智能技术生成

在React Flow Renderer中,节点是构建工作流界面的基本组成部分之一。本文将介绍如何创建和定制不同类型的节点,以满足您的工作流需求。

我已将当前系列的Demo上传至GitHub,其中包含一个简单的画布、侧边栏、内容编辑以及其他功能。这个Demo旨在供大家参考和学习。地址: Evanzew/React-Flow-Renderer

引言

节点是工作流中的核心元素,它们代表了不同的操作、条件或数据处理步骤。在React Flow Renderer中,节点是可拖动和可配置的,允许用户根据其需求创建自定义的节点。

导入必要的Redux钩子

在项目中使用Redux时,需要创建一个reducer来管理状态。以下是一个示例Reducer,用于处理与节点相关的状态和操作

声明redux的action

// consatants.ts 

// 规则链
export const OPEN_RULE_CHAIN_MODAL = 'OPEN_RULE_CHAIN_MODAL'; // 打开规则链模态框的动作类型
export const CLOSE_RULE_CHAIN_MODAL = 'CLOSE_RULE_CHAIN_MODAL'; // 关闭规则链模态框的动作类型
export const SET_RULE_CHAIN_NODE = 'SET_RULE_CHAIN_NODE'; // 设置规则链节点的动作类型
// ruleChainAction.ts

import * as Actions from '../constant';
// 打开模态框的动作
export const openModal = (data: any) => ({
  type: Actions.OPEN_RULE_CHAIN_MODAL,
  data
});

// 关闭模态框的动作
export const closeModal = (data: any) => ({
  type: Actions.CLOSE_RULE_CHAIN_MODAL,
  data
});

// 设置规则链节点的动作
export const setRuleChainNode = (data: any) => ({
  type: Actions.SET_RULE_CHAIN_NODE,
  data
});

创建reducer

// ruleChainReducer.ts

import { CLOSE_RULE_CHAIN_MODAL, OPEN_RULE_CHAIN_MODAL, SET_RULE_CHAIN_NODE } from '../constant';

const initState = {
  nodes: [],
  // 弹窗信息
  modalConfig: {
    visible: false,
    node: null
  }
};

export default function ruleChainReducer(
  state = {
    ...initState
  },
  action: { type: any; data: any }
) {
  // 从action对象中获取:type,data
  const { type, data } = action;

  // 根据type决定加工数据
  switch (type) {
  case OPEN_RULE_CHAIN_MODAL:
    return {
      ...state,
      modalConfig: {
        visible: true,
        node: data
      }
    };
  case CLOSE_RULE_CHAIN_MODAL:
    return {
      ...state,
      modalConfig: {
        visible: false,
        node: null
      }
    };
  case SET_RULE_CHAIN_NODE:
    return {
      ...state,
      nodes: data
    };
  default:
    return state;
  }
}

创建节点组件

首先,让我们看一下如何创建一个节点组件。每个节点通常都有自己的特性和配
置选项。在你的示例中,我们有一个名为Index的节点组件,它接收不同的节点类型和数据,并以不同的方式呈现它们。

// /Node/index.tsx

import React, { useState } from 'react';
import { Handle, Position, useNodes, useReactFlow } from 'react-flow-renderer';
import { IconButton, Menu, MenuItem } from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { useDispatch } from 'react-redux';
import { OPEN_RULE_CHAIN_MODAL, SET_RULE_CHAIN_NODE } from '@/store/constant';

interface EditMenuProps {
  anchorEl: HTMLButtonElement | null;
  open: boolean;
  handleClose: () => void;
  node: any;
}

// 编辑菜单
const EditMenu = (props: EditMenuProps) => {
  const dispatch = useDispatch();
  const { setNodes } = useReactFlow();
  const nodes = useNodes();
  const { anchorEl, open, handleClose, node } = props;

  const edit = () => {
    // 打开编辑模态框
    dispatch({
      type: OPEN_RULE_CHAIN_MODAL,
      data: node
    });
    handleClose();
  };

  const remove = () => {
    setNodes(nodes.filter((item) => item.id !== node.id));
    // 更新节点对象并分发action
    dispatch({
      type: SET_RULE_CHAIN_NODE,
      data: nodes.filter((item) => item.id !== node.id)
    });
  };

  return (
    <Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
      <MenuItem key="1" onClick={edit}>
        Edit
      </MenuItem>
      <MenuItem key="2" onClick={remove}>
        Delete
      </MenuItem>
    </Menu>
  );
};

export const NodeType = {
  relation: 'relation',
  input: 'input',
  filter: 'filter',
  action: 'action',
  flow: 'FLOW'
};

const Index = (props: any) => {
  const { ...currentNode } = props;
  // Menu用的方法
  const [anchorEl, setAnchorEl] = useState(null);
  const open = Boolean(anchorEl);
  const handleClick = (event: any) => {
    setAnchorEl(event.currentTarget);
  };
  const handleClose = () => {
    setAnchorEl(null);
  };

  const classMap = new Map([
    ['ACTION', 'relation-node'],
    ['input', 'input-node'],
    ['FILTER', 'filter-node'],
    ['ENRICHMENT', 'enrichment-node'],
    ['TRANSFORMATION', 'transformation-node'],
    ['EXTERNAL', 'external-node'],
    ['FLOW', 'flow-node']
  ]);

  return (
    <div
      className={`relation-node ${
        classMap.get(currentNode.type) || 'default-node'
      }`}
    >
      <div className="relation-node-title">
        {currentNode.type !== NodeType.input && currentNode.data.label &&
          <>
            {currentNode.data.label}
            <br />
          </>
        }
        {currentNode.data.name}
      </div>
      <div
        className="relation-node-action"
        style={{
          display: 'flex',
          alignItems: 'flex-end',
          justifyContent: 'center'
        }}
      >
        <IconButton aria-label="delete" size="small" onClick={handleClick}>
          <MoreVertIcon fontSize="inherit" />
        </IconButton>
        <EditMenu
          anchorEl={anchorEl}
          open={open}
          handleClose={handleClose}
          node={currentNode}
          {...props}
        />
      </div>
      {/* 提供一个入口和一个出口 */}
      {currentNode.type !== NodeType.input &&
        <Handle
          type="target"
          position={Position.Left}
          isConnectable={currentNode.isConnectable}
        />
      }
      <Handle
        type="source"
        position={Position.Right}
        isConnectable={currentNode.isConnectable}
      />
    </div>
  );
};

export default React.memo(Index);

上述代码展示了一个节点组件的基本结构,其中包括节点的外观和操作。

创建节点模态框

我们希望节点能够编辑和删除,这时候我们就需要一个可编辑的模态框,下面是一个简单的模态框的代码。

首先我们创建一个自定义的表单

// modal/RelationNodeForm.tsx
import React, { useImperativeHandle } from 'react';
import { connect, useDispatch } from 'react-redux';
import { useForm } from 'react-hook-form';
import {
  FormContainer,
  TextFieldElement,
  TextareaAutosizeElement
} from 'react-hook-form-mui';
import { useNodes, useReactFlow } from 'react-flow-renderer';
import { SET_RULE_CHAIN_NODE } from '@/store/constant';

interface RelationNodeFormProps {
  modalConfig: any;
  events: any;
  nodes: any;
}

function Index(props: RelationNodeFormProps) {
  const { modalConfig, events, nodes } = props; // 从props中解构获取所需的变量
  const { setNodes } = useReactFlow(); // 使用useReactFlow钩子获取setNodes函数
  const flowNodes = useNodes(); // 使用useNodes钩子获取当前节点列表
  const initialValues = nodes.find(
    (node: any) => node.id === modalConfig.node?.id
  ); // 根据modalConfig中的node.id查找对应的初始值
  const dispatch = useDispatch(); // 获取dispatch函数
  const formContext = useForm<any>({
    defaultValues: {
      name: '',
      remark: ''
    },
    mode: 'all' // 验证模式切换为all
  });

  /**
   * 构建更新后的节点对象
   * @param {any} data - 表单数据
   * @param {any} node - 节点数据
   */
  function buildUpdateNode(data: any, node: any) {
    return {
      ...node,
      name: data.name,
      description: data.remark
    };
  }

  function submit() {
    // 获取表单数据
    const data = formContext.watch();

    // 更新节点对象并分发action
    dispatch({
      type: SET_RULE_CHAIN_NODE,
      data: nodes.map((node: any) =>
        node.id === modalConfig.node.id ? buildUpdateNode(data, node) : node
      )
    });

    // 更新节点数组
    setNodes(
      flowNodes.map((node: any) =>
        node.id === modalConfig.node.id ? buildUpdateNode(data, node) : node
      )
    );
  }

  // 暴露submit的方法
  useImperativeHandle(
    events,
    () => {
      return {
        submit
      };
    },
    []
  );

  React.useEffect(() => {
    formContext.reset({
      name: initialValues?.data.name,
      remark: initialValues?.additionalInfo.description
    });

  }, []);

  return (
    <FormContainer formContext={formContext}>
      {/* 节点名称 */}
      <TextFieldElement
        required
        margin="normal"
        fullWidth
        label={'name'}
        name="name"
        size="small"
        variant="outlined"
      />
      {/* 节点描述 */}
      <TextareaAutosizeElement
        rows={2}
        margin="normal"
        fullWidth
        label={'description'}
        name="remark"
        size="small"
        variant="outlined"
      />
    </FormContainer>
  );
}

// redux获取当前flow的数据
const mapStateToProps = (state: any) => {
  const { modalConfig, nodes } = state.ruleChainReducer;

  return {
    modalConfig,
    nodes
  };
};

export default connect(mapStateToProps)(Index);

接着我们创建一个模态框来包含这个表单

// modal/Index.tsx
// Modal/index.jsx

import React, { useRef } from 'react';
import RelationNodeForm from './RelationNodeForm';
import { connect, useDispatch } from 'react-redux';
import { EnhancedDialog } from '@/components/EnhancedDialog';
import { CLOSE_RULE_CHAIN_MODAL } from '@/store/constant';

interface ModalProps {
  modalConfig: any;
}

export function Index(props: ModalProps) {
  const formRef = useRef();
  const { modalConfig } = props;
  const dispatch = useDispatch();
  const Component = RelationNodeForm;
  const handleOk = () => {
    const current = formRef.current as any;

    // 组件内部需要暴露一个 submit 方法
    current?.submit();
    dispatch({ type: CLOSE_RULE_CHAIN_MODAL });
  };
  const handleCancel = () => dispatch({ type: CLOSE_RULE_CHAIN_MODAL });

  return (
    //这是自定义模态框,你可以使用你熟悉或者项目中使用的组件
    <EnhancedDialog
      title={`${modalConfig.node?.type} - ${modalConfig.node?.data.label}`}
      visible={modalConfig.visible}
      onOk={handleOk}
      onCancel={handleCancel}
      maxWidth="xs"
    >
      {Component && <Component events={formRef} />}
    </EnhancedDialog>
  );
}

// redux获取当前flow的数据
const mapStateToProps = (state: any) => {
  const { modalConfig } = state.ruleChainReducer;

  return {
    modalConfig
  };
};

export default connect(mapStateToProps)(Index);

总结

本文深入介绍了如何创建和定制节点,在React Flow Renderer中,节点是构建工作流界面的关键部分。通过定制节点组件和注册节点类型,您可以轻松扩展工作流的功能和外观,以满足项目的需求。

在下一篇博客中,我们将继续探讨React Flow Renderer的画布功能和连接线功能。敬请期待!

以下是React Flow的一个基本示例,该示例使用自定义节点和连线样式,以及可拖动的节点和连线。你可以根据自己的需要进行修改和扩展。 ```jsx import React, { useState } from 'react'; import ReactFlow, { ReactFlowProvider, addEdge, removeElements, Controls, Background, } from 'react-flow-renderer'; const initialElements = [ { id: '1', type: 'input', data: { label: 'Input Node' }, position: { x: 250, y: 5 } }, { id: '2', type: 'default', data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, { id: '3', type: 'default', data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, { id: '4', type: 'output', data: { label: 'Output Node' }, position: { x: 250, y: 200 } }, { id: 'e1-2', source: '1', target: '2', animated: true }, { id: 'e2-3', source: '2', target: '3', animated: true }, { id: 'e3-4', source: '3', target: '4', animated: true }, ]; const nodeTypes = { input: ({ data }) => ( <div className="node-input"> {data.label} </div> ), default: ({ data }) => ( <div className="node-default"> {data.label} </div> ), output: ({ data }) => ( <div className="node-output"> {data.label} </div> ), }; const App = () => { const [elements, setElements] = useState(initialElements); const onElementsRemove = (elementsToRemove) => setElements((els) => removeElements(elementsToRemove, els)); const onConnect = (params) => setElements((els) => addEdge(params, els)); return ( <ReactFlowProvider> <div className="react-flow-wrapper"> <ReactFlow elements={elements} onElementsRemove={onElementsRemove} onConnect={onConnect} nodeTypes={nodeTypes} snapToGrid={true} snapGrid={[15, 15]} > <Controls /> <Background color="#888" gap={16} /> </ReactFlow> </div> </ReactFlowProvider> ); }; export default App; ``` 在上面的示例中,我们定义了一个最初包含四个节点个连接的元素数组。我们还定义了节点类型,分别是输入节点、默认节点和输出节点。 在ReactFlow组件中,我们将元素和节点类型传递给它,并指定了一些其他属性,如onElementsRemove和onConnect,用于删除和添加元素,以及snapToGrid和snapGrid,用于将节点对齐到网格上。 最后,我们使用ReactFlowProvider包装ReactFlow组件,以便在整个应用程序中共享ReactFlow的状态和功能。 如果你想要一个更复杂的示例,可以访问React Flow的官方网站,那里有许多其他示例和文档。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值