React FlowでControlsに独自のノード・エッジを追加する方法
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>
)
}
実際の画面
最後に
間違っていることがあれば、コメントに書いていただけると幸いです。
今年もありがとうございました。
皆さん良いお年を。
来年もよろしくお願いいたします。
Discussion