Skip to content

Fix: Distinguish empty handle arrays from undefined to allow hiding ports#37

Merged
philippjfr merged 2 commits intomainfrom
copilot/fix-hide-port-handles
Feb 11, 2026
Merged

Fix: Distinguish empty handle arrays from undefined to allow hiding ports#37
philippjfr merged 2 commits intomainfrom
copilot/fix-hide-port-handles

Conversation

Copy link
Contributor

Copilot AI commented Feb 9, 2026

Empty handle arrays (inputs=[] or outputs=[]) were rendering default handles instead of hiding them. The renderHandles function used !handles?.length which is falsy for both null/undefined and [], preventing users from creating source/sink nodes without unwanted ports.

Changes

src/panel_reactflow/models/reactflow.jsx

  • Add explicit check for empty arrays before default-handle fallback
  • Array.isArray(handles) && handles.length === 0 → return null
  • null/undefined → render default handle (unchanged)
function renderHandles(direction, handles) {
  // Explicitly empty array → no handles
  if (Array.isArray(handles) && handles.length === 0) {
    return null;
  }
  // null/undefined → default handle
  if (!handles?.length) {
    return <Handle ... />;
  }
  // ... named handles
}

Example

from panel_reactflow import ReactFlow, NodeSpec, NodeType

# Source node with no input handles
node_types = {
    "source": NodeType(type="source", inputs=[], outputs=["out"]),
    "sink": NodeType(type="sink", inputs=["in"], outputs=[]),
}

Before/After

Before: Both nodes show handles on both sides despite empty arrays
Before

After: Source has no left handle, sink has no right handle
After

Original prompt

This section details on the original issue you should resolve

<issue_title>No way to hide port</issue_title>
<issue_description>## Empty handle list [] still renders a default handle

Description

Setting inputs=[] or outputs=[] on a NodeType should hide handles on that side, but a default unnamed handle is rendered instead. There is no way to declare that a node has no input or no output handles.

Root Cause

In src/panel_reactflow/models/reactflow.jsx, the renderHandles function (line 27-35) uses:

if (!handles?.length) {
  return <Handle ... />;  // default handle
}

!handles?.length is falsy for both null/undefined (no handles specified) and [] (explicitly empty). Both cases render a default handle. The function cannot distinguish "not specified" from "explicitly none".

Minimal Reproducible Example

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

pn.extension("jsoneditor")

# Define node types: "source" has no input handles, "sink" has no output handles
node_types = {
    "source": NodeType(type="source", label="Source", inputs=[], outputs=["out"]),
    "sink": NodeType(type="sink", label="Sink", inputs=["in"], outputs=[]),
}

flow = ReactFlow(
    nodes=[
        NodeSpec(id="a", type="source", position={"x": 0, "y": 0}, label="Source").to_dict(),
        NodeSpec(id="b", type="sink", position={"x": 400, "y": 0}, label="Sink").to_dict(),
    ],
    edges=[
        EdgeSpec(id="e1", source="a", target="b").to_dict(),
    ],
    node_types=node_types,
    sizing_mode="stretch_both",
    min_height=300,
)
flow.servable()
Image

Expected: "Source" node has no left (input) handle. "Sink" node has no right (output) handle.

Actual: Both nodes show handles on both sides.

Suggested Fix

Add an early return for explicitly empty arrays before the default-handle fallback:

function renderHandles(direction, handles) {
  if (Array.isArray(handles) && handles.length === 0) {
    return null;  // explicitly empty → no handles
  }
  if (!handles?.length) {
    return <Handle ... />;  // null/undefined → default handle
  }
  // named handles ...
}
```</issue_description>

## Comments on the Issue (you are @copilot in this section)

<comments>
</comments>

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Co-authored-by: MarcSkovMadsen <42288570+MarcSkovMadsen@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix issue with hiding input and output handles Fix: Distinguish empty handle arrays from undefined to allow hiding ports Feb 9, 2026
Copilot AI requested a review from MarcSkovMadsen February 9, 2026 06:08
Copy link
Contributor

@MarcSkovMadsen MarcSkovMadsen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me and local Claude.

Test Example
"""Test app for verifying the hide-port-handles fix.

This app demonstrates three handle behaviors:
1. Default handles (null/undefined inputs/outputs) -> shows default handle dots
2. Empty handles (inputs=[] / outputs=[]) -> hides handles completely
3. Named handles (inputs=["a","b"] / outputs=["x"]) -> shows named port handles
"""

import panel as pn

from panel_reactflow import EdgeSpec, NodeSpec, NodeType, ReactFlow

pn.extension("jsoneditor")

# Define three node types to test all handle cases
node_types = {
    # Case 1: No inputs/outputs specified (None) -> default handles
    "default_handles": NodeType(
        type="default_handles",
        label="Default Handles",
        inputs=None,
        outputs=None,
    ),
    # Case 2: Empty lists -> NO handles at all
    "no_handles": NodeType(
        type="no_handles",
        label="No Handles",
        inputs=[],
        outputs=[],
    ),
    # Case 3: Named ports -> shows named handles
    "named_handles": NodeType(
        type="named_handles",
        label="Named Handles",
        inputs=["in_a", "in_b"],
        outputs=["out_x"],
    ),
}

nodes = [
    NodeSpec(
        id="n1",
        type="default_handles",
        position={"x": 50, "y": 50},
        label="Default (should have dots)",
    ).to_dict(),
    NodeSpec(
        id="n2",
        type="no_handles",
        position={"x": 50, "y": 200},
        label="Empty [] (NO dots)",
    ).to_dict(),
    NodeSpec(
        id="n3",
        type="named_handles",
        position={"x": 50, "y": 350},
        label="Named ports (2 in, 1 out)",
    ).to_dict(),
    # A plain "panel" node (built-in) for comparison - should have default handles
    NodeSpec(
        id="n4",
        type="panel",
        position={"x": 350, "y": 125},
        label="Built-in panel type",
    ).to_dict(),
]

edges = [
    EdgeSpec(id="e1", source="n1", target="n4").to_dict(),
    EdgeSpec(id="e3", source="n3", target="n4", label="named->panel").to_dict(),
]

flow = ReactFlow(
    nodes=nodes,
    edges=edges,
    node_types=node_types,
    min_height=600,
    sizing_mode="stretch_both",
)

description = pn.pane.Markdown(
    """## Handle Visibility Test

- **Node 1 (Default Handles)**: `inputs=None, outputs=None` — should show default handle dots on left and right
- **Node 2 (No Handles)**: `inputs=[], outputs=[]` — should have **NO** handle dots at all
- **Node 3 (Named Handles)**: `inputs=["in_a","in_b"], outputs=["out_x"]` — should show 2 left handles + 1 right handle
- **Node 4 (Built-in panel)**: Default built-in type — should show default handle dots
""",
    sizing_mode="stretch_width",
)

layout = pn.Column(description, flow, sizing_mode="stretch_both", min_height=700)
layout.servable()
Image

@MarcSkovMadsen MarcSkovMadsen marked this pull request as ready for review February 9, 2026 08:30
@philippjfr philippjfr merged commit 25c49c9 into main Feb 11, 2026
16 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No way to hide port

3 participants