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

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

提供github地址供大家参考。地址: 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,Renderer从0到1,react.js,javascript,前端)