diff --git a/docs/how-to/embed-views-in-nodes.md b/docs/how-to/embed-views-in-nodes.md index 507e4e3..242ef23 100644 --- a/docs/how-to/embed-views-in-nodes.md +++ b/docs/how-to/embed-views-in-nodes.md @@ -50,13 +50,17 @@ flow = ReactFlow(nodes=nodes, edges=[], sizing_mode="stretch_both") ## Add a view when adding a node at runtime -`add_node()` accepts an optional `view` keyword argument: +Pass a `NodeSpec` with a `view` to `add_node()`: ```python -flow.add_node( - {"id": "live", "label": "Live Feed", "position": {"x": 600, "y": 0}, "data": {}}, +from panel_reactflow import NodeSpec + +flow.add_node(NodeSpec( + id="live", + label="Live Feed", + position={"x": 600, "y": 0}, view=pn.indicators.Number(value=42, name="Metric"), -) +)) ``` --- diff --git a/src/panel_reactflow/base.py b/src/panel_reactflow/base.py index a683be1..a6798c9 100644 --- a/src/panel_reactflow/base.py +++ b/src/panel_reactflow/base.py @@ -414,6 +414,9 @@ class NodeSpec: ``{"backgroundColor": "#ff0000", "border": "2px solid black"}`` className : str, optional CSS class name applied to the node for custom styling. + view : Panel viewable, optional + Optional Panel viewable (widget, pane, layout) to render inside + the node. The view will be displayed as the node's content. Methods ------- @@ -454,6 +457,16 @@ class NodeSpec: ... data={"operation": "filter", "threshold": 0.5} ... ) + Create a node with an embedded view: + + >>> import panel as pn + >>> node = NodeSpec( + ... id="plot1", + ... position={"x": 400, "y": 200}, + ... label="Data Plot", + ... view=pn.pane.Markdown("# Hello World") + ... ) + Add to a ReactFlow graph: >>> from panel_reactflow import ReactFlow @@ -472,6 +485,7 @@ class NodeSpec: deletable: bool = True style: dict[str, Any] | None = None className: str | None = None + view: Any | None = None def __post_init__(self) -> None: if self.position is None: @@ -502,6 +516,8 @@ def to_dict(self) -> dict[str, Any]: payload["style"] = self.style if self.className is not None: payload["className"] = self.className + if self.view is not None: + payload["view"] = self.view return payload @classmethod @@ -1372,12 +1388,12 @@ def _process_param_change(self, params): params.pop("_edge_editors", None) return params - def add_node(self, node: dict[str, Any] | NodeSpec, *, view: Any | None = None) -> None: + def add_node(self, node: dict[str, Any] | NodeSpec) -> None: """Add a node to the graph. Adds a new node to the graph with optional validation. If a ``view`` - is provided, it will be embedded inside the node and rendered as - Panel content. + is included in the node dict or :class:`NodeSpec`, it will be embedded + inside the node and rendered as Panel content. Parameters ---------- @@ -1390,9 +1406,6 @@ def add_node(self, node: dict[str, Any] | NodeSpec, *, view: Any | None = None) (defaults to ``{"x": 0.0, "y": 0.0}``) - ``type``: Node type (defaults to ``"panel"``) - ``data``: Custom data dict (defaults to ``{}``) - view : Panel viewable, optional - Optional Panel viewable (widget, pane, layout) to render inside - the node. The view will be displayed as the node's content. Raises ------ @@ -1422,13 +1435,14 @@ def add_node(self, node: dict[str, Any] | NodeSpec, *, view: Any | None = None) ... label="Another Node" ... )) - Add a node with embedded view: + Add a node with embedded view via NodeSpec: >>> import panel as pn - >>> flow.add_node( - ... NodeSpec(id="plot1", position={"x": 200, "y": 0}), + >>> flow.add_node(NodeSpec( + ... id="plot1", + ... position={"x": 200, "y": 0}, ... view=pn.pane.Markdown("# Hello World") - ... ) + ... )) Add a typed node with data: @@ -1452,7 +1466,7 @@ def add_node(self, node: dict[str, Any] | NodeSpec, *, view: Any | None = None) if self.validate_on_add: schema = self._get_node_schema(payload.get("type", "panel")) _validate_data(payload.get("data", {}), schema) - self.nodes = self.nodes + [dict(payload, view=view)] + self.nodes = self.nodes + [payload] self._emit("node_added", {"type": "node_added", "node": payload}) def _handle_msg(self, msg: dict[str, Any]) -> None: diff --git a/tests/test_api.py b/tests/test_api.py index 86ea185..c54d7ac 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -35,6 +35,45 @@ def test_node_spec_roundtrip() -> None: assert NodeSpec.from_dict(payload).to_dict() == payload +def test_node_spec_with_view() -> None: + view = pn.pane.Markdown("Hello World") + node = NodeSpec( + id="n1", + position={"x": 1, "y": 2}, + label="Node 1", + view=view, + ) + payload = node.to_dict() + assert payload["id"] == "n1" + assert payload["label"] == "Node 1" + assert payload["view"] is view + # Test roundtrip with view + node_from_dict = NodeSpec.from_dict(payload) + assert node_from_dict.view is view + assert node_from_dict.to_dict() == payload + + +def test_node_spec_without_view() -> None: + """Test that view is not included in dict when None.""" + node = NodeSpec( + id="n1", + position={"x": 1, "y": 2}, + label="Node 1", + ) + payload = node.to_dict() + assert "view" not in payload + + +def test_reactflow_add_node_with_nodespec_view() -> None: + """Test that NodeSpec view is preserved when passed to add_node.""" + flow = ReactFlow() + view = pn.pane.Markdown("Hello") + node = NodeSpec(id="n1", position={"x": 0, "y": 0}, label="Node", view=view) + flow.add_node(node) + assert len(flow.nodes) == 1 + assert flow.nodes[0]["view"] is view + + def test_edge_spec_roundtrip() -> None: edge = EdgeSpec(id="e1", source="n1", target="n2", data={"weight": 0.5}) payload = edge.to_dict() @@ -60,15 +99,15 @@ def test_reactflow_add_node_with_view() -> None: events = [] flow.on("node_added", events.append) view = pn.pane.Markdown("Hello") - flow.add_node({"id": "n1", "position": {"x": 0, "y": 0}, "label": "Pane", "data": {}}, view=view) + flow.add_node({"id": "n1", "position": {"x": 0, "y": 0}, "label": "Pane", "data": {}, "view": view}) assert events[-1]["type"] == "node_added" def test_view_idx_updates_on_remove_node(document, comm) -> None: flow = ReactFlow() - flow.add_node({"id": "n1", "position": {"x": 0, "y": 0}, "data": {}}, view=pn.pane.Markdown("A")) - flow.add_node({"id": "n2", "position": {"x": 1, "y": 1}, "data": {}}, view=pn.pane.Markdown("B")) - flow.add_node({"id": "n3", "position": {"x": 2, "y": 2}, "data": {}}, view=None) + flow.add_node({"id": "n1", "position": {"x": 0, "y": 0}, "data": {}, "view": pn.pane.Markdown("A")}) + flow.add_node({"id": "n2", "position": {"x": 1, "y": 1}, "data": {}, "view": pn.pane.Markdown("B")}) + flow.add_node({"id": "n3", "position": {"x": 2, "y": 2}, "data": {}}) model = flow.get_root(document, comm=comm)