Ports

Understanding ports - connection points on nodes

Ports

Ports are connection points on nodes where edges can be attached. They define how nodes can connect to each other in your graph.

Port Structure

class Port {
  final String id;              // Unique identifier
  final String name;            // Display name
  final PortPosition position;  // left, right, top, bottom
  final PortType type;          // source, target, both
  final Offset offset;          // Offset from default position
  final bool multiConnections;  // Allow multiple connections
}

Port Positions

Ports can be positioned on any side of a node:

enum PortPosition {
  left,   // Left edge of node
  right,  // Right edge of node
  top,    // Top edge of node
  bottom, // Bottom edge of node
}

Positioning Examples

// Port on the left side
Port(
  id: 'input-port',
  name: 'Input',
  position: PortPosition.left,
  type: PortType.target,
)

// Port on the right side
Port(
  id: 'output-port',
  name: 'Output',
  position: PortPosition.right,
  type: PortType.source,
)

// Port on top
Port(
  id: 'trigger-port',
  name: 'Trigger',
  position: PortPosition.top,
  type: PortType.target,
)

// Port on bottom
Port(
  id: 'result-port',
  name: 'Result',
  position: PortPosition.bottom,
  type: PortType.source,
)

Port Types

Ports have three types that control connection direction:

enum PortType {
  source,  // Can only output connections
  target,  // Can only receive connections
  both,    // Can both send and receive
}

Source Ports

Output ports that create connections to other nodes:

Port(
  id: 'out-1',
  name: 'Output',
  position: PortPosition.right,
  type: PortType.source,
)

Target Ports

Input ports that receive connections from other nodes:

Port(
  id: 'in-1',
  name: 'Input',
  position: PortPosition.left,
  type: PortType.target,
)

Bidirectional Ports

Ports that can both send and receive:

Port(
  id: 'data-1',
  name: 'Data',
  position: PortPosition.left,
  type: PortType.both,
)

Port Offsets

Fine-tune port positioning with offsets:

// Default position (centered)
Port(
  id: 'port-1',
  name: 'Port 1',
  position: PortPosition.right,
  type: PortType.source,
  offset: Offset.zero, // Default
)

// Offset from center
Port(
  id: 'port-2',
  name: 'Port 2',
  position: PortPosition.right,
  type: PortType.source,
  offset: Offset(0, 20), // 20 pixels down from center
)

Port(
  id: 'port-3',
  name: 'Port 3',
  position: PortPosition.right,
  type: PortType.source,
  offset: Offset(0, -20), // 20 pixels up from center
)

Multiple Ports with Offsets

Create evenly spaced ports:

List<Port> createMultipleOutputPorts(int count, String nodeId) {
  final ports = <Port>[];
  final spacing = 60.0;
  final startOffset = -(spacing * (count - 1)) / 2;

  for (int i = 0; i < count; i++) {
    ports.add(
      Port(
        id: '$nodeId-out-$i',
        name: 'Output $i',
        position: PortPosition.right,
        type: PortType.source,
        offset: Offset(0, startOffset + (i * spacing)),
      ),
    );
  }

  return ports;
}

// Usage
final node = Node(
  id: 'multi-output',
  // ...
  outputPorts: createMultipleOutputPorts(4, 'multi-output'),
);

Multi-Connections

Control whether a port can have multiple connections:

// Single connection only (default for source ports)
Port(
  id: 'single-out',
  name: 'Output',
  position: PortPosition.right,
  type: PortType.source,
  multiConnections: false,
)

// Allow multiple connections (common for target ports)
Port(
  id: 'multi-in',
  name: 'Input',
  position: PortPosition.left,
  type: PortType.target,
  multiConnections: true,
)

Common Port Patterns

Simple Flow Node

// Input on left, output on right
final flowNode = Node<MyData>(
  id: 'flow-node',
  type: 'process',
  position: Offset(200, 100),
  size: Size(150, 80),
  data: MyData(label: 'Process'),
  inputPorts: [
    Port(
      id: 'flow-in',
      name: 'Input',
      position: PortPosition.left,
      type: PortType.target,
    ),
  ],
  outputPorts: [
    Port(
      id: 'flow-out',
      name: 'Output',
      position: PortPosition.right,
      type: PortType.source,
    ),
  ],
);

Conditional Node

// One input, two outputs
final conditionNode = Node<MyData>(
  id: 'condition',
  type: 'condition',
  position: Offset(200, 100),
  size: Size(180, 100),
  data: MyData(label: 'If/Else'),
  inputPorts: [
    Port(
      id: 'cond-in',
      name: 'Input',
      position: PortPosition.left,
      type: PortType.target,
    ),
  ],
  outputPorts: [
    Port(
      id: 'cond-true',
      name: 'True',
      position: PortPosition.right,
      type: PortType.source,
      offset: Offset(0, -25),
    ),
    Port(
      id: 'cond-false',
      name: 'False',
      position: PortPosition.right,
      type: PortType.source,
      offset: Offset(0, 25),
    ),
  ],
);

Merge Node

// Multiple inputs, one output
final mergeNode = Node<MyData>(
  id: 'merge',
  type: 'merge',
  position: Offset(200, 100),
  size: Size(150, 120),
  data: MyData(label: 'Merge'),
  inputPorts: [
    Port(
      id: 'merge-in-1',
      name: 'Input 1',
      position: PortPosition.left,
      type: PortType.target,
      multiConnections: true,
      offset: Offset(0, -30),
    ),
    Port(
      id: 'merge-in-2',
      name: 'Input 2',
      position: PortPosition.left,
      type: PortType.target,
      multiConnections: true,
      offset: Offset(0, 0),
    ),
    Port(
      id: 'merge-in-3',
      name: 'Input 3',
      position: PortPosition.left,
      type: PortType.target,
      multiConnections: true,
      offset: Offset(0, 30),
    ),
  ],
  outputPorts: [
    Port(
      id: 'merge-out',
      name: 'Output',
      position: PortPosition.right,
      type: PortType.source,
    ),
  ],
);

Split Node

// One input, multiple outputs
final splitNode = Node<MyData>(
  id: 'split',
  type: 'split',
  position: Offset(200, 100),
  size: Size(150, 120),
  data: MyData(label: 'Split'),
  inputPorts: [
    Port(
      id: 'split-in',
      name: 'Input',
      position: PortPosition.left,
      type: PortType.target,
    ),
  ],
  outputPorts: [
    Port(
      id: 'split-out-1',
      name: 'Output 1',
      position: PortPosition.right,
      type: PortType.source,
      offset: Offset(0, -30),
    ),
    Port(
      id: 'split-out-2',
      name: 'Output 2',
      position: PortPosition.right,
      type: PortType.source,
      offset: Offset(0, 0),
    ),
    Port(
      id: 'split-out-3',
      name: 'Output 3',
      position: PortPosition.right,
      type: PortType.source,
      offset: Offset(0, 30),
    ),
  ],
);

Port Theming

Customize port appearance with PortTheme:

theme: NodeFlowTheme(
  portTheme: PortTheme(
    size: 12,                    // Port circle size
    color: Colors.blue,          // Default color
    hoverColor: Colors.blue[700]!, // Hover color
    borderColor: Colors.white,   // Border color
    borderWidth: 2,              // Border width
  ),
)

Custom Port Colors by Type

Widget buildPortWithTypeColor(Port port) {
  Color portColor;
  switch (port.type) {
    case PortType.source:
      portColor = Colors.green;
      break;
    case PortType.target:
      portColor = Colors.blue;
      break;
    case PortType.both:
      portColor = Colors.purple;
      break;
  }

  // Port is automatically rendered by the editor
  // This is just for visualization
  return Container(
    width: 12,
    height: 12,
    decoration: BoxDecoration(
      color: portColor,
      shape: BoxShape.circle,
      border: Border.all(color: Colors.white, width: 2),
    ),
  );
}

Querying Ports

Get Port from Node

final node = controller.graph.getNode('node-1');
if (node != null) {
  // Get specific port
  final port = node.inputPorts.firstWhere(
    (p) => p.id == 'input-1',
    orElse: () => throw Exception('Port not found'),
  );

  // Get all input ports
  final allInputs = node.inputPorts;

  // Get all output ports
  final allOutputs = node.outputPorts;

  // Get all ports
  final allPorts = [...node.inputPorts, ...node.outputPorts];
}

Find Connections to/from Port

// Find connections from a source port
List<Connection> getConnectionsFromPort(String nodeId, String portId) {
  return controller.graph.connections.values
      .where((c) => c.sourceNodeId == nodeId && c.sourcePortId == portId)
      .toList();
}

// Find connections to a target port
List<Connection> getConnectionsToPort(String nodeId, String portId) {
  return controller.graph.connections.values
      .where((c) => c.targetNodeId == nodeId && c.targetPortId == portId)
      .toList();
}

// Check if port has connections
bool hasConnections(String nodeId, String portId) {
  return controller.graph.connections.values.any(
    (c) =>
        (c.sourceNodeId == nodeId && c.sourcePortId == portId) ||
        (c.targetNodeId == nodeId && c.targetPortId == portId),
  );
}

Dynamic Ports

Add or remove ports at runtime:

// Note: Ports are part of the Node constructor
// To change ports, you need to recreate the node or modify it

void addPortToNode(String nodeId, Port newPort, bool isInput) {
  final node = controller.graph.getNode(nodeId);
  if (node == null) return;

  // Create updated node
  final updatedNode = Node<MyData>(
    id: node.id,
    type: node.type,
    position: node.position.value,
    size: node.size,
    data: node.data,
    inputPorts: isInput
        ? [...node.inputPorts, newPort]
        : node.inputPorts,
    outputPorts: !isInput
        ? [...node.outputPorts, newPort]
        : node.outputPorts,
  );

  // Replace node
  controller.removeNode(nodeId);
  controller.addNode(updatedNode);
}

Port Labels

Ports can have labels displayed near them:

// Port names are used as labels automatically
Port(
  id: 'data-in',
  name: 'Data Input',  // This becomes the label
  position: PortPosition.left,
  type: PortType.target,
)

Customize label appearance in theme:

theme: NodeFlowTheme(
  labelTheme: LabelTheme(
    fontSize: 10,
    color: Colors.black87,
    backgroundColor: Colors.white,
    padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
    borderRadius: 3,
  ),
)

Best Practices

  1. Unique IDs: Ensure port IDs are unique across all nodes
  2. Meaningful Names: Use descriptive port names
  3. Consistent Positioning: Keep similar ports in similar positions
  4. Logical Flow: Input ports on left/top, output ports on right/bottom
  5. Multi-Connections: Enable for merge points, disable for one-to-one
  6. Offset Spacing: Use consistent spacing between multiple ports
  7. Type Safety: Use appropriate port types to guide connections

Common Patterns

Port ID Generation

String generatePortId(String nodeId, String portName) {
  return '$nodeId-${portName.toLowerCase().replaceAll(' ', '-')}';
}

// Usage
final port = Port(
  id: generatePortId('node-1', 'Data Input'),  // 'node-1-data-input'
  name: 'Data Input',
  position: PortPosition.left,
  type: PortType.target,
);

Port Factory

class PortFactory {
  static Port createInputPort(String nodeId, String name, {Offset offset = Offset.zero}) {
    return Port(
      id: '$nodeId-in-${name.toLowerCase().replaceAll(' ', '-')}',
      name: name,
      position: PortPosition.left,
      type: PortType.target,
      offset: offset,
      multiConnections: true,
    );
  }

  static Port createOutputPort(String nodeId, String name, {Offset offset = Offset.zero}) {
    return Port(
      id: '$nodeId-out-${name.toLowerCase().replaceAll(' ', '-')}',
      name: name,
      position: PortPosition.right,
      type: PortType.source,
      offset: offset,
      multiConnections: false,
    );
  }
}

Next Steps

On this page