Skip to content

EdgeSpec missing sourceHandle / targetHandle #25

@MarcSkovMadsen

Description

@MarcSkovMadsen

EdgeSpec missing sourceHandle / targetHandle

Description

When a node defines multiple named handles (e.g., inputs=["text", "mode"]), there is no way to route an edge to a specific handle using EdgeSpec. The sourceHandle and targetHandle fields are standard React Flow edge properties but are not exposed in the Python EdgeSpec dataclass.

Root Cause

EdgeSpec in src/panel_reactflow/base.py (line 268-304) does not include sourceHandle or targetHandle fields. The to_dict() method only serializes id, source, target, label, type, selected, data, style, and markerEnd.

Minimal Reproducible Example

import panel as pn
from panel_reactflow import ReactFlow, NodeSpec, EdgeSpec, NodeType

pn.extension("jsoneditor")

node_types = {
    "producer": NodeType(type="producer", label="Producer", inputs=[], outputs=["result"]),
    "consumer": NodeType(type="consumer", label="Consumer", inputs=["text", "mode"], outputs=[]),
}

flow = ReactFlow(
    nodes=[
        NodeSpec(id="p", type="producer", position={"x": 0, "y": 0}, label="Producer").to_dict(),
        NodeSpec(id="c", type="consumer", position={"x": 400, "y": 0}, label="Consumer").to_dict(),
    ],
    edges=[
        # Want to connect producer's "result" output to consumer's "mode" input
        # EdgeSpec has no sourceHandle/targetHandle fields:
        EdgeSpec(id="e1", source="p", target="c").to_dict(),
        # Workaround: use raw dict instead of EdgeSpec
        {"id": "e2", "source": "p", "target": "c", "sourceHandle": "result", "targetHandle": "mode"},
    ],
    node_types=node_types,
    sizing_mode="stretch_both",
    min_height=300,
)
flow.servable()
Image

Expected: EdgeSpec should accept sourceHandle and targetHandle parameters.

Actual: Must bypass EdgeSpec and use a raw dict to set these fields.

Suggested Fix

Add optional fields to EdgeSpec:

@dataclass
class EdgeSpec:
    # ... existing fields ...
    sourceHandle: str | None = None
    targetHandle: str | None = None

    def to_dict(self):
        payload = { ... }
        if self.sourceHandle is not None:
            payload["sourceHandle"] = self.sourceHandle
        if self.targetHandle is not None:
            payload["targetHandle"] = self.targetHandle
        return payload

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions