diff --git a/examples/react/src/examples/UseNodesData/ResultNode.tsx b/examples/react/src/examples/UseNodesData/ResultNode.tsx
index 09603eac..324da1fe 100644
--- a/examples/react/src/examples/UseNodesData/ResultNode.tsx
+++ b/examples/react/src/examples/UseNodesData/ResultNode.tsx
@@ -5,7 +5,7 @@ function ResultNode() {
const connections = useHandleConnections({
handleType: 'target',
});
- const nodesData = useNodesData<{ text: string }>(connections.map((connection) => connection.source));
+ const nodesData = useNodesData(connections.map((connection) => connection.source));
useEffect(() => {
console.log('incoming data changed', nodesData);
@@ -16,7 +16,8 @@ function ResultNode() {
incoming texts:{' '}
- {nodesData?.filter((nodeData) => nodeData.text).map(({ text }, i) =>
{text}
) || 'none'}
+ {nodesData?.filter((nodeData) => nodeData.text !== undefined).map(({ text }, i) =>
{text}
) ||
+ 'none'}
);
diff --git a/examples/react/src/examples/UseNodesData/UppercaseNode.tsx b/examples/react/src/examples/UseNodesData/UppercaseNode.tsx
index 7f0b964f..06cf84a8 100644
--- a/examples/react/src/examples/UseNodesData/UppercaseNode.tsx
+++ b/examples/react/src/examples/UseNodesData/UppercaseNode.tsx
@@ -5,7 +5,7 @@ function UppercaseNode({ id }: NodeProps) {
const connections = useHandleConnections({
handleType: 'target',
});
- const nodeData = useNodesData<{ text: string }>(connections[0]?.source);
+ const nodeData = useNodesData(connections[0]?.source);
const updateNodeData = useUpdateNodeData();
useEffect(() => {
@@ -14,9 +14,9 @@ function UppercaseNode({ id }: NodeProps) {
return (
-
-
uppercase transform
+
uppercase transform
+
);
}
diff --git a/examples/react/src/examples/UseNodesData/index.tsx b/examples/react/src/examples/UseNodesData/index.tsx
index 040ca35a..7bb1a94e 100644
--- a/examples/react/src/examples/UseNodesData/index.tsx
+++ b/examples/react/src/examples/UseNodesData/index.tsx
@@ -15,13 +15,18 @@ import TextNode from './TextNode';
import ResultNode from './ResultNode';
import UppercaseNode from './UppercaseNode';
+export type TextNode = Node<{ text: string }, 'text'>;
+export type ResultNode = Node<{}, 'result'>;
+export type UppercaseNode = Node<{}, 'uppercase'>;
+export type MyNode = Node<{ text: string }, 'text'> | Node<{}, 'result'> | Node<{}, 'uppercase'>;
+
const nodeTypes = {
text: TextNode,
result: ResultNode,
uppercase: UppercaseNode,
};
-const initNodes: Node[] = [
+const initNodes: MyNode[] = [
{
id: '1',
type: 'text',
diff --git a/examples/svelte/src/components/Header/Header.svelte b/examples/svelte/src/components/Header/Header.svelte
index ad34d289..9d67f049 100644
--- a/examples/svelte/src/components/Header/Header.svelte
+++ b/examples/svelte/src/components/Header/Header.svelte
@@ -11,6 +11,7 @@
'drag-n-drop',
'edges',
'figma',
+ 'handle-connect',
'interaction',
'intersections',
'node-toolbar',
@@ -18,6 +19,7 @@
'stress',
'subflows',
'two-way-viewport',
+ 'usenodesdata',
'usesvelteflow',
'useupdatenodeinternals',
'validation'
diff --git a/examples/svelte/src/routes/examples/customnode/CustomNode.svelte b/examples/svelte/src/routes/examples/customnode/CustomNode.svelte
index 51ba33ea..25d37d32 100644
--- a/examples/svelte/src/routes/examples/customnode/CustomNode.svelte
+++ b/examples/svelte/src/routes/examples/customnode/CustomNode.svelte
@@ -1,6 +1,6 @@
+
+
+
+
+
+
diff --git a/examples/svelte/src/routes/examples/handle-connect/MultiHandleNode.svelte b/examples/svelte/src/routes/examples/handle-connect/MultiHandleNode.svelte
new file mode 100644
index 00000000..ebc85c74
--- /dev/null
+++ b/examples/svelte/src/routes/examples/handle-connect/MultiHandleNode.svelte
@@ -0,0 +1,110 @@
+
+
+
+
+
node {id}
+
onConnectSource('a', connections)}
+ ondisconnect={(connections) => onDisconnectSource('a', connections)}
+ class="source-a"
+ />
+ onConnectSource('b', connections)}
+ ondisconnect={(connections) => onDisconnectSource('b', connections)}
+ class="source-b"
+ />
+
+
+
diff --git a/examples/svelte/src/routes/examples/handle-connect/SingleHandleNode.svelte b/examples/svelte/src/routes/examples/handle-connect/SingleHandleNode.svelte
new file mode 100644
index 00000000..7c1e026d
--- /dev/null
+++ b/examples/svelte/src/routes/examples/handle-connect/SingleHandleNode.svelte
@@ -0,0 +1,77 @@
+
+
+
+
+
diff --git a/examples/svelte/src/routes/examples/usenodesdata/+page.svelte b/examples/svelte/src/routes/examples/usenodesdata/+page.svelte
new file mode 100644
index 00000000..7b2d2204
--- /dev/null
+++ b/examples/svelte/src/routes/examples/usenodesdata/+page.svelte
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
diff --git a/examples/svelte/src/routes/examples/usenodesdata/ResultNode.svelte b/examples/svelte/src/routes/examples/usenodesdata/ResultNode.svelte
new file mode 100644
index 00000000..c4150f42
--- /dev/null
+++ b/examples/svelte/src/routes/examples/usenodesdata/ResultNode.svelte
@@ -0,0 +1,38 @@
+
+
+
+
+
incoming texts:
+
+ {#each $nodeData as data}
+
{data.text}
+ {/each}
+
+
+
diff --git a/examples/svelte/src/routes/examples/usenodesdata/TextNode.svelte b/examples/svelte/src/routes/examples/usenodesdata/TextNode.svelte
new file mode 100644
index 00000000..142a14bb
--- /dev/null
+++ b/examples/svelte/src/routes/examples/usenodesdata/TextNode.svelte
@@ -0,0 +1,29 @@
+
+
+
+
node {id}
+
+ updateNodeData(id, { text: evt.currentTarget.value })}
+ />
+
+
+
+
+
diff --git a/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte b/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte
new file mode 100644
index 00000000..3e311f1f
--- /dev/null
+++ b/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte
@@ -0,0 +1,40 @@
+
+
+
+
+
uppercase transform
+
+
+
+
diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx
index 29231b71..a2c0101f 100644
--- a/packages/react/src/components/Handle/index.tsx
+++ b/packages/react/src/components/Handle/index.tsx
@@ -171,7 +171,7 @@ const Handle = forwardRef(
lib,
});
- if (isValid) {
+ if (isValid && connection) {
onConnectExtended(connection);
}
diff --git a/packages/react/src/hooks/useHandleConnections.ts b/packages/react/src/hooks/useHandleConnections.ts
index f692ba37..58987aec 100644
--- a/packages/react/src/hooks/useHandleConnections.ts
+++ b/packages/react/src/hooks/useHandleConnections.ts
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef } from 'react';
-import { Connection, HandleType } from '@xyflow/system';
+import { Connection, HandleType, areConnectionMapsEqual, handleConnectionChange } from '@xyflow/system';
import { useStore } from './useStore';
import { useNodeId } from '../contexts/NodeIdContext';
@@ -52,55 +52,3 @@ export function useHandleConnections({
return useMemo(() => Array.from(connections?.values() ?? []), [connections]);
}
-
-/**
- * @internal
- */
-function areConnectionMapsEqual(a?: Map, b?: Map) {
- if (!a && !b) {
- return true;
- }
-
- if (!a || !b || a.size !== b.size) {
- return false;
- }
-
- if (!a.size && !b.size) {
- return true;
- }
-
- for (const key of a.keys()) {
- if (!b.has(key)) {
- return false;
- }
- }
-
- return true;
-}
-
-/**
- * We call the callback for all connections in a that are not in b
- *
- * @internal
- */
-function handleConnectionChange(
- a: Map,
- b: Map,
- cb?: (diff: Connection[]) => void
-) {
- if (!cb) {
- return;
- }
-
- const diff: Connection[] = [];
-
- a.forEach((connection, key) => {
- if (!b?.has(key)) {
- diff.push(connection);
- }
- });
-
- if (diff.length) {
- cb(diff);
- }
-}
diff --git a/packages/react/src/hooks/useNodesData.ts b/packages/react/src/hooks/useNodesData.ts
index 69927642..4c242ad3 100644
--- a/packages/react/src/hooks/useNodesData.ts
+++ b/packages/react/src/hooks/useNodesData.ts
@@ -2,9 +2,14 @@ import { useCallback } from 'react';
import { shallow } from 'zustand/shallow';
import { useStore } from '../hooks/useStore';
+import type { Node } from '../types';
-export function useNodesData(nodeId: string): NodeData | null;
-export function useNodesData(nodeIds: string[]): NodeData[];
+export function useNodesData(nodeId: string): NodeType['data'] | null;
+export function useNodesData(nodeIds: string[]): NodeType['data'][];
+export function useNodesData(
+ nodeIds: string[],
+ guard: (node: Node) => node is NodeType
+): NodeType['data'][];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useNodesData(nodeIds: any): any {
const nodesData = useStore(
@@ -14,15 +19,17 @@ export function useNodesData(nodeIds: any): any {
return s.nodeLookup.get(nodeIds)?.data || null;
}
- return nodeIds.reduce((res, id) => {
- const node = s.nodeLookup.get(id);
+ const data = [];
- if (node) {
- res.push(node.data);
+ for (const nodeId of nodeIds) {
+ const nodeData = s.nodeLookup.get(nodeId)?.data;
+
+ if (nodeData) {
+ data.push(nodeData);
}
+ }
- return res;
- }, []);
+ return data;
},
[nodeIds]
),
diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts
index 0b308752..c5c21d7a 100644
--- a/packages/react/src/store/index.ts
+++ b/packages/react/src/store/index.ts
@@ -7,10 +7,11 @@ import {
panBy as panBySystem,
Dimensions,
updateNodeDimensions as updateNodeDimensionsSystem,
+ updateConnectionLookup,
} from '@xyflow/system';
import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
-import { updateConnectionLookup, updateNodesAndEdgesSelections } from './utils';
+import { updateNodesAndEdgesSelections } from './utils';
import getInitialState from './initialState';
import type {
ReactFlowState,
diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts
index f167a9aa..8d07617a 100644
--- a/packages/react/src/store/initialState.ts
+++ b/packages/react/src/store/initialState.ts
@@ -5,11 +5,10 @@ import {
getNodesBounds,
getViewportForBounds,
Transform,
- Connection,
+ updateConnectionLookup,
} from '@xyflow/system';
import type { Edge, Node, ReactFlowStore } from '../types';
-import { updateConnectionLookup } from './utils';
const getInitialState = ({
nodes = [],
@@ -24,8 +23,8 @@ const getInitialState = ({
height?: number;
fitView?: boolean;
} = {}): ReactFlowStore => {
- const nodeLookup = new Map();
- const connectionLookup = updateConnectionLookup(new Map>(), edges);
+ const nodeLookup = new Map();
+ const connectionLookup = updateConnectionLookup(new Map(), edges);
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
let transform: Transform = [0, 0, 1];
diff --git a/packages/react/src/store/utils.ts b/packages/react/src/store/utils.ts
index 7ad5ad43..86cddde7 100644
--- a/packages/react/src/store/utils.ts
+++ b/packages/react/src/store/utils.ts
@@ -1,6 +1,5 @@
import type { StoreApi } from 'zustand';
import type { Edge, EdgeSelectionChange, Node, NodeSelectionChange, ReactFlowState } from '../types';
-import { Connection } from '@xyflow/system';
export function handleControlledSelectionChange(
changes: NodeSelectionChange[] | EdgeSelectionChange[],
@@ -43,23 +42,3 @@ export function updateNodesAndEdgesSelections({ changedNodes, changedEdges, get,
onEdgesChange?.(changedEdges);
}
}
-
-export function updateConnectionLookup(lookup: Map>, edges: Edge[]) {
- lookup.clear();
-
- edges.forEach(({ source, target, sourceHandle = null, targetHandle = null }) => {
- if (source && target) {
- const sourceKey = `${source}-source-${sourceHandle}`;
- const targetKey = `${target}-target-${targetHandle}`;
-
- const prevSource = lookup.get(sourceKey) || new Map();
- const prevTarget = lookup.get(targetKey) || new Map();
- const connection = { source, target, sourceHandle, targetHandle };
-
- lookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection));
- lookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection));
- }
- });
-
- return lookup;
-}
diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte
index e4b6a823..06a6198e 100644
--- a/packages/svelte/src/lib/components/Handle/Handle.svelte
+++ b/packages/svelte/src/lib/components/Handle/Handle.svelte
@@ -6,7 +6,9 @@
XYHandle,
isMouseEvent,
type Connection,
- type HandleType
+ type HandleType,
+ areConnectionMapsEqual,
+ handleConnectionChange
} from '@xyflow/system';
import { useStore } from '$lib/store';
@@ -20,6 +22,8 @@
export let position: $$Props['position'] = Position.Top;
export let style: $$Props['style'] = undefined;
export let isConnectable: $$Props['isConnectable'] = undefined;
+ export let onconnect: $$Props['onconnect'] = undefined;
+ export let ondisconnect: $$Props['ondisconnect'] = undefined;
// export let isConnectableStart: $$Props['isConnectableStart'] = undefined;
// export let isConnectableEnd: $$Props['isConnectableEnd'] = undefined;
@@ -59,7 +63,9 @@
panBy,
cancelConnection,
updateConnection,
- autoPanOnConnect
+ autoPanOnConnect,
+ edges,
+ connectionLookup
} = store;
function onPointerDown(event: MouseEvent | TouchEvent) {
@@ -108,6 +114,26 @@
}
}
+ let prevConnections: Map | null = null;
+ let connections: Map | undefined;
+
+ $: if (onconnect || ondisconnect) {
+ // connectionLookup is not reactive, so we use edges to get notified about updates
+ $edges;
+ connections = $connectionLookup.get(`${nodeId}-${type}-${id || null}`);
+ }
+
+ $: {
+ if (prevConnections && !areConnectionMapsEqual(connections, prevConnections)) {
+ const _connections = connections ?? new Map();
+
+ handleConnectionChange(prevConnections, _connections, ondisconnect);
+ handleConnectionChange(_connections, prevConnections, onconnect);
+ }
+
+ prevConnections = connections ?? new Map();
+ }
+
// @todo implement connectablestart, connectableend
diff --git a/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte b/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte
index f059ff8e..f52d696a 100644
--- a/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte
+++ b/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte
@@ -107,7 +107,7 @@
trigger: [
{
...deleteKeyDefinition,
- callback: () => deleteKeyDefinition.key && deleteKeyPressed.set(true)
+ callback: (event) => deleteKeyDefinition.key && deleteKeyPressed.set(true)
}
],
type: 'keydown'
diff --git a/packages/svelte/src/lib/hooks/useHandleConnections.ts b/packages/svelte/src/lib/hooks/useHandleConnections.ts
new file mode 100644
index 00000000..c8df0dae
--- /dev/null
+++ b/packages/svelte/src/lib/hooks/useHandleConnections.ts
@@ -0,0 +1,30 @@
+import { derived } from 'svelte/store';
+import { areConnectionMapsEqual, type Connection, type HandleType } from '@xyflow/system';
+
+import { useStore } from '$lib/store';
+
+export type useHandleConnectionsParams = {
+ nodeId: string;
+ type: HandleType;
+ id?: string | null;
+};
+
+const initialConnections: Connection[] = [];
+
+export function useHandleConnections({ nodeId, type, id = null }: useHandleConnectionsParams) {
+ const { edges, connectionLookup } = useStore();
+ let prevConnections: Map | undefined = undefined;
+
+ return derived(
+ [edges, connectionLookup],
+ ([, connectionLookup], set) => {
+ const nextConnections = connectionLookup.get(`${nodeId}-${type}-${id || null}`);
+
+ if (!areConnectionMapsEqual(nextConnections, prevConnections)) {
+ prevConnections = nextConnections;
+ set(Array.from(prevConnections?.values() || []));
+ }
+ },
+ initialConnections
+ );
+}
diff --git a/packages/svelte/src/lib/hooks/useNodesData.ts b/packages/svelte/src/lib/hooks/useNodesData.ts
new file mode 100644
index 00000000..f50bc8b8
--- /dev/null
+++ b/packages/svelte/src/lib/hooks/useNodesData.ts
@@ -0,0 +1,64 @@
+import { derived, type Readable } from 'svelte/store';
+
+import type { Node } from '$lib/types';
+import { useStore } from '$lib/store';
+
+function areNodesDataEqual(a: Node['data'][] | null, b: Node['data'][] | null) {
+ if ((!a && !b) || (!a?.length && !b?.length)) {
+ true;
+ }
+
+ if (!a || !b || a.length !== b.length) {
+ return false;
+ }
+
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== b[i]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+export function useNodesData(
+ nodeId: string
+): Readable;
+export function useNodesData(
+ nodeIds: string[]
+): Readable;
+export function useNodesData(
+ nodeIds: string[],
+ guard: (node: Node) => node is NodeType
+): Readable;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function useNodesData(nodeIds: any): any {
+ const { nodes, nodeLookup } = useStore();
+ let prevNodesData: (Node['data'] | null)[] | null = null;
+
+ return derived([nodes, nodeLookup], ([, nodeLookup], set) => {
+ let nextNodesData: (Node['data'] | null)[] | null = null;
+ const nodeIdArray = Array.isArray(nodeIds);
+
+ if (!nodeIdArray) {
+ nextNodesData = [nodeLookup.get(nodeIds)?.data || null];
+ } else {
+ const data = [];
+
+ for (const nodeId of nodeIds) {
+ const nodeData = nodeLookup.get(nodeId)?.data;
+
+ if (nodeData) {
+ data.push(nodeData);
+ }
+ }
+
+ nextNodesData = data;
+ }
+
+ if (!areNodesDataEqual(nextNodesData, prevNodesData)) {
+ prevNodesData = nextNodesData;
+ set(nodeIdArray ? nextNodesData : nextNodesData[0]);
+ }
+ });
+}
diff --git a/packages/svelte/src/lib/hooks/useUpdateNodeData.ts b/packages/svelte/src/lib/hooks/useUpdateNodeData.ts
new file mode 100644
index 00000000..cb45dd10
--- /dev/null
+++ b/packages/svelte/src/lib/hooks/useUpdateNodeData.ts
@@ -0,0 +1,22 @@
+import { useStore } from '$lib/store';
+
+export function useUpdateNodeData(): (id: string, data: unknown) => void {
+ const { nodes } = useStore();
+
+ const updateNodeData = (id: string, data: unknown) => {
+ nodes.update((nds) =>
+ nds.map((node) => {
+ if (node.id === id) {
+ return {
+ ...node,
+ data
+ };
+ }
+
+ return node;
+ })
+ );
+ };
+
+ return updateNodeData;
+}
diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts
index a9107415..71841ec7 100644
--- a/packages/svelte/src/lib/index.ts
+++ b/packages/svelte/src/lib/index.ts
@@ -27,6 +27,9 @@ export * from '$lib/hooks/useSvelteFlow';
export * from '$lib/hooks/useUpdateNodeInternals';
export * from '$lib/hooks/useConnection';
export * from '$lib/hooks/useNodesEdges';
+export * from '$lib/hooks/useHandleConnections';
+export * from '$lib/hooks/useNodesData';
+export * from '$lib/hooks/useUpdateNodeData';
// types
export type {
diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts
index 9f51abc3..ceec1c7b 100644
--- a/packages/svelte/src/lib/store/initial-store.ts
+++ b/packages/svelte/src/lib/store/initial-store.ts
@@ -17,7 +17,9 @@ import {
type Viewport,
updateNodes,
getNodesBounds,
- getViewportForBounds
+ getViewportForBounds,
+ updateConnectionLookup,
+ type ConnectionLookup
} from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -72,11 +74,12 @@ export const getInitialStore = ({
height?: number;
fitView?: boolean;
}) => {
- const nodeLookup = new Map();
+ const nodeLookup = new Map();
const nextNodes = updateNodes(nodes, nodeLookup, {
nodeOrigin: [0, 0],
elevateNodesOnSelect: false
});
+ const connectionLookup = updateConnectionLookup(new Map(), edges);
let viewport: Viewport = { x: 0, y: 0, zoom: 1 };
@@ -91,8 +94,9 @@ export const getInitialStore = ({
nodes: createNodesStore(nextNodes, nodeLookup),
nodeLookup: readable