5

This is my first introduction to react-flow. I am looking to create a custom node where after creation, the user can enter information in the node and save/display it. From the react-flow documentation on custom nodes, they have a similar example where they created a TextUpdaterNode that console.logs the user input.

Instead of logging it it via console, I am looking for a way to save the information to the node itself and display it on the node. For example, if a user were to enter "24, male" into the input and hit the "enter" key, I want the node to be updated with that information.

What are the ways I can go about doing this?

Ed Lucas
  • 5,955
  • 4
  • 30
  • 42
Peek0
  • 173
  • 2
  • 9
  • You could try to create a state within the TextUpdater instead of console logging. I'll try to create an example later today. – Moa Sep 08 '22 at 20:36

3 Answers3

4

What you're trying to do needs a little more than that:

You can see alive example here: https://codesandbox.io/s/dank-waterfall-8jfcf4?file=/src/App.js

Basically, you need:

  • Import useNodesState from 'react-flow-renderer';
  • Instead of basic definition of nodes, you will need to use: const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  • Then, will have to define the onAdd, which looks like:
 const onAdd = useCallback(() => {
   const newNode = {
     id: getNodeId(),
     data: { label: `${state.name} (${state.age})` },
     position: {
       x: 0,
       y: 0 + (nodes.length + 1) * 20
     }
   };
   setNodes((nds) => nds.concat(newNode));
 }, [nodes, setNodes, state.name, state.age]);
  • You can include edit, pretty similar like:
  const onEdit = () => {
    setNodes((nds) =>
      nds.map((node) => {
        if (node.id === editState.id) {
          node.data = {
            ...node.data,
            label: `${node.id} - ${editState.name} (${editState.age})`
          };
        }

        return node;
      })
    );
  };
  • Finally, draw the flow: <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} />

The whole code looks like:

import React, { useState, useCallback } from "react";
import ReactFlow, {
  ReactFlowProvider,
  useNodesState,
  useEdgesState
} from "react-flow-renderer";

import "./styles.css";

const getNodeId = () => `randomnode_${+new Date()}`;

const initialNodes = [
  { id: "1", data: { label: "Node 1" }, position: { x: 100, y: 100 } },
  { id: "2", data: { label: "Node 2" }, position: { x: 100, y: 200 } }
];

const initialEdges = [{ id: "e1-2", source: "1", target: "2" }];

const FlowExample = () => {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges] = useEdgesState(initialEdges);
  const [state, setState] = useState({ name: "", age: "" });

  const onAdd = useCallback(() => {
    const newNode = {
      id: getNodeId(),
      data: { label: `${state.name} (${state.age})` },
      position: {
        x: 0,
        y: 0 + (nodes.length + 1) * 20
      }
    };
    setNodes((nds) => nds.concat(newNode));
  }, [nodes, setNodes, state.name, state.age]);

  return (
    <div>
      Name:{" "}
      <input
        type="text"
        onChange={(e) => {
          setState((prev) => ({ ...prev, name: e.target.value }));
        }}
      />
      Age:{" "}
      <input
        type="text"
        onChange={(e) => {
          setState((prev) => ({ ...prev, age: e.target.value }));
        }}
      />
      <button onClick={onAdd}>add node</button>
      <div style={{ width: "500px", height: "500px" }}>
        <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} />
      </div>
    </div>
  );
};

export default () => (
  <ReactFlowProvider>
    <FlowExample />
  </ReactFlowProvider>
);

Also, with edit:

import React, { useState, useCallback } from "react";
import ReactFlow, {
  ReactFlowProvider,
  useNodesState,
  useEdgesState
} from "react-flow-renderer";

import "./styles.css";

const getNodeId = () => `${String(+new Date()).slice(6)}`;

const initialNodes = [
  { id: "1", data: { label: "Node 1" }, position: { x: 100, y: 100 } },
  { id: "2", data: { label: "Node 2" }, position: { x: 100, y: 200 } }
];

const initialEdges = [{ id: "e1-2", source: "1", target: "2" }];

const FlowExample = () => {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges] = useEdgesState(initialEdges);
  const [state, setState] = useState({ name: "", age: "" });
  const [editState, setEditState] = useState({ id: "", name: "", age: "" });

  const onEdit = () => {
    setNodes((nds) =>
      nds.map((node) => {
        if (node.id === editState.id) {
          node.data = {
            ...node.data,
            label: `${node.id} - ${editState.name} (${editState.age})`
          };
        }

        return node;
      })
    );
  };

  const onAdd = () => {
    const id = getNodeId();
    const newNode = {
      id,
      data: { label: `${id} - ${state.name} (${state.age})` },
      position: {
        x: 0,
        y: 0 + (nodes.length + 1) * 20
      }
    };
    setNodes((nds) => nds.concat(newNode));
  };

  return (
    <div>
      Name:{" "}
      <input
        type="text"
        onChange={(e) => {
          setState((prev) => ({ ...prev, name: e.target.value }));
        }}
      />
      Age:{" "}
      <input
        type="text"
        onChange={(e) => {
          setState((prev) => ({ ...prev, age: e.target.value }));
        }}
      />
      <button onClick={onAdd}>add node</button>
      <br />
      Id:{" "}
      <input
        type="text"
        onChange={(e) => {
          setEditState((prev) => ({ ...prev, id: e.target.value }));
        }}
      />
      Name:{" "}
      <input
        type="text"
        onChange={(e) => {
          setEditState((prev) => ({ ...prev, name: e.target.value }));
        }}
      />
      Age:{" "}
      <input
        type="text"
        onChange={(e) => {
          setEditState((prev) => ({ ...prev, age: e.target.value }));
        }}
      />
      <button onClick={onEdit}>Edit node</button>
      <div style={{ width: "500px", height: "500px" }}>
        <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} />
      </div>
    </div>
  );
};

export default () => (
  <ReactFlowProvider>
    <FlowExample />
  </ReactFlowProvider>
);

A more helpful example from documentation would be:

But you have to remove all the extra information (Also, you can use it to go deeper!)

joseglego
  • 2,011
  • 1
  • 17
  • 28
  • I realised I did this in a slightly different way from yours. I basically created a custom node object that allowed me to save more information under the nodes `data` property. Once I figured out how to do that and write the relevant code to write it, I just had to leverage some `react` code to ensure that the updates would be displayed. A difference I saw between our implementations is how I can make the input forms for my nodes be displayed within the node itself. – Peek0 Sep 13 '22 at 11:36
1

Custom Node

I managed to come up a solution to create such a custom node that allowed you to input, save and display information. I have tried to include relevant information and the code block I used below.

Custom Node

import { useCallback } from 'react';
import { Handle, Position} from 'react-flow-renderer';
const handleStyle = { left: 10 };
//Custom node requires props of data to be passed to it.
function CustomNode({ data }) {    
    let serviceType = "offered";
    //This handles pressing enter inside the description
    const handleKeyDown = (evt) => {
        if (evt.key === "Enter") {
            //Check if empty string
            if (evt.target.value.length !== 0) {
                //This code is because services are either offered or borrowed.
              if (serviceType === "offered") {
                data.serviceOffered.push(evt.target.value);
              } else if (serviceType === "borrowed") {
                data.serviceBorrowed.push(evt.target.value);
              }
                //Clearing input after pressing enter
              evt.currentTarget.value = "";
            }
        }

  };
  const onChange = useCallback((evt) => {
    //Update service type without pressing enter
    serviceType = evt.target.value;
  });

  return (
    <div className="text-updater-node">
      <Handle type="target" position={Position.Top} />
      <div>
        <p>Entity</p>
        <label htmlFor="text"><p className='nodeTitle'>{data.label}</p></label>
        <input id="text" name="text" onKeyDown={handleKeyDown} /> 
        <select name="type" onChange={onChange}>
          <option value="offered" >Offered </option>
          <option value="borrowed">Borrowed</option>
        </select>
        <div className="info">
            {/* This is where the description information is displayed. It checks if it is empty, if not it loops through and displays it.  */}
            <h2>Service Borrowed</h2>
            <ul>
              {data.serviceBorrowed.length? data.serviceBorrowed.map(service => (<li key={service}>{service}</li>)) : <span></span>} 
            </ul>
            <h2>Service Offered</h2> 
            <ul>
              {data.serviceOffered.length? data.serviceOffered.map(service => (<li key={service}>{service}</li>)) : <span></span>}
            </ul>
        </div>
      </div>
      <Handle type="source" position={Position.Bottom} id="a" style={handleStyle} />
      <Handle type="source" position={Position.Bottom} id="b" />
    </div>
  );
}
export default CustomNode;

I have a parent reactFlow component with the following code block. The important thing about this is to set the custom node type of react flow and pass in an object containing information about the nodes and edges to be rendered.

import { Fragment, useCallback, useState } from "react";
import ReactFlow, {
  addEdge,
  applyEdgeChanges,
  applyNodeChanges,
} from "react-flow-renderer";
import initialNodes from "../data/nodes"; //This both ended up being empty file
import initialEdges from "../data/edges"; //This both ended up being empty file
import CustomNode from "./customNode";
import "./customNode.css";
//Set nodetype as Custom node, IMPORTANT!
const nodeTypes = { customNode: CustomNode };

function Flow() {
  const defaultEdgeOptions = { animated: true };
  //Input Elements
  const [name, setName] = useState("");
  const addNode = () => {
    setNodes((e) =>
      e.concat({
        id: (e.length + 1).toString(),
        data: { label: `${name}`, serviceOffered: [], serviceBorrowed: [] },
        position: { x: 0, y: 0 },
        type: "customNode",
      })
    );
  };
  //Nodes and edges containing information of the nodes and edges
  const [nodes, setNodes] = useState(initialNodes);
  const [edges, setEdges] = useState(initialEdges);
  //Boiler plate code for reactFlow
  const onNodesChange = useCallback(
    (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
    [setNodes]
  );
  const onEdgesChange = useCallback(
    (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
    [setEdges]
  );
  const onConnect = useCallback(
    (connection) => setEdges((eds) => addEdge(connection, eds)),
    [setEdges]
  );

  return (
    <Fragment>
      <Row>
        <Col lg={9}>
          <ReactFlow
            className="Canvas mt-1 border border-secondary rounded"
            nodes={nodes} //Node information is passed here
            edges={edges} //Edges information is passed here
            onNodesChange={onNodesChange}
            onEdgesChange={onEdgesChange}
            onConnect={onConnect}
            defaultEdgeOptions={defaultEdgeOptions}
            style={{ width: "100%", height: "80vh" }}
            fitView
            nodeTypes={nodeTypes}
          />
        </Col>
      </Row>
    </Fragment>
  );
}

export default Flow;

I added more information inside the data property of my node.js. It ended up being initialize as empty but this template should be helpful in understanding how I saved the information for the node. The edge followed the standard format shown on react-flow documentation.

export default [
    // {
    //   id: '1',
    //   type: 'customNode',
    //   data: { label: 'Input Node', info: [{id:1, action:"Everything is burning"}, {id:2, action:"I'm fine"}], noOfActions:2 },
    //   position: { x: 250, y: 25 },
    // },
  ];

I hope this has been useful!

Peek0
  • 173
  • 2
  • 9
  • I use this for save information in node, i want to save form data in node.I set ` data: { label: `${type} node`, attribute: { type: 0 } }` for add node, but in custom node, when i wan to set new data `setFormData(values => ({ ...values, [name]: value })) data.attribute = formData;` get error :`"attribute" is read-only` – ar.gorgin Dec 24 '22 at 06:36
1

Accepted answer is about modifying properties of components which is not React way. That code may break easily. There are other ways to bring callback to custom nodes.

  1. Put callback into node's data

This is from React flow documentation: https://reactflow.dev/docs/examples/nodes/custom-node/

    setNodes([
      ...
      {
        id: '2',
        type: 'selectorNode',
        data: { onChange: onChange, color: initBgColor },
      ...

Cons: you need pay extra attention when you modify or create new nodes dynamically

  1. or Define custom types dynamically

In this approach, you keep node data and behavior concerns separate.

I'm using TypeScript in order to show types of data we operate along the way.

First, you extend your custom node properties with your callback:

import {NodeProps} from "react-flow-renderer/dist/esm/types/nodes";

// by default, custom node is provisioned with NodeProps<T>
// we extend it with additional property
export type CustomNodeProps = NodeProps<CustomData> & {
  onClick: (id: string) => void
}

function CustomNode(props: CustomNodeProps) {
  return <button onClick={() => props.onClick(props.id)}>Do it</button>
}

Then you create new constructor that provides callback and put it into custom nodes mapping using memoization:

function Flow() {
  const [graph, dispatchAction] = useReducer(...);
  ...

  // useMemo is neccessary https://reactflow.dev/docs/guides/troubleshooting/#it-looks-like-you-have-created-a-new-nodetypes-or-edgetypes-object-if-this-wasnt-on-purpose-please-define-the-nodetypesedgetypes-outside-of-the-component-or-memoize-them
  const nodeTypes = useMemo(() => {
    return {
      custom: (props: NodeProps<CustomData>) => {
        return CustomNode({...props, onClick: (id: string) => {
          dispatchAction({
            type: 'customNodeButtonClicked',
            nodeId: id,
          })
        }})
      }
    }
  }, [])

  return (
    <>
      <ReactFlow nodeTypes={nodeTypes} ... />
    </>
  );
}
Hasselbach
  • 136
  • 2
  • 6