feat(svelte): add useHandleConnections, useNodesData and useUpdateNodeData

This commit is contained in:
moklick
2023-12-11 18:31:37 +01:00
parent 5d8c1bbff9
commit d4d773d9c6
32 changed files with 761 additions and 108 deletions
@@ -171,7 +171,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
lib,
});
if (isValid) {
if (isValid && connection) {
onConnectExtended(connection);
}
@@ -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<string, Connection>, b?: Map<string, Connection>) {
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<string, Connection>,
b: Map<string, Connection>,
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);
}
}
+15 -8
View File
@@ -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<NodeData = unknown>(nodeId: string): NodeData | null;
export function useNodesData<NodeData = unknown>(nodeIds: string[]): NodeData[];
export function useNodesData<NodeType extends Node = Node>(nodeId: string): NodeType['data'] | null;
export function useNodesData<NodeType extends Node = Node>(nodeIds: string[]): NodeType['data'][];
export function useNodesData<NodeType extends Node = Node>(
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]
),
+2 -1
View File
@@ -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,
+3 -4
View File
@@ -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<string, Node>();
const connectionLookup = updateConnectionLookup(new Map<string, Map<string, Connection>>(), 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];
-21
View File
@@ -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<NodeOrEdge extends Node | Edge>(
changes: NodeSelectionChange[] | EdgeSelectionChange[],
@@ -43,23 +42,3 @@ export function updateNodesAndEdgesSelections({ changedNodes, changedEdges, get,
onEdgesChange?.(changedEdges);
}
}
export function updateConnectionLookup(lookup: Map<string, Map<string, Connection>>, 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;
}
@@ -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<string, Connection> | null = null;
let connections: Map<string, Connection> | 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
</script>
@@ -107,7 +107,7 @@
trigger: [
{
...deleteKeyDefinition,
callback: () => deleteKeyDefinition.key && deleteKeyPressed.set(true)
callback: (event) => deleteKeyDefinition.key && deleteKeyPressed.set(true)
}
],
type: 'keydown'
@@ -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<string, Connection> | 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
);
}
@@ -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<NodeType extends Node = Node>(
nodeId: string
): Readable<NodeType['data'] | null>;
export function useNodesData<NodeType extends Node = Node>(
nodeIds: string[]
): Readable<NodeType['data'][]>;
export function useNodesData<NodeType extends Node = Node>(
nodeIds: string[],
guard: (node: Node) => node is NodeType
): Readable<NodeType['data'][]>;
// 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]);
}
});
}
@@ -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;
}
+3
View File
@@ -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 {
@@ -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<string, Node>();
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<Map<string, Node>>(nodeLookup),
visibleNodes: readable<Node[]>([]),
edges: createEdgesStore(edges),
edges: createEdgesStore(edges, connectionLookup),
edgeTree: readable<GroupedEdges<EdgeLayouted>[]>([]),
connectionLookup: readable<ConnectionLookup>(connectionLookup),
height: writable<number>(500),
width: writable<number>(500),
minZoom: writable<number>(0.5),
+11 -1
View File
@@ -6,7 +6,13 @@ import {
type Writable,
get
} from 'svelte/store';
import { updateNodes, type Viewport, type PanZoomInstance } from '@xyflow/system';
import {
updateNodes,
type Viewport,
type PanZoomInstance,
type ConnectionLookup,
updateConnectionLookup
} from '@xyflow/system';
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types';
@@ -168,6 +174,7 @@ export const createNodesStore = (
export const createEdgesStore = (
edges: Edge[],
connectionLookup: ConnectionLookup,
defaultOptions?: DefaultEdgeOptions
): Writable<Edge[]> & { setDefaultOptions: (opts: DefaultEdgeOptions) => void } => {
const { subscribe, set, update } = writable<Edge[]>([]);
@@ -176,6 +183,9 @@ export const createEdgesStore = (
const _set: typeof set = (eds: Edge[]) => {
const nextEdges = defaults ? eds.map((edge) => ({ ...defaults, ...edge })) : eds;
updateConnectionLookup(connectionLookup, nextEdges);
value = nextEdges;
set(value);
};
+2
View File
@@ -31,6 +31,8 @@ export type HandleComponentProps = {
isConnectable?: boolean;
isConnectableStart?: boolean;
isConnectableEnd?: boolean;
onconnect?: (connections: Connection[]) => void;
ondisconnect?: (connections: Connection[]) => void;
};
export type FitViewOptions = FitViewOptionsBase<Node>;
+2
View File
@@ -139,3 +139,5 @@ export type UpdateConnection = (params: {
export type ColorModeClass = 'light' | 'dark';
export type ColorMode = ColorModeClass | 'system';
export type ConnectionLookup = Map<string, Map<string, Connection>>;
+53
View File
@@ -0,0 +1,53 @@
import { Connection } from '../types';
/**
* @internal
*/
export function areConnectionMapsEqual(a?: Map<string, Connection>, b?: Map<string, Connection>) {
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
*/
export function handleConnectionChange(
a: Map<string, Connection>,
b: Map<string, Connection>,
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);
}
}
+1
View File
@@ -1,3 +1,4 @@
export * from './connections';
export * from './dom';
export * from './edges';
export * from './graph';
+25 -1
View File
@@ -9,6 +9,8 @@ import {
Transform,
XYPosition,
XYZPosition,
ConnectionLookup,
EdgeBase,
} from '../types';
import { getDimensions, getHandleBounds } from './dom';
import { isNumeric } from './general';
@@ -71,11 +73,13 @@ export function updateNodes<NodeType extends NodeBase>(
defaults: {},
}
): NodeType[] {
const tmpLookup = new Map(nodeLookup);
nodeLookup.clear();
const parentNodes: ParentNodes = {};
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
const nextNodes = nodes.map((n) => {
const currentStoreNode = nodeLookup.get(n.id);
const currentStoreNode = tmpLookup.get(n.id);
const node: NodeType = {
...options.defaults,
...n,
@@ -233,3 +237,23 @@ export function panBy({
return transformChanged;
}
export function updateConnectionLookup(lookup: ConnectionLookup, edges: EdgeBase[]) {
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;
}
+7 -5
View File
@@ -58,12 +58,10 @@ export type XYHandleInstance = {
type Result = {
handleDomNode: Element | null;
isValid: boolean;
connection: Connection;
connection: Connection | null;
endHandle: ConnectingHandle | null;
};
const nullConnection: Connection = { source: null, target: null, sourceHandle: null, targetHandle: null };
const alwaysValid = () => true;
let connectionStartHandle: ConnectingHandle | null = null;
@@ -197,7 +195,7 @@ function onPointerDown(
return resetRecentHandle(prevActiveHandle, lib);
}
if (connection.source !== connection.target && handleDomNode) {
if (connection?.source !== connection?.target && handleDomNode) {
resetRecentHandle(prevActiveHandle, lib);
prevActiveHandle = handleDomNode;
handleDomNode.classList.add('connecting', `${lib}-flow__handle-connecting`);
@@ -269,7 +267,7 @@ function isValidHandle(
const result: Result = {
handleDomNode: handleToCheck,
isValid: false,
connection: nullConnection,
connection: null,
endHandle: null,
};
@@ -280,6 +278,10 @@ function isValidHandle(
const connectable = handleToCheck.classList.contains('connectable');
const connectableEnd = handleToCheck.classList.contains('connectableend');
if (!handleNodeId) {
return result;
}
const connection: Connection = {
source: isTarget ? handleNodeId : fromNodeId,
sourceHandle: isTarget ? handleId : fromHandleId,