From e5c667d0683bb82b30634c244f1327e161218041 Mon Sep 17 00:00:00 2001 From: Moritz Klack Date: Tue, 27 Feb 2024 15:21:35 +0100 Subject: [PATCH] Feat(nodes): add `initialWidth` and `initialHeight` (#3953) * feat(react/svelte): add initialWidth/initialHeight closes #3793 * chore(packages): update changelogs --- .../src/components/ReactFlowExample/index.tsx | 2 + .../ReactFlowInitialExample/CustomNode.tsx | 21 ++++++ .../ReactFlowInitialExample/index.tsx | 69 +++++++++++++++++++ .../CustomNode.svelte | 25 +++++++ .../SvelteFlowInitialExample/index.svelte | 39 +++++++++++ examples/astro-xyflow/src/pages/index.astro | 24 +++++-- packages/react/CHANGELOG.md | 3 +- .../MiniMap/MiniMapNodes.tsx | 11 +-- .../src/components/NodeWrapper/index.tsx | 21 +++--- .../src/components/NodeWrapper/utils.tsx | 21 +++++- .../src/components/StoreUpdater/index.tsx | 1 + .../react/src/container/ReactFlow/index.tsx | 2 + packages/react/src/store/index.ts | 16 ++++- packages/react/src/store/initialState.ts | 5 +- packages/react/src/types/component-props.ts | 5 ++ packages/react/src/types/store.ts | 1 + packages/svelte/CHANGELOG.md | 5 +- .../components/NodeWrapper/NodeWrapper.svelte | 18 ++++- .../src/lib/components/NodeWrapper/types.ts | 8 ++- .../src/lib/components/NodeWrapper/utils.ts | 33 +++++++++ .../NodeRenderer/NodeRenderer.svelte | 22 ++++-- .../src/lib/plugins/Minimap/Minimap.svelte | 14 ++-- packages/svelte/src/lib/store/index.ts | 2 - .../svelte/src/lib/store/initial-store.ts | 6 +- packages/system/src/types/nodes.ts | 6 +- packages/system/src/utils/edges/positions.ts | 13 ++-- packages/system/src/utils/general.ts | 16 +++++ packages/system/src/utils/graph.ts | 13 ++-- 28 files changed, 362 insertions(+), 60 deletions(-) create mode 100644 examples/astro-xyflow/src/components/ReactFlowInitialExample/CustomNode.tsx create mode 100644 examples/astro-xyflow/src/components/ReactFlowInitialExample/index.tsx create mode 100644 examples/astro-xyflow/src/components/SvelteFlowInitialExample/CustomNode.svelte create mode 100644 examples/astro-xyflow/src/components/SvelteFlowInitialExample/index.svelte create mode 100644 packages/svelte/src/lib/components/NodeWrapper/utils.ts diff --git a/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx b/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx index 4f8ef0ab..863458fb 100644 --- a/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx +++ b/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx @@ -81,6 +81,8 @@ const initialNodes: Node[] = [ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, + width: 200, + height: 50, type: 'custom', }, ]; diff --git a/examples/astro-xyflow/src/components/ReactFlowInitialExample/CustomNode.tsx b/examples/astro-xyflow/src/components/ReactFlowInitialExample/CustomNode.tsx new file mode 100644 index 00000000..c7fae3d8 --- /dev/null +++ b/examples/astro-xyflow/src/components/ReactFlowInitialExample/CustomNode.tsx @@ -0,0 +1,21 @@ +import { memo, useState } from 'react'; +import { Handle, Position } from '@xyflow/react'; + +function CustomNode() { + const [text, setText] = useState('this is a pretty long text'); + + return ( + <> + +
+
+ setText(e.target.value)} /> +
text: {text}
+
+
+ + + ); +} + +export default memo(CustomNode); diff --git a/examples/astro-xyflow/src/components/ReactFlowInitialExample/index.tsx b/examples/astro-xyflow/src/components/ReactFlowInitialExample/index.tsx new file mode 100644 index 00000000..b3f99a6b --- /dev/null +++ b/examples/astro-xyflow/src/components/ReactFlowInitialExample/index.tsx @@ -0,0 +1,69 @@ +import { useCallback } from 'react'; +import { + ReactFlow, + addEdge, + useEdgesState, + useNodesState, + Background, + Controls, + type Connection, + type Edge, + type Node, +} from '@xyflow/react'; + +import CustomNode from './CustomNode'; + +import '@xyflow/react/dist/style.css'; + +const initialNodes: Node[] = [ + { + id: '1', + data: {}, + position: { x: 0, y: 0 }, + initialWidth: 200, + initialHeight: 50, + type: 'custom', + }, + { + id: '2', + data: {}, + position: { x: 0, y: 200 }, + width: 200, + initialHeight: 50, + type: 'custom', + }, +]; + +const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }]; + +const nodeTypes = { + custom: CustomNode, +}; + +function Flow() { + const [nodes, , onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]); + + return ( +
+ + + + +
+ ); +} + +export default Flow; diff --git a/examples/astro-xyflow/src/components/SvelteFlowInitialExample/CustomNode.svelte b/examples/astro-xyflow/src/components/SvelteFlowInitialExample/CustomNode.svelte new file mode 100644 index 00000000..5bef87c9 --- /dev/null +++ b/examples/astro-xyflow/src/components/SvelteFlowInitialExample/CustomNode.svelte @@ -0,0 +1,25 @@ + + + +
+
+ text: {text} +
+ +
+ + + diff --git a/examples/astro-xyflow/src/components/SvelteFlowInitialExample/index.svelte b/examples/astro-xyflow/src/components/SvelteFlowInitialExample/index.svelte new file mode 100644 index 00000000..8fbe4905 --- /dev/null +++ b/examples/astro-xyflow/src/components/SvelteFlowInitialExample/index.svelte @@ -0,0 +1,39 @@ + + +
+ + + + +
diff --git a/examples/astro-xyflow/src/pages/index.astro b/examples/astro-xyflow/src/pages/index.astro index be9d1885..d7b14558 100644 --- a/examples/astro-xyflow/src/pages/index.astro +++ b/examples/astro-xyflow/src/pages/index.astro @@ -1,6 +1,8 @@ --- -import ReactFlowApp from '../components/ReactFlowExample' -import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte' +import ReactFlowApp from '../components/ReactFlowExample'; +import ReactFlowInitialApp from '../components/ReactFlowInitialExample'; +import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte'; +import SvelteFlowInitialApp from '../components/SvelteFlowInitialExample/index.svelte'; --- @@ -18,10 +20,24 @@ import SvelteFlowApp from '../components/SvelteFlowExample/index.svelte' +

React Flow

+

no client hydration

+ + +

client hydration on load (client:load)

+ + +

client hydration on load (client:load) and initialWidth / initialHeight

+ +

Svelte Flow

-

React Flow

- +

client hydration on load (client:load)

+ + +

client hydration on load (client:load) and initialWidth / initialHeight

+ + diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index d9529c27..69b51bf7 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -4,13 +4,14 @@ ## ⚠️ Breaking changes -- `useNodesData` not only returns data objects but also the type and the id of the node. +- `useNodesData` not only returns data objects but also the type and the id of the node ## Patch changes - better cursor defaults for the pane, nodes and edges - `disableKeyboardA11y` now also disables Enter and Escape for selecting/deselecting nodes and edges - fix bug where users couldn't drag a node after toggle nodes `hidden` attribute +- add `initialWidth` and `initialHeight` node attributes for specifying initial dimensions for ssr ## 12.0.0-next.9 diff --git a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx index 0dfb8fff..0abe3177 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ComponentType, memo } from 'react'; -import { NodeOrigin, getNodePositionWithOrigin } from '@xyflow/system'; +import { NodeOrigin, getNodeDimensions, getNodePositionWithOrigin, nodeHasDimensions } from '@xyflow/system'; import { shallow } from 'zustand/shallow'; import { useStore } from '../../hooks/useStore'; @@ -94,16 +94,19 @@ function NodeComponentWrapperInner({ y, }; }, shallow); - if (!node || node.hidden || !(node.computed?.width || node.width) || !(node.computed?.height || node.height)) { + + if (!node || node.hidden || !nodeHasDimensions(node)) { return null; } + const { width, height } = getNodeDimensions(node); + return ( ({ @@ -79,11 +81,9 @@ export function NodeWrapper({ const prevTargetPosition = useRef(node.targetPosition); const prevType = useRef(nodeType); - const width = node.width ?? undefined; - const height = node.height ?? undefined; - const computedWidth = node.computed?.width; - const computedHeight = node.computed?.height; - const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height); + const nodeDimensions = getNodeDimensions(node); + const inlineDimensions = getNodeInlineStyleDimensions(node); + const initialized = nodeHasDimensions(node); const hasHandleBounds = !!node[internalsSymbol]?.handleBounds; const moveSelectedNodes = useMoveSelectedNodes(); @@ -143,8 +143,7 @@ export function NodeWrapper({ const positionAbsoluteOrigin = getPositionWithOrigin({ x: positionAbsoluteX, y: positionAbsoluteY, - width: computedWidth ?? width ?? 0, - height: computedHeight ?? height ?? 0, + ...nodeDimensions, origin: node.origin || nodeOrigin, }); const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; @@ -226,8 +225,7 @@ export function NodeWrapper({ pointerEvents: hasPointerEvents ? 'all' : 'none', visibility: initialized ? 'visible' : 'hidden', ...node.style, - width: width ?? node.style?.width, - height: height ?? node.style?.height, + ...inlineDimensions, }} data-id={id} data-testid={`rf__node-${id}`} @@ -248,8 +246,6 @@ export function NodeWrapper({ id={id} data={node.data} type={nodeType} - width={computedWidth} - height={computedHeight} positionAbsoluteX={positionAbsoluteX} positionAbsoluteY={positionAbsoluteY} selected={node.selected} @@ -259,6 +255,7 @@ export function NodeWrapper({ dragging={dragging} dragHandle={node.dragHandle} zIndex={zIndex} + {...nodeDimensions} /> diff --git a/packages/react/src/components/NodeWrapper/utils.tsx b/packages/react/src/components/NodeWrapper/utils.tsx index 5dd73964..f98b6bb6 100644 --- a/packages/react/src/components/NodeWrapper/utils.tsx +++ b/packages/react/src/components/NodeWrapper/utils.tsx @@ -5,7 +5,7 @@ import { InputNode } from '../Nodes/InputNode'; import { DefaultNode } from '../Nodes/DefaultNode'; import { GroupNode } from '../Nodes/GroupNode'; import { OutputNode } from '../Nodes/OutputNode'; -import type { NodeTypes } from '../../types'; +import type { Node, NodeTypes } from '../../types'; export const arrowKeyDiffs: Record = { ArrowUp: { x: 0, y: -1 }, @@ -20,3 +20,22 @@ export const builtinNodeTypes: NodeTypes = { output: OutputNode as ComponentType, group: GroupNode as ComponentType, }; + +export function getNodeInlineStyleDimensions( + node: NodeType +): { + width: number | string | undefined; + height: number | string | undefined; +} { + if (!node.computed) { + return { + width: node.width ?? node.initialWidth ?? node.style?.width, + height: node.height ?? node.initialHeight ?? node.style?.height, + }; + } + + return { + width: node.width ?? node.style?.width, + height: node.height ?? node.style?.height, + }; +} diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index 7917e3d6..6d92e8b4 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -65,6 +65,7 @@ const reactFlowFieldsToTrack = [ 'selectNodesOnDrag', 'nodeDragThreshold', 'onBeforeDelete', + 'debug', ] as const; type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number]; diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index c80552f8..88d8843e 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -139,6 +139,7 @@ function ReactFlow( width, height, colorMode = 'light', + debug, ...rest }: ReactFlowProps, ref: ForwardedRef @@ -274,6 +275,7 @@ function ReactFlow( selectNodesOnDrag={selectNodesOnDrag} nodeDragThreshold={nodeDragThreshold} onBeforeDelete={onBeforeDelete} + debug={debug} /> {children} diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 71a51a1e..823d5022 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -89,6 +89,7 @@ const createRFStore = ({ fitViewOnInitOptions, domNode, nodeOrigin, + debug, } = get(); const changes: NodeDimensionChange[] = []; @@ -130,6 +131,9 @@ const createRFStore = ({ set({ nodes: nextNodes, fitViewDone: nextFitViewDone }); if (changes?.length > 0) { + if (debug) { + console.log('React Flow: trigger node changes', changes); + } onNodesChange?.(changes); } }, @@ -149,7 +153,7 @@ const createRFStore = ({ get().triggerNodeChanges(changes); }, triggerNodeChanges: (changes) => { - const { onNodesChange, setNodes, nodes, hasDefaultNodes } = get(); + const { onNodesChange, setNodes, nodes, hasDefaultNodes, debug } = get(); if (changes?.length) { if (hasDefaultNodes) { @@ -157,11 +161,15 @@ const createRFStore = ({ setNodes(updatedNodes); } + if (debug) { + console.log('React Flow: trigger node changes', changes); + } + onNodesChange?.(changes); } }, triggerEdgeChanges: (changes) => { - const { onEdgesChange, setEdges, edges, hasDefaultEdges } = get(); + const { onEdgesChange, setEdges, edges, hasDefaultEdges, debug } = get(); if (changes?.length) { if (hasDefaultEdges) { @@ -169,6 +177,10 @@ const createRFStore = ({ setEdges(updatedEdges); } + if (debug) { + console.log('React Flow: trigger edge changes', changes); + } + onEdgesChange?.(changes); } }, diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 5a1f9562..0cf3c7f0 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -43,7 +43,9 @@ const getInitialState = ({ let transform: Transform = [0, 0, 1]; if (fitView && width && height) { - const nodesWithDimensions = nextNodes.filter((node) => node.width && node.height); + const nodesWithDimensions = nextNodes.filter( + (node) => (node.width || node.initialWidth) && (node.height || node.initialHeight) + ); // @todo users nodeOrigin should be used here const bounds = getNodesBounds(nodesWithDimensions, { nodeOrigin: [0, 0] }); const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1); @@ -113,6 +115,7 @@ const getInitialState = ({ onSelectionChangeHandlers: [], lib: 'react', + debug: false, }; }; diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 650d0b0c..e8d91ba4 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -503,6 +503,11 @@ export interface ReactFlowProps; lib: string; + debug: boolean; }; export type ReactFlowActions = { diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 0e8a5abf..948e69eb 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -4,11 +4,12 @@ ## ⚠️ Breaking changes -- `useNodesData` not only returns data objects but also the type and the id of the node. +- `useNodesData` not only returns data objects but also the type and the id of the node ## Patch changes -- better cursor defaults for the pane, nodes and edges. +- better cursor defaults for the pane, nodes and edges +- add `initialWidth` and `initialHeight` node attributes for specifying initial dimensions for ssr ## 0.0.36 diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index ae894066..91027f53 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -11,6 +11,7 @@ import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; import type { NodeWrapperProps } from './types'; import type { Node } from '$lib/types'; + import { getNodeInlineStyleDimensions } from './utils'; interface $$Props extends NodeWrapperProps {} @@ -34,6 +35,10 @@ export let sourcePosition: NodeWrapperProps['sourcePosition'] = undefined; export let targetPosition: NodeWrapperProps['targetPosition'] = undefined; export let zIndex: NodeWrapperProps['zIndex']; + export let computedWidth: NodeWrapperProps['computedWidth'] = undefined; + export let computedHeight: NodeWrapperProps['computedHeight'] = undefined; + export let initialWidth: NodeWrapperProps['initialWidth'] = undefined; + export let initialHeight: NodeWrapperProps['initialHeight'] = undefined; export let width: NodeWrapperProps['width'] = undefined; export let height: NodeWrapperProps['height'] = undefined; export let dragHandle: NodeWrapperProps['dragHandle'] = undefined; @@ -77,6 +82,15 @@ let prevSourcePosition: Position | undefined = undefined; let prevTargetPosition: Position | undefined = undefined; + $: inlineStyleDimensions = getNodeInlineStyleDimensions({ + width, + height, + initialWidth, + initialHeight, + computedWidth, + computedHeight + }); + $: { connectableStore.set(!!connectable); } @@ -170,9 +184,7 @@ style:z-index={zIndex} style:transform="translate({positionOriginX}px, {positionOriginY}px)" style:visibility={initialized ? 'visible' : 'hidden'} - style="{style ?? ''}; {!width ? '' : `width:${width}px;`} {!height - ? '' - : `height:${height}px;`}" + style="{inlineStyleDimensions.width} {inlineStyleDimensions.height} {style ?? ''};" on:click={onSelectNodeHandler} on:mouseenter={(event) => dispatch('nodemouseenter', { node, event })} on:mouseleave={(event) => dispatch('nodemouseleave', { node, event })} diff --git a/packages/svelte/src/lib/components/NodeWrapper/types.ts b/packages/svelte/src/lib/components/NodeWrapper/types.ts index 39fb0c10..a290ec08 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/types.ts +++ b/packages/svelte/src/lib/components/NodeWrapper/types.ts @@ -16,9 +16,13 @@ export type NodeWrapperProps = Pick< | 'targetPosition' | 'dragHandle' | 'hidden' + | 'width' + | 'height' + | 'initialWidth' + | 'initialHeight' > & { - width?: number; - height?: number; + computedWidth?: number; + computedHeight?: number; type: string; positionX: number; positionY: number; diff --git a/packages/svelte/src/lib/components/NodeWrapper/utils.ts b/packages/svelte/src/lib/components/NodeWrapper/utils.ts new file mode 100644 index 00000000..b2609587 --- /dev/null +++ b/packages/svelte/src/lib/components/NodeWrapper/utils.ts @@ -0,0 +1,33 @@ +export function getNodeInlineStyleDimensions({ + width, + height, + initialWidth, + initialHeight, + computedWidth, + computedHeight +}: { + width?: number; + height?: number; + initialWidth?: number; + initialHeight?: number; + computedWidth?: number; + computedHeight?: number; +}): { + width: string | undefined; + height: string | undefined; +} { + if (computedWidth === undefined && computedHeight === undefined) { + const styleWidth = width ?? initialWidth; + const styleHeight = height ?? initialHeight; + + return { + width: styleWidth ? `width:${styleWidth}px;` : '', + height: styleHeight ? `height:${styleHeight}px;` : '' + }; + } + + return { + width: width ? `width:${width}px;` : '', + height: height ? `height:${height}px;` : '' + }; +} diff --git a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte index df399a7d..8761942d 100644 --- a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte +++ b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte @@ -1,6 +1,11 @@