😄

React FlowでControlsに独自のノード・エッジを追加する方法

2024/12/31に公開

React FlowでControlsに独自のノード・エッジを追加する方法

React Flowでは独自のノードやエッジを追加することができる機能がある。これを使うと、クラス図みたいなカスタムノードも作って呼び出せたりする。その簡単な実装のメモ

React Flowとは

フローチャートや組織図などのチャートを簡単に描くことができるコンポーネント

公式ページより
A customizable React component for building node-based editors and interactive diagrams

実装

以下のページの実装に独自のノード、エッジを追加したいと思う。

ノード

Handleはカスタムノードを実装するときに使う組み込みコンポーネントで接続部分を定義する。TailwindCSSを使っていると、classNameを渡すことで簡単にスタイルを定義できるのですごく便利。

重要なpropsは以下の通り
source

接続元

target

接続先。もちろんtargetからsourceに接続することはできない。

position

接続部分の位置。以下の4つのPositionを指定できる。

  • Top
  • Bottom
  • Left
  • Right
const JobNode = memo(({ data }: NodeProps<Node>) => {
  return (
    <>
      <div
        className="relative px-6 py-3 rounded-sm border bg-slate-300"
        >
        <h1>Hello World</h1>
      </div>
      <Handle
        type='target'
        position={Position.Left}
        className='!-left-4 !size-3 !border !border-gray-600 !bg-white !rounded-[2px]'
      />
      <Handle
        type='source'
        position={Position.Right}
        className='!-right-4 !size-3 !border !border-gray-600 !bg-white !rounded-[2px]'
      />
    </>
  );
});

エッジ

BaseEdge
パスを表示する。デベロッパーツールで確認したがたしかにsvgが表示されていた。

EdgeLabelRenderer
エッジにラベルを追加することができる。このコンポーネントで囲んだ範囲がすべてラベルとして表示されるっぽい?

getSmoothStepPath
直角に曲がるエッジを表示するユーティリティ関数。エッジのtypeに応じたユーティリティ関数がいくつかある。

export function dame({
  id,
  sourceX,
  sourceY,
  targetX,
  targetY,
  sourcePosition,
  targetPosition,
  style = {},
  markerEnd,
}: EdgeProps) {
  const { setEdges } = useReactFlow();
  const [edgePath, labelX, labelY] = getSmoothStepPath({
    sourceX,
    sourceY,
    sourcePosition,
    targetX,
    targetY,
    targetPosition,
    borderRadius: 0,
  });

  const onDelete = () => {
    setEdges((es) => es.filter((e) => e.id !== id));
  }

  return (
    <>
      <BaseEdge path={edgePath} markerEnd={markerEnd} type="step" style={{ ...style }} className='!stroke-red-600 !z-10' />
      <EdgeLabelRenderer>
        <div
          className="nodrag nopan pointer-events-auto absolute"
          style={{
            transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
          }}
        >
          <h1 className="text-red-500">これは禁止なやつ</h1>
          <button onClick={() => onDelete()} className="px-3 py-1 rounded-sm text-white bg-red-600 hover:bg-red-700">解除</button>
        </div>
      </EdgeLabelRenderer>
    </>
  );
}

全体

スタイルはTailwindCSSを使って実装。layout.tsxでimport '@xyflow/react/dist/style.css';を呼び出しているが、TailwinsCSSを使う場合は最低限のCSSのみのimport '@xyflow/react/dist/base.css';を使う方が良さそう。じゃないと今回の実装のように!を使ってスタイルを上書きしなければいけない。

"use client"

import { addEdge, Background, BaseEdge, Connection, Controls, Edge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath, Handle, Node, NodeProps, Position, ReactFlow, SelectionMode, useEdgesState, useNodesState, useReactFlow } from "@xyflow/react"
import { memo, useCallback } from "react";

const initialNodes = [
  {
    id: '1',
    position: { x: 0, y: 0 },
    data: { label: 'Hello' },
    type: 'input',
  },
  {
    id: '2',
    position: { x: 100, y: 100 },
    data: { label: 'World' },
  },
  {
    id: '3',
    position: { x: 200, y: 200 },
    data: { label: 'World' },
    type: "job"
  },
];

const initialEdges = [{ id: '1-2', source: '1', target: '2' }, { id: '2-3', source: '2', target: '3', type: "dame" }];

export function dame({
  id,
  sourceX,
  sourceY,
  targetX,
  targetY,
  sourcePosition,
  targetPosition,
  style = {},
  markerEnd,
}: EdgeProps) {
  const { setEdges } = useReactFlow();
  const [edgePath, labelX, labelY] = getSmoothStepPath({
    sourceX,
    sourceY,
    sourcePosition,
    targetX,
    targetY,
    targetPosition,
    borderRadius: 0,
  });

  const onDelete = () => {
    setEdges((es) => es.filter((e) => e.id !== id));
  }

  return (
    <>
      <BaseEdge path={edgePath} markerEnd={markerEnd} style={{ ...style }} className='!stroke-red-600 !z-10' />
      <EdgeLabelRenderer>
        <div
          className="nodrag nopan pointer-events-auto absolute"
          style={{
            transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
          }}
        >
          <h1 className="text-red-500">これは禁止なやつ</h1>
          <button onClick={() => onDelete()} className="px-3 py-1 rounded-sm text-white bg-red-600 hover:bg-red-700">解除</button>
        </div>
      </EdgeLabelRenderer>
    </>
  );
}

const JobNode = memo(({ data }: NodeProps<Node>) => {
  return (
    <>
      <div
        className="relative px-6 py-3 rounded-sm border bg-slate-300"
        >
        <h1>Hello World</h1>
      </div>
      <Handle
        type='target'
        position={Position.Left}
        className='!-left-4 !size-3 !border !border-gray-600 !bg-white !rounded-[2px]'
      />
      <Handle
        type='source'
        position={Position.Right}
        className='!-right-4 !size-3 !border !border-gray-600 !bg-white !rounded-[2px]'
      />
    </>
  );
});

export const Flow = () => {
  const [nodes, setNodes, onNodesChange] = useNodesState<Node>(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>(initialEdges);

  const onConnect = useCallback(
    (connection: Connection) => {
      const sourceNode = nodes.find(node => node.id === connection.source);
      const targetNode = nodes.find(node => node.id === connection.target);

      if (!sourceNode || !targetNode) return false;

      if (sourceNode.id === "2" && targetNode.id === "3") {
        setEdges((eds) => addEdge({ ...connection, type: "dame" }, eds))
      } else {
        setEdges((eds) => addEdge(connection, eds))
      }
    },
    [setEdges],
  );

  const nodeTypes = {
    job: JobNode,
  };
  
  const edgeTypes = {
    dame: dame,
  };

  return (
    <ReactFlow
      nodes={nodes}
      edges={edges}
      onNodesChange={onNodesChange}
      onEdgesChange={onEdgesChange}
      onConnect={onConnect}
      nodeTypes={nodeTypes}
      edgeTypes={edgeTypes}
      panOnScroll
      selectionOnDrag
      panOnDrag={[1, 2]}
      selectionMode={SelectionMode.Partial}
      >
      <Background />
      <Controls />
    </ReactFlow>
  )
}

実際の画面

実際の画面

最後に

間違っていることがあれば、コメントに書いていただけると幸いです。
今年もありがとうございました。
皆さん良いお年を。
来年もよろしくお願いいたします。

GitHubで編集を提案

Discussion