Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.
"use client";
import "@xyflow/react/dist/style.css";
import {
addEdge,
applyEdgeChanges,
applyNodeChanges,
Background,
type Connection,
type EdgeChange,
type Node,
type NodeChange,
type NodeProps,
type NodeTypes,
Position,
ReactFlow,
ReactFlowProvider,
useUpdateNodeInternals,
} from "@xyflow/react";
import { nanoid } from "nanoid";
import { useCallback, useState } from "react";
import { Button } from "@/components/ui/button";
import {
EditableHandle,
EditableHandleDialog,
} from "@/components/ui/flow/editable-handle";
const DynamicHandlesNode = ({ id }: NodeProps<Node>) => {
const updateNodeInternals = useUpdateNodeInternals();
const [handles, setHandles] = useState<
{ id: string; name: string; description?: string }[]
>([
{
id: "1",
name: "input1",
description: "Input 1 description",
},
]);
const handleCreate = useCallback(
(name: string, description?: string) => {
console.log("New handle", name, description);
const newHandle = {
id: `handle-${nanoid()}`,
name,
description,
};
setHandles((prev) => [...prev, newHandle]);
updateNodeInternals(id);
return true;
},
[id, updateNodeInternals],
);
const handleUpdate = useCallback(
(handleId: string, newName: string, newDescription?: string) => {
setHandles((prev) =>
prev.map((handle) =>
handle.id === handleId
? {
...handle,
name: newName,
description: newDescription,
}
: handle,
),
);
return true;
},
[],
);
const handleDelete = useCallback(
(handleId: string) => {
setHandles((prev) =>
prev.filter((handle) => handle.id !== handleId),
);
updateNodeInternals(id);
},
[id, updateNodeInternals],
);
return (
<div className="py-4 border rounded-lg bg-background w-[300px]">
<h2 className="text-lg font-semibold mb-4 px-4">
Node with dynamic Handles
</h2>
<EditableHandleDialog
variant="create"
label=""
onSave={handleCreate}
onCancel={() => {}}
align="start"
>
<Button variant="outline" size="sm" className="h-8 ml-4">
Create New Handle
</Button>
</EditableHandleDialog>
<div className="mt-4 space-y-2">
{handles.map((handle) => (
<div key={handle.id} className="flex items-center gap-2">
<EditableHandle
nodeId={id}
handleId={handle.id}
name={handle.name}
description={handle.description}
type="target"
position={Position.Left}
wrapperClassName="w-full"
onUpdateTool={handleUpdate}
onDelete={handleDelete}
/>
</div>
))}
</div>
</div>
);
};
const nodeTypes: NodeTypes = {
"editable-handle-node": DynamicHandlesNode,
};
const initialNodes = [
{
id: "node-1",
type: "editable-handle-node",
position: { x: 0, y: 0 },
data: {},
},
];
export function EditableHandleDemo() {
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState([]);
const defaultViewport = { x: 100, y: 150, zoom: 1.1 };
const onNodesChange = useCallback(
(changes: NodeChange<Node>[]) =>
setNodes((nds) => applyNodeChanges(changes, nds)),
[],
);
const onEdgesChange = useCallback(
(changes: EdgeChange<never>[]) =>
setEdges((eds) => applyEdgeChanges(changes, eds)),
[],
);
const onConnect = useCallback(
(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
[],
);
return (
<div className="w-full h-full">
<ReactFlowProvider>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
defaultViewport={defaultViewport}
>
<Background />
</ReactFlow>
</ReactFlowProvider>
</div>
);
}
Overview
The Editable Handle component makes it easy to create nodes with dynamic connection points (handles). Instead of having fixed inputs and outputs, your nodes can:
- Let users add new input/output handles on the fly
- Edit handle labels and descriptions
- Remove handles when they're no longer needed
This is particularly useful when building nodes that need to adapt to different use cases or when you want to give users more flexibility in how nodes connect to each other.
Installation
pnpm dlx shadcn@latest add @simple-ai/editable-handle
Usage
The Editable Handle system has two main parts:
EditableHandleDialog: Used to create new handles with a user-friendly popupEditableHandle: The handle component itself that users can edit or delete
Adding New Handles
Use EditableHandleDialog to let users create new handles. It provides a simple interface for entering a handle's label and optional description.
import { EditableHandleDialog } from "@/components/ui/flow/editable-handle"
function MyNode() {
const handleCreate = (name: string, description?: string) => {
// Add the new handle to your state
return true // Return true if handle was created successfully
}
return (
<EditableHandleDialog
variant="create"
onSave={handleCreate}
align="start"
>
<Button variant="outline" size="sm">
Add New Input
</Button>
</EditableHandleDialog>
)
}Managing Handles
Use EditableHandle to display handles that users can edit or remove.
import { EditableHandle } from "@/components/ui/flow/editable-handle"
import { Position } from "@xyflow/react"
function MyNode({ id }: NodeProps) {
const handleUpdate = (handleId: string, newName: string, newDescription?: string) => {
// Update your handle state
return true // Return true if update was successful
}
const handleDelete = (handleId: string) => {
// Remove the handle from your state
}
return (
<EditableHandle
nodeId={id}
handleId="handle-1"
name="Input 1"
description="Optional description"
type="target"
position={Position.Left}
onUpdateTool={handleUpdate}
onDelete={handleDelete}
showDescription
/>
)
}