In-depth React Flow Renderer (3): Create and customize nodes (with code)

In React Flow Renderer, nodes are one of the basic building blocks for building workflow interfaces. This article explains how to create and customize different types of nodes to meet your workflow needs.

Provide the github address for your reference. Address: React-Flow-Renderer

introduction

Nodes are the core elements in a workflow, and they represent different operations, conditions, or data processing steps. In React Flow Renderer, nodes are draggable and configurable, allowing users to create custom nodes based on their needs.

Import necessary Redux hooks

When using Redux in a project, you need to create a reducer to manage state. The following is a sample Reducer that handles node-related state and operations

Declare 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
});

Create 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;
  }
}

Create node component

First, let's look at how to create a node component. Each node typically has its own features and configuration
options. In your example, we have a Indexnode component called node that receives different node types and data and renders them in different ways.

// /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);

The above code shows the basic structure of a node component, including the appearance and operation of the node.

Create node modal box

We hope that nodes can be edited and deleted. At this time, we need an editable modal box. The following is the code for a simple modal box.

First we create a custom form

// 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);

Then we create a modal box to contain this form

// 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);

Summarize

This article provides an in-depth introduction to how to create and customize nodes. In React Flow Renderer, nodes are a key part of building workflow interfaces. By customizing node components and registering node types, you can easily extend the functionality and appearance of your workflow to meet your project's needs.

In the next blog, we will continue to explore the canvas function and connecting line function of React Flow Renderer. Stay tuned!

Guess you like

Origin blog.csdn.net/m0_73117087/article/details/133357124