feat(react): ssr
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useEffect, useState, type FC, type PropsWithChildren } from 'react';
|
import { memo, type FC, type PropsWithChildren } from 'react';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
|
|
||||||
@@ -37,18 +37,9 @@ const Controls: FC<PropsWithChildren<ControlProps>> = ({
|
|||||||
position = 'bottom-left',
|
position = 'bottom-left',
|
||||||
}) => {
|
}) => {
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
|
||||||
const { isInteractive, minZoomReached, maxZoomReached } = useStore(selector, shallow);
|
const { isInteractive, minZoomReached, maxZoomReached } = useStore(selector, shallow);
|
||||||
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsVisible(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onZoomInHandler = () => {
|
const onZoomInHandler = () => {
|
||||||
zoomIn();
|
zoomIn();
|
||||||
onZoomIn?.();
|
onZoomIn?.();
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
|||||||
disableKeyboardA11y,
|
disableKeyboardA11y,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
rfId,
|
rfId,
|
||||||
|
dimensionWidth,
|
||||||
|
dimensionHeight,
|
||||||
}: WrapNodeProps) => {
|
}: WrapNodeProps) => {
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const nodeRef = useRef<HTMLDivElement>(null);
|
const nodeRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -181,7 +183,9 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
|||||||
zIndex,
|
zIndex,
|
||||||
transform: `translate(${xPosOrigin}px,${yPosOrigin}px)`,
|
transform: `translate(${xPosOrigin}px,${yPosOrigin}px)`,
|
||||||
pointerEvents: hasPointerEvents ? 'all' : 'none',
|
pointerEvents: hasPointerEvents ? 'all' : 'none',
|
||||||
visibility: initialized ? 'visible' : 'visible',
|
visibility: initialized ? 'visible' : 'hidden',
|
||||||
|
width: dimensionWidth,
|
||||||
|
height: dimensionHeight,
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
data-id={id}
|
data-id={id}
|
||||||
|
|||||||
@@ -6,11 +6,23 @@ import { Provider } from '../../contexts/RFStoreContext';
|
|||||||
import { createRFStore } from '../../store';
|
import { createRFStore } from '../../store';
|
||||||
import type { ReactFlowState, Node, Edge } from '../../types';
|
import type { ReactFlowState, Node, Edge } from '../../types';
|
||||||
|
|
||||||
function ReactFlowProvider({ children, nodes, edges }: { children: ReactNode; nodes?: Node[]; edges?: Edge[] }) {
|
function ReactFlowProvider({
|
||||||
|
children,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
nodes?: Node[];
|
||||||
|
edges?: Edge[];
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}) {
|
||||||
const storeRef = useRef<UseBoundStoreWithEqualityFn<StoreApi<ReactFlowState>> | null>(null);
|
const storeRef = useRef<UseBoundStoreWithEqualityFn<StoreApi<ReactFlowState>> | null>(null);
|
||||||
|
|
||||||
if (!storeRef.current) {
|
if (!storeRef.current) {
|
||||||
storeRef.current = createRFStore({ nodes, edges });
|
storeRef.current = createRFStore({ nodes, edges, width, height });
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Provider value={storeRef.current}>{children}</Provider>;
|
return <Provider value={storeRef.current}>{children}</Provider>;
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
|
|||||||
height: node.height ?? 0,
|
height: node.height ?? 0,
|
||||||
origin: node.origin || props.nodeOrigin,
|
origin: node.origin || props.nodeOrigin,
|
||||||
});
|
});
|
||||||
|
const initialized = (!!node.width && !!node.height) || (!!node.dimensions?.width && !!node.dimensions?.height);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeComponent
|
<NodeComponent
|
||||||
@@ -105,6 +106,8 @@ const NodeRenderer = (props: NodeRendererProps) => {
|
|||||||
id={node.id}
|
id={node.id}
|
||||||
className={node.className}
|
className={node.className}
|
||||||
style={node.style}
|
style={node.style}
|
||||||
|
dimensionWidth={node.dimensions?.width}
|
||||||
|
dimensionHeight={node.dimensions?.height}
|
||||||
type={nodeType}
|
type={nodeType}
|
||||||
data={node.data}
|
data={node.data}
|
||||||
sourcePosition={node.sourcePosition || Position.Bottom}
|
sourcePosition={node.sourcePosition || Position.Bottom}
|
||||||
@@ -131,7 +134,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
|
|||||||
isParent={!!node[internalsSymbol]?.isParent}
|
isParent={!!node[internalsSymbol]?.isParent}
|
||||||
noDragClassName={props.noDragClassName}
|
noDragClassName={props.noDragClassName}
|
||||||
noPanClassName={props.noPanClassName}
|
noPanClassName={props.noPanClassName}
|
||||||
initialized={!!node.width && !!node.height}
|
initialized={initialized}
|
||||||
rfId={props.rfId}
|
rfId={props.rfId}
|
||||||
disableKeyboardA11y={props.disableKeyboardA11y}
|
disableKeyboardA11y={props.disableKeyboardA11y}
|
||||||
ariaLabel={node.ariaLabel}
|
ariaLabel={node.ariaLabel}
|
||||||
|
|||||||
@@ -4,7 +4,19 @@ import StoreContext from '../../contexts/RFStoreContext';
|
|||||||
import ReactFlowProvider from '../../components/ReactFlowProvider';
|
import ReactFlowProvider from '../../components/ReactFlowProvider';
|
||||||
import type { Node, Edge } from '../../types';
|
import type { Node, Edge } from '../../types';
|
||||||
|
|
||||||
function Wrapper({ children, nodes, edges }: { children: ReactNode; nodes?: Node[]; edges?: Edge[] }) {
|
function Wrapper({
|
||||||
|
children,
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
nodes?: Node[];
|
||||||
|
edges?: Edge[];
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}) {
|
||||||
const isWrapped = useContext(StoreContext);
|
const isWrapped = useContext(StoreContext);
|
||||||
|
|
||||||
if (isWrapped) {
|
if (isWrapped) {
|
||||||
@@ -14,7 +26,7 @@ function Wrapper({ children, nodes, edges }: { children: ReactNode; nodes?: Node
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlowProvider nodes={nodes} edges={edges}>
|
<ReactFlowProvider nodes={nodes} edges={edges} width={width} height={height}>
|
||||||
{children}
|
{children}
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
|||||||
nodeDragThreshold,
|
nodeDragThreshold,
|
||||||
viewport,
|
viewport,
|
||||||
onViewportChange,
|
onViewportChange,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
@@ -181,7 +183,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
|||||||
data-testid="rf__wrapper"
|
data-testid="rf__wrapper"
|
||||||
id={id}
|
id={id}
|
||||||
>
|
>
|
||||||
<Wrapper nodes={nodes} edges={edges}>
|
<Wrapper nodes={nodes} edges={edges} width={width} height={height}>
|
||||||
<GraphView
|
<GraphView
|
||||||
onInit={onInit}
|
onInit={onInit}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
|
|||||||
@@ -24,10 +24,20 @@ import type {
|
|||||||
FitViewOptions,
|
FitViewOptions,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
const createRFStore = ({ nodes, edges }: { nodes?: Node[]; edges?: Edge[] }) =>
|
const createRFStore = ({
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}: {
|
||||||
|
nodes?: Node[];
|
||||||
|
edges?: Edge[];
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}) =>
|
||||||
createWithEqualityFn<ReactFlowState>(
|
createWithEqualityFn<ReactFlowState>(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
...getInitialState({ nodes, edges }),
|
...getInitialState({ nodes, edges, width, height }),
|
||||||
setNodes: (nodes: Node[]) => {
|
setNodes: (nodes: Node[]) => {
|
||||||
const { nodes: storeNodes, nodeOrigin, elevateNodesOnSelect } = get();
|
const { nodes: storeNodes, nodeOrigin, elevateNodesOnSelect } = get();
|
||||||
const nextNodes = updateNodes(nodes, storeNodes, { nodeOrigin, elevateNodesOnSelect });
|
const nextNodes = updateNodes(nodes, storeNodes, { nodeOrigin, elevateNodesOnSelect });
|
||||||
|
|||||||
@@ -1,15 +1,45 @@
|
|||||||
import { devWarn, infiniteExtent, ConnectionMode, updateNodes } from '@xyflow/system';
|
import {
|
||||||
|
devWarn,
|
||||||
|
infiniteExtent,
|
||||||
|
ConnectionMode,
|
||||||
|
updateNodes,
|
||||||
|
getRectOfNodes,
|
||||||
|
getTransformForBounds,
|
||||||
|
Transform,
|
||||||
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import type { Edge, Node, ReactFlowStore } from '../types';
|
import type { Edge, Node, ReactFlowStore } from '../types';
|
||||||
|
|
||||||
const getInitialState = ({ nodes = [], edges = [] }: { nodes?: Node[]; edges?: Edge[] }): ReactFlowStore => {
|
const getInitialState = ({
|
||||||
|
nodes = [],
|
||||||
|
edges = [],
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}: {
|
||||||
|
nodes?: Node[];
|
||||||
|
edges?: Edge[];
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}): ReactFlowStore => {
|
||||||
const nextNodes = updateNodes(nodes, [], { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
|
const nextNodes = updateNodes(nodes, [], { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
|
||||||
|
|
||||||
|
let transform: Transform = [0, 0, 1];
|
||||||
|
|
||||||
|
if (width && height) {
|
||||||
|
const nodesWithDimensions = nextNodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
width: node.dimensions?.width,
|
||||||
|
height: node.dimensions?.height,
|
||||||
|
}));
|
||||||
|
const bounds = getRectOfNodes(nodesWithDimensions, [0, 0]);
|
||||||
|
transform = getTransformForBounds(bounds, width, height, 0.5, 2, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rfId: '1',
|
rfId: '1',
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
transform: [0, 0, 1],
|
transform,
|
||||||
nodes: nextNodes,
|
nodes: nextNodes,
|
||||||
edges: edges,
|
edges: edges,
|
||||||
onNodesChange: null,
|
onNodesChange: null,
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
|
|||||||
onError?: OnError;
|
onError?: OnError;
|
||||||
isValidConnection?: IsValidConnection;
|
isValidConnection?: IsValidConnection;
|
||||||
nodeDragThreshold?: number;
|
nodeDragThreshold?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReactFlowRefType = HTMLDivElement;
|
export type ReactFlowRefType = HTMLDivElement;
|
||||||
|
|||||||
@@ -40,4 +40,6 @@ export type WrapNodeProps<NodeData = any> = Pick<
|
|||||||
noPanClassName: string;
|
noPanClassName: string;
|
||||||
rfId: string;
|
rfId: string;
|
||||||
disableKeyboardA11y: boolean;
|
disableKeyboardA11y: boolean;
|
||||||
|
dimensionWidth?: number;
|
||||||
|
dimensionHeight?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user