Skip to content

Commit 6247ec4

Browse files
committed
feat: adds moving-handles example
1 parent 85a364c commit 6247ec4

File tree

14 files changed

+216
-42
lines changed

14 files changed

+216
-42
lines changed

playground/constants.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Interaction,
1414
InteractiveMinimap,
1515
Intersections,
16+
MovingHandles,
1617
NodeResizer,
1718
NodeToolbar,
1819
Overview,
@@ -44,6 +45,7 @@ export const SolidFlowExamplesMap = {
4445
Interaction,
4546
InteractiveMinimap,
4647
Intersections,
48+
MovingHandles,
4749
NodeResizer,
4850
NodeToolbar,
4951
Overview,

playground/examples/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export { HandleConnect } from "./handle-connect/page";
1212
export { Interaction } from "./interaction/page";
1313
export { InteractiveMinimap } from "./interactive-minimap/page";
1414
export { Intersections } from "./intersections/page";
15+
export { MovingHandles } from "./moving-handles/page";
1516
export { NodeResizer } from "./node-resizer/page";
1617
export { NodeToolbar } from "./node-toolbar/page";
1718
export { Overview } from "./overview/page";
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Position } from "@xyflow/system";
2+
import type { JSX } from "solid-js";
3+
4+
import { Handle, useConnection } from "@/index";
5+
import type { NodeProps } from "@/types";
6+
7+
const sourceHandleStyle: JSX.CSSProperties = {
8+
position: "relative",
9+
transform: "translate(-50%, 0)",
10+
top: 0,
11+
transition: "transform 0.5s",
12+
};
13+
14+
export const MovingHandleNode = (_props: NodeProps<Record<string, never>, "movingHandle">) => {
15+
const connection = useConnection();
16+
17+
return (
18+
<>
19+
<div
20+
style={{
21+
display: "flex",
22+
"flex-direction": "column",
23+
position: "absolute",
24+
left: 0,
25+
top: 0,
26+
"justify-content": "space-around",
27+
height: "100%",
28+
}}
29+
>
30+
<Handle
31+
type="target"
32+
id="a"
33+
position={Position.Left}
34+
style={{
35+
...sourceHandleStyle,
36+
transform: connection().inProgress ? "translate(-20px, 0)" : "translate(-50%, 0)",
37+
}}
38+
/>
39+
<Handle
40+
type="target"
41+
id="b"
42+
position={Position.Left}
43+
style={{
44+
...sourceHandleStyle,
45+
transform: connection().inProgress ? "translate(-20px, 0)" : "translate(-50%, 0)",
46+
}}
47+
/>
48+
</div>
49+
<div
50+
style={{
51+
background: "#f4f4f4",
52+
padding: "10px",
53+
}}
54+
>
55+
<div>moving handles</div>
56+
<Handle type="source" position={Position.Right} />
57+
<Handle type="source" position={Position.Right} />
58+
</div>
59+
</>
60+
);
61+
};
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Position } from "@xyflow/system";
2+
import { createEffect } from "solid-js";
3+
import { produce } from "solid-js/store";
4+
5+
import {
6+
Background,
7+
Controls,
8+
createEdgeStore,
9+
createNodeStore,
10+
SolidFlow,
11+
useConnection,
12+
useSolidFlow,
13+
useUpdateNodeInternals,
14+
} from "@/index";
15+
import type { EdgeConnection } from "@/types";
16+
17+
import { MovingHandleNode } from "./MovingHandleNode";
18+
19+
const nodeTypes = {
20+
movingHandle: MovingHandleNode,
21+
};
22+
23+
const NodeUpdater = () => {
24+
const connection = useConnection();
25+
const { getNodes } = useSolidFlow();
26+
const updateNodeInternals = useUpdateNodeInternals();
27+
28+
createEffect(() => {
29+
if (connection()) {
30+
const startTime = Date.now();
31+
const nodeIds = getNodes().map((n) => n.id);
32+
33+
const update = () => {
34+
if (Date.now() - startTime < 500) {
35+
updateNodeInternals(nodeIds);
36+
requestAnimationFrame(update);
37+
}
38+
};
39+
40+
update();
41+
}
42+
});
43+
44+
return null;
45+
};
46+
47+
export const MovingHandles = () => {
48+
const [nodes] = createNodeStore([
49+
{
50+
id: "input",
51+
type: "input",
52+
data: { label: "input" },
53+
position: { x: -300, y: 0 },
54+
sourcePosition: Position.Right,
55+
},
56+
...new Array(10).fill(0).map(
57+
(_, i) =>
58+
({
59+
id: `${i}`,
60+
type: "movingHandle",
61+
position: { x: 0, y: i * 60 },
62+
data: {},
63+
}) as Parameters<typeof createNodeStore>[0][number],
64+
),
65+
]);
66+
67+
const [edges, setEdges] = createEdgeStore([]);
68+
69+
const onConnect = (connection: EdgeConnection) => {
70+
setEdges(
71+
(edge) => edge.id === connection.id,
72+
produce((edge) => {
73+
edge.animated = true;
74+
}),
75+
);
76+
};
77+
78+
return (
79+
<SolidFlow
80+
nodes={nodes}
81+
edges={edges}
82+
onConnect={onConnect}
83+
nodeTypes={nodeTypes}
84+
minZoom={0.2}
85+
fitView
86+
connectionLineStyle={{
87+
stroke: "#ff0000",
88+
"stroke-width": "3",
89+
}}
90+
>
91+
<Controls />
92+
<Background />
93+
<NodeUpdater />
94+
</SolidFlow>
95+
);
96+
};

src/components/SolidFlow/types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type {
44
CoordinateExtent,
55
IsValidConnection,
66
NodeOrigin,
7-
OnConnect,
87
OnConnectEnd,
98
OnConnectStart,
109
OnError,
@@ -36,10 +35,11 @@ import type {
3635
NodeEvents,
3736
NodeSelectionEvents,
3837
NodeTypes,
39-
OnBeforeConnect,
4038
OnBeforeDelete,
39+
OnBeforeEdgeConnect,
4140
OnBeforeReconnect,
4241
OnDelete,
42+
OnEdgeConnect,
4343
OnSelectionChange,
4444
OnSelectionDrag,
4545
PaneEvents,
@@ -463,9 +463,9 @@ export type SolidFlowProps<
463463
/** This handler gets called before the user deletes nodes or edges and provides a way to abort the deletion by returning false. */
464464
readonly onBeforeDelete?: OnBeforeDelete<NodeType, EdgeType>;
465465
/** This handler gets called when a new edge is created. You can use it to modify the newly created edge. */
466-
readonly onBeforeConnect?: OnBeforeConnect<EdgeType>;
466+
readonly onBeforeConnect?: OnBeforeEdgeConnect<EdgeType>;
467467
/** This event gets fired when a connection successfully completes and an edge is created. */
468-
readonly onConnect?: OnConnect;
468+
readonly onConnect?: OnEdgeConnect;
469469
/** When a user starts to drag a connection line, this event gets fired. */
470470
readonly onConnectStart?: OnConnectStart;
471471
/** When a user stops dragging a connection line, this event gets fired. */

src/components/graph/connection/ConnectionLine.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const ConnectionLine = <NodeType extends Node = Node>(
2323
props: ParentProps<Partial<ConnectionLineProps<NodeType>>>,
2424
) => {
2525
const { store } = useInternalSolidFlow<NodeType>();
26+
const connectionStatus = () => getConnectionStatus(store.connection.isValid);
2627

2728
return (
2829
<Show when={store.connection.inProgress}>
@@ -32,7 +33,7 @@ const ConnectionLine = <NodeType extends Node = Node>(
3233
height={store.height}
3334
style={props.containerStyle}
3435
>
35-
<g class={clsx(["solid-flow__connection", getConnectionStatus(store.connection.isValid)])}>
36+
<g class={clsx(["solid-flow__connection", connectionStatus()])}>
3637
<Show when={props.component} fallback={<InternalConnectionLine style={props.style} />}>
3738
{(CustomComponent) => {
3839
const UserConnectionLine = CustomComponent();
@@ -49,7 +50,7 @@ const ConnectionLine = <NodeType extends Node = Node>(
4950
toY={store.connection.to!.y}
5051
fromPosition={store.connection.fromPosition!}
5152
toPosition={store.connection.toPosition!}
52-
connectionStatus={getConnectionStatus(store.connection.isValid)}
53+
connectionStatus={connectionStatus()}
5354
toNode={store.connection.toNode!}
5455
toHandle={store.connection.toHandle!}
5556
/>

src/components/graph/handle/Handle.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { unwrap } from "solid-js/store";
1919

2020
import { useInternalSolidFlow, useNodeId } from "@/components/contexts";
2121
import { useNodeConnectable } from "@/components/contexts/nodeConnectable";
22+
import { getEdgeId } from "@/data/utils";
2223
import type { Edge, Node, Position } from "@/types";
2324

2425
type HandleProps = Omit<SystemHandleProps, "position"> & {
@@ -112,12 +113,15 @@ export const Handle = <NodeType extends Node = Node, EdgeType extends Edge = Edg
112113
});
113114

114115
const onConnectExtended = (connection: Connection) => {
115-
const edge = store.onBeforeConnect?.(connection) ?? connection;
116+
const handleConnection = {
117+
...connection,
118+
id: getEdgeId(connection),
119+
};
116120

117-
if (!edge) return;
121+
const edge = store.onBeforeConnect?.(handleConnection) ?? handleConnection;
118122

119123
actions.addEdge(edge);
120-
store.onConnect?.(connection);
124+
store.onConnect?.(handleConnection);
121125
};
122126

123127
const onPointerDown = (event: PointerEvent) => {

src/components/graph/plugins/NodeToolbar.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { type Align, getNodeToolbarTransform, Position as SystemPosition } from
22
import { type JSX, mergeProps, type ParentComponent, Show, splitProps, useContext } from "solid-js";
33
import { Portal } from "solid-js/web";
44

5+
import { useInternalSolidFlow } from "@/components/contexts";
56
import { NodeIdContext } from "@/components/contexts/nodeId";
67
import { useSolidFlow } from "@/hooks";
7-
import { useSolidFlowStore } from "@/hooks/useSolidFlowStore";
88
import type { InternalNode, Position } from "@/types";
99

1010
export type NodeToolbarProps = Omit<JSX.HTMLAttributes<HTMLDivElement>, "style"> & {
@@ -28,15 +28,9 @@ export type NodeToolbarProps = Omit<JSX.HTMLAttributes<HTMLDivElement>, "style">
2828
};
2929

3030
export const NodeToolbar: ParentComponent<Partial<NodeToolbarProps>> = (props) => {
31-
const store = useSolidFlowStore();
31+
const { store } = useInternalSolidFlow();
3232
const { getNodes, getNodesBounds, getInternalNode } = useSolidFlow();
3333

34-
const ctxNodeId = () => {
35-
// NodeToolbar can be rendered outside of NodeWrapper, so we need to use the context directly.
36-
const id = useContext(NodeIdContext);
37-
return id ? id() : "";
38-
};
39-
4034
const _props = mergeProps(
4135
{
4236
offset: 10,
@@ -57,6 +51,12 @@ export const NodeToolbar: ParentComponent<Partial<NodeToolbarProps>> = (props) =
5751
"children",
5852
]);
5953

54+
const ctxNodeId = () => {
55+
// NodeToolbar can be rendered outside of NodeWrapper, so we need to use the context directly.
56+
const id = useContext(NodeIdContext);
57+
return id ? id() : "";
58+
};
59+
6060
const toolbarNodes = () => {
6161
const nodeIds = Array.isArray(local.nodeId) ? local.nodeId : [local.nodeId ?? ctxNodeId()];
6262

src/data/createSolidFlow.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ export const createSolidFlow = <NodeType extends Node = Node, EdgeType extends E
257257
const state = connection();
258258
return {
259259
...state,
260+
from: state.inProgress ? pointToRendererPoint(state.from, this.transform) : state.from,
260261
to: state.inProgress ? pointToRendererPoint(state.to, this.transform) : state.to,
261262
} as ConnectionState<InternalNode<NodeType>>;
262263
},

src/data/utils.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1-
import { getNodesInside, type NodeLookup, type Transform } from "@xyflow/system";
1+
import {
2+
type Connection,
3+
type EdgeBase,
4+
getNodesInside,
5+
type NodeLookup,
6+
type Transform,
7+
} from "@xyflow/system";
28

39
import type { InternalNode, Node } from "../types";
410

11+
export const getEdgeId = (connection: Connection | EdgeBase) => {
12+
const { source, sourceHandle, target, targetHandle } = connection;
13+
return `xy-edge__${source}${sourceHandle || ""}-${target}${targetHandle || ""}`;
14+
};
15+
516
export function getVisibleNodes<NodeType extends Node = Node>(
617
nodeLookup: NodeLookup<InternalNode<NodeType>>,
718
transform: Transform,

0 commit comments

Comments
 (0)