feat(react): ssr

This commit is contained in:
moklick
2023-10-05 17:28:05 +02:00
parent f37a2d9a19
commit 1acc59e44c
10 changed files with 90 additions and 22 deletions
@@ -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}
+12 -2
View File
@@ -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 });
+33 -3
View File
@@ -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;
+2
View File
@@ -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;
}; };