Skip to content

Instantly share code, notes, and snippets.

@epicbytes
Last active October 22, 2022 05:41
Show Gist options
  • Save epicbytes/b28761eb9800ba9f53622ef2b7690ce8 to your computer and use it in GitHub Desktop.
Save epicbytes/b28761eb9800ba9f53622ef2b7690ce8 to your computer and use it in GitHub Desktop.
Example how to use reactflow and effector together
import { NodeTypes } from "@reactflow/core/dist/esm/types";
import { Edge, Node } from "reactflow";
export type FlowEditorProps = {
nodeTypes?: NodeTypes;
nodes?: Node[];
edges?: Edge[];
library: FlowEditorLibrary
children?: JSX.Element;
};
export type FlowEditorLibrarySection = {
name: string
items: FlowEditorLibraryElement[]
}
export type FlowEditorLibraryElement = any
export type FlowEditorLibrary = FlowEditorLibrarySection[]
import React, { ReactElement, useCallback, useRef } from "react";
import type { FlowEditorProps } from "./flow-editor.d";
import { flowEditorModel } from "@/components/complex/flow-editor/model";
import { modelView } from "effector-factorio";
import { ReactFlow, NodeAddChange } from "reactflow";
import "reactflow/dist/style.css";
import { useGate, useStore } from "effector-react";
export const FlowEditor = <T extends {}>(
props: FlowEditorProps
): ReactElement => {
const model = flowEditorModel.createModel(props);
useGate(model.FlowEditorGate);
return <FlowEditorElement model={model} />;
};
export const FlowEditorElement = modelView(flowEditorModel, () => {
const reactFlowWrapper = useRef(null);
const {
$instance,
$nodes,
$edges,
onConnect,
onInit,
onNodesChange,
onEdgesChange,
onConnectStart,
nodeTypes,
library,
} = flowEditorModel.useModel();
const nodes = useStore($nodes);
const edges = useStore($edges);
const reactFlowInstance = useStore($instance);
const onDragStart = (event, nodeType, item) => {
event.dataTransfer.setData("application/reactflow", nodeType);
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("item", JSON.stringify(item));
};
const onDrop = useCallback(
(event) => {
event.preventDefault();
// @ts-ignore
const reactFlowBounds = reactFlowWrapper.current?.getBoundingClientRect();
const type = event.dataTransfer.getData("application/reactflow");
const item = JSON.parse(event.dataTransfer.getData("item"));
if (typeof type === "undefined" || !type) {
return;
}
const position = reactFlowInstance?.project({
x: event.clientX - reactFlowBounds.left,
y: event.clientY - reactFlowBounds.top,
});
const newNode = {
id: item.name,
type,
position,
data: item,
};
onNodesChange([{ item: newNode, type: "add" } as NodeAddChange]);
},
[reactFlowInstance]
);
const onDragOver = useCallback((event) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}, []);
return (
<div className={"grid h-[500px] w-full grid-cols-[200px,1fr]"}>
<div>
{library.map((section, index) => (
<ul className={"rounded-box bg-base-100 p-2"}>
<li key={index}>
<span>{section.name}</span>
<ul className={"rounded-box bg-base-100 p-2"}>
{section.items.map((item, index2) => (
<li
key={index2}
draggable
className={"my-1"}
onDragStart={(event) =>
onDragStart(event, "activity", item)
}
>
<span>{item.name}</span>
</li>
))}
</ul>
</li>
</ul>
))}
</div>
<div className={"h-full"} ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
onInit={onInit}
onDrop={onDrop}
onDragOver={onDragOver}
onConnect={onConnect}
onConnectStart={(_, payload) => onConnectStart(payload)}
onConnectEnd={() => onConnectStart(null)}
fitView
attributionPosition="top-right"
/>
</div>
</div>
);
});
import { modelFactory } from "effector-factorio";
import { combine, createEvent, createStore } from "effector";
import type {
Edge,
EdgeChange,
Node,
NodeDimensionChange,
NodePositionChange,
NodeSelectionChange,
Connection,
NodeChange,
NodeRemoveChange,
ReactFlowInstance,
EdgeRemoveChange,
EdgeSelectionChange,
EdgeAddChange,
OnConnectStartParams,
} from "reactflow";
import { createGate } from "effector-react";
const flowEditorModel = modelFactory((props) => {
const FlowEditorGate = createGate();
const onNodesChange = createEvent<NodeChange[]>();
const onEdgesChange = createEvent<EdgeChange[]>();
const onConnect = createEvent<Connection>();
const onInit = createEvent<ReactFlowInstance>();
const onConnectStart = createEvent<OnConnectStartParams | null>();
const $activeHandleSource = createStore<OnConnectStartParams | null>(null).on(
onConnectStart,
(state, payload) => payload
);
const $instance = createStore<ReactFlowInstance | null>(null).on(
onInit,
(state, payload) => payload
);
const $nodes = createStore<Node[]>(props.nodes || []).on(
onNodesChange,
(state, payload) => {
for (let item of payload) {
switch (item.type) {
case "remove":
state = state.filter(
(stateItem) => stateItem.id != (item as NodeRemoveChange).id
);
break;
case "dimensions":
state = state.map((stateItem) =>
stateItem.id == (item as NodeDimensionChange).id
? {
...stateItem,
dimensions: (item as NodeDimensionChange).dimensions,
}
: stateItem
);
break;
case "position":
state = state.map((stateItem) =>
stateItem.id == (item as NodePositionChange).id &&
(item as NodePositionChange).dragging
? ({
...stateItem,
position: (item as NodePositionChange).position,
positionAbsolute: (item as NodePositionChange)
.positionAbsolute,
dragging: (item as NodePositionChange).dragging,
} as Node)
: stateItem
);
break;
case "select":
state = state.map((stateItem) =>
stateItem.id == (item as NodeSelectionChange).id
? {
...stateItem,
selected: (item as NodeSelectionChange).selected,
}
: { ...stateItem, selected: false }
);
break;
case "add":
state = [...state, item.item];
break;
case "reset":
break;
}
return state;
}
}
);
const $edges = createStore<Edge[]>(props.edges || [])
.on(onEdgesChange, (state, payload) => {
for (let item of payload) {
switch (item.type) {
case "add":
const itm = (item as EdgeAddChange).item;
state = state.some(
(edge) => edge.target === itm.target && edge.source === itm.source
)
? state
: [...state, item.item];
break;
case "remove":
state = state.filter(
(stateItem) => stateItem.id != (item as EdgeRemoveChange).id
);
break;
case "reset":
break;
case "select":
state = state.map((stateItem) =>
stateItem.id == (item as EdgeSelectionChange).id
? {
...stateItem,
selected: (item as EdgeSelectionChange).selected,
}
: { ...stateItem, selected: false }
);
break;
}
}
return state;
})
.on(onConnect, (state, payload) => {
return [
...state,
{
animated: true,
id: `edge-${payload.source}_${payload.target}`,
...payload,
},
] as Edge[];
});
const mappingType = {
target: "input_fields",
source: "output_fields",
};
const $activeConnector = combine(
$nodes,
$activeHandleSource,
(nodes, handle) => {
if (!handle) return null;
const node = nodes.find((item) => item.id === handle.nodeId);
if (!node) return null;
return node.data[mappingType[handle.handleType as string]].find(
(item) => item.name === handle.handleId
);
}
);
return {
FlowEditorGate,
onNodesChange,
onEdgesChange,
onInit,
onConnect,
onConnectStart,
$activeHandleSource,
$instance,
$nodes,
$edges,
$activeConnector,
nodeTypes: props.nodeTypes,
library: props.library,
};
});
export { flowEditorModel };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment