Skip to content

Commit b7a5599

Browse files
committed
..
1 parent 946874c commit b7a5599

File tree

4 files changed

+318
-3
lines changed

4 files changed

+318
-3
lines changed

docs/enterprise/react_flow/edges.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,124 @@ edges: list[Edge] = [
9494
}
9595
]
9696
```
97+
98+
# Custom Edges
99+
100+
React Flow in Reflex also allows you to define custom edge types. This is useful when you want edges to carry extra functionality (like buttons, labels, or dynamic styling) beyond the default straight or bezier connectors.
101+
102+
```python demo exec
103+
import reflex as rx
104+
import reflex_enterprise as rxe
105+
from reflex_enterprise.components.flow.types import (
106+
ConnectionInProgress,
107+
Edge,
108+
NoConnection,
109+
Node,
110+
Position,
111+
)
112+
113+
class SimpleEdgeDemoState(rx.State):
114+
nodes: list[Node] = [
115+
{"id": "1", "position": {"x": 0, "y": 0}, "data": {"label": "Node A"}, "style": {"color": "#000000",}},
116+
{"id": "2", "position": {"x": 250, "y": 150}, "data": {"label": "Node B"}, "style": {"color": "#000000",}},
117+
]
118+
edges: list[Edge] = [
119+
{"id": "e1-2", "source": "1", "target": "2", "type": "button"}
120+
]
121+
122+
@rx.event
123+
def set_nodes(self, nodes: list[Node]):
124+
self.nodes = nodes
125+
126+
@rx.event
127+
def set_edges(self, edges: list[Edge]):
128+
self.edges = edges
129+
130+
@rx.event
131+
def handle_connect_end(
132+
self,
133+
connection_status: NoConnection | ConnectionInProgress,
134+
):
135+
if not connection_status["isValid"]:
136+
new_edge = {
137+
"id": f"{connection_status['fromNode']['id']}-{connection_status['toNode']['id']}",
138+
"source": connection_status["fromNode"]["id"],
139+
"target": connection_status["toNode"]["id"],
140+
"type": "button",
141+
}
142+
self.edges.append(new_edge)
143+
144+
145+
146+
@rx.memo
147+
def button_edge(
148+
id: rx.Var[str],
149+
sourceX: rx.Var[float],
150+
sourceY: rx.Var[float],
151+
targetX: rx.Var[float],
152+
targetY: rx.Var[float],
153+
sourcePosition: rx.Var[Position],
154+
targetPosition: rx.Var[Position],
155+
markerEnd: rx.Var[str],
156+
):
157+
bezier_path = rxe.components.flow.util.get_bezier_path(
158+
source_x=sourceX,
159+
source_y=sourceY,
160+
target_x=targetX,
161+
target_y=targetY,
162+
source_position=sourcePosition,
163+
target_position=targetPosition,
164+
)
165+
166+
mid_x = bezier_path.label_x
167+
mid_y = bezier_path.label_y
168+
169+
return rx.fragment(
170+
rxe.flow.base_edge(path=bezier_path.path, markerEnd=markerEnd),
171+
rxe.flow.edge_label_renderer(
172+
rx.el.div(
173+
rx.el.button(
174+
"×",
175+
class_name=("w-[30px] h-[30px] border-2 border-gray-200 bg-gray-200 text-black rounded-full text-[12px] pt-0 cursor-pointer hover:bg-gray-400 hover:text-white"),
176+
on_click=rx.run_script(
177+
rxe.flow.api.set_edges(
178+
rx.vars.FunctionStringVar.create(
179+
"Array.prototype.filter.call"
180+
).call(
181+
rxe.flow.api.get_edges(),
182+
rx.Var(f"((edge) => edge.id !== {id})"),
183+
),
184+
)
185+
),
186+
style={
187+
"position": "absolute",
188+
"left": f"{mid_x}px",
189+
"top": f"{mid_y}px",
190+
"transform": "translate(-50%, -50%)",
191+
"pointerEvents": "all",
192+
},
193+
),
194+
)
195+
),
196+
)
197+
198+
def very_simple_custom_edge_example():
199+
return rx.box(
200+
rxe.flow(
201+
rxe.flow.background(),
202+
default_nodes=SimpleEdgeDemoState.nodes,
203+
default_edges=SimpleEdgeDemoState.edges,
204+
nodes=SimpleEdgeDemoState.nodes,
205+
edges=SimpleEdgeDemoState.edges,
206+
on_connect=lambda connection: SimpleEdgeDemoState.set_edges(
207+
rxe.flow.util.add_edge(connection, SimpleEdgeDemoState.edges)
208+
),
209+
on_connect_end=lambda status, event: SimpleEdgeDemoState.handle_connect_end(status),
210+
edge_types={"button": button_edge},
211+
fit_view=True,
212+
),
213+
height="100vh",
214+
width="100%",
215+
)
216+
217+
```

docs/enterprise/react_flow/interactivity.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ def interactive_flow():
5353
rxe.flow.controls(),
5454
rxe.flow.background(),
5555
rxe.flow.mini_map(),
56-
5756
nodes=FlowState.nodes,
5857
edges=FlowState.edges,
5958
on_nodes_change=lambda node_changes: FlowState.set_nodes(

docs/enterprise/react_flow/nodes.md

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,198 @@ node_with_handles = {
8383
"targetPosition": "left"
8484
}
8585
```
86+
87+
# Custom Nodes
88+
89+
Creating custom nodes is as easy as building a regular React component and passing it to the `node_types`. Since they’re standard React components, you can display any content and implement any functionality you need. Plus, you’ll have access to a range of props that allow you to extend and customize the default node behavior.
90+
91+
Below is an example custom node using a `color picker` component.
92+
93+
```python demo exec
94+
95+
from typing import Any
96+
97+
import reflex as rx
98+
99+
import reflex_enterprise as rxe
100+
from reflex_enterprise.components.flow.types import Connection, Edge, Node
101+
102+
103+
class CustomNodeState(rx.State):
104+
bg_color: rx.Field[str] = rx.field(default="#c9f1dd")
105+
nodes: rx.Field[list[Node]] = rx.field(
106+
default_factory=lambda: [
107+
{
108+
"id": "1",
109+
"type": "input",
110+
"data": {"label": "An input node"},
111+
"position": {"x": 0, "y": 50},
112+
"sourcePosition": "right",
113+
"style": {"color": "#000000",}
114+
},
115+
{
116+
"id": "2",
117+
"type": "selectorNode",
118+
"data": {
119+
"color": "#c9f1dd",
120+
},
121+
"position": {"x": 300, "y": 50},
122+
},
123+
{
124+
"id": "3",
125+
"type": "output",
126+
"data": {"label": "Output A"},
127+
"position": {"x": 650, "y": 25},
128+
"targetPosition": "left",
129+
"style": {"color": "#000000",}
130+
},
131+
{
132+
"id": "4",
133+
"type": "output",
134+
"data": {"label": "Output B"},
135+
"position": {"x": 650, "y": 100},
136+
"targetPosition": "left",
137+
"style": {"color": "#000000",}
138+
},
139+
]
140+
)
141+
edges: rx.Field[list[Edge]] = rx.field(
142+
default_factory=lambda: [
143+
{
144+
"id": "e1-2",
145+
"source": "1",
146+
"target": "2",
147+
"animated": True,
148+
},
149+
{
150+
"id": "e2a-3",
151+
"source": "2",
152+
"target": "3",
153+
"animated": True,
154+
},
155+
{
156+
"id": "e2b-4",
157+
"source": "2",
158+
"target": "4",
159+
"animated": True,
160+
},
161+
]
162+
)
163+
164+
@rx.event
165+
def on_change_color(self, color: str):
166+
self.nodes = [
167+
node
168+
if node["id"] != "2" or "data" not in node
169+
else {**node, "data": {**node["data"], "color": color}}
170+
for node in self.nodes
171+
]
172+
self.bg_color = color
173+
174+
@rx.event
175+
def set_nodes(self, nodes: list[Node]):
176+
self.nodes = nodes
177+
178+
@rx.event
179+
def set_edges(self, edges: list[Edge]):
180+
self.edges = edges
181+
182+
183+
@rx.memo
184+
def color_selector_node(data: rx.Var[dict[str, Any]], isConnectable: rx.Var[bool]): # noqa: N803
185+
data = data.to(dict)
186+
return rx.el.div(
187+
rxe.flow.handle(
188+
type="target",
189+
position="left",
190+
on_connect=lambda params: rx.console_log(f"handle onConnect {params}"),
191+
is_connectable=isConnectable,
192+
),
193+
rx.el.div(
194+
"Custom Color Picker Node: ",
195+
rx.el.strong(data["color"]),
196+
),
197+
rx.el.input(
198+
class_name="nodrag",
199+
type="color",
200+
on_change=CustomNodeState.on_change_color,
201+
default_value=data["color"],
202+
),
203+
rxe.flow.handle(
204+
type="source",
205+
position="right",
206+
is_connectable=isConnectable,
207+
),
208+
class_name="border border-1 p-2 rounded-sm",
209+
border_color=rx.color_mode_cond("black", ""),
210+
color="black",
211+
bg="white",
212+
)
213+
214+
215+
def node_stroke_color(node: rx.vars.ObjectVar[Node]):
216+
return rx.match(
217+
node["type"],
218+
("input", "#0041d0"),
219+
(
220+
"selectorNode",
221+
CustomNodeState.bg_color,
222+
),
223+
("output", "#ff0072"),
224+
None,
225+
)
226+
227+
228+
def node_color(node: rx.vars.ObjectVar[Node]):
229+
return rx.match(
230+
node["type"],
231+
(
232+
"selectorNode",
233+
CustomNodeState.bg_color,
234+
),
235+
"#fff",
236+
)
237+
238+
239+
@rx.page(route="/nodes/custom-node", title="Custom Node Demo")
240+
def custom_node():
241+
return rx.box(
242+
rxe.flow(
243+
rxe.flow.background(bg_color=CustomNodeState.bg_color),
244+
rxe.flow.mini_map(
245+
node_stroke_color=rx.vars.function.ArgsFunctionOperation.create(
246+
("node",), node_stroke_color(rx.Var("node").to(Node))
247+
),
248+
node_color=rx.vars.function.ArgsFunctionOperation.create(
249+
("node",), node_color(rx.Var("node").to(Node))
250+
),
251+
),
252+
rxe.flow.controls(),
253+
default_nodes=CustomNodeState.nodes,
254+
default_edges=CustomNodeState.edges,
255+
nodes=CustomNodeState.nodes,
256+
edges=CustomNodeState.edges,
257+
on_nodes_change=lambda changes: CustomNodeState.set_nodes(
258+
rxe.flow.util.apply_node_changes(CustomNodeState.nodes, changes)
259+
),
260+
on_edges_change=lambda changes: CustomNodeState.set_edges(
261+
rxe.flow.util.apply_edge_changes(CustomNodeState.edges, changes)
262+
),
263+
on_connect=lambda connection: CustomNodeState.set_edges(
264+
rxe.flow.util.add_edge(
265+
connection.to(dict).merge({"animated": True}).to(Connection),
266+
CustomNodeState.edges,
267+
)
268+
),
269+
node_types={"selectorNode": color_selector_node},
270+
color_mode="light",
271+
snap_grid=(20, 20),
272+
default_viewport={"x": 0, "y": 0, "zoom": 1.5},
273+
snap_to_grid=True,
274+
attribution_position="bottom-left",
275+
fit_view=True,
276+
),
277+
height="100vh",
278+
width="100vw",
279+
)
280+
```

uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)