canConnect method
Validates whether a connection can be made from the current drag state to the specified target port.
This method checks:
- Not connecting a port to itself (same node + same port is invalid, but same node with different ports is OK for self-loops)
- Direction compatibility (output→input or input←output)
- Port is connectable
- No duplicate connections
- Max connections limit not exceeded
- Custom validation via ConnectionEvents.onBeforeComplete callback
(only when
skipCustomValidationis false)
The skipCustomValidation parameter allows skipping the expensive custom
validation callback during drag updates. This is useful for hover feedback
where only basic structural validation is needed. The full validation
including custom callbacks runs at connection completion time.
Returns a ConnectionValidationResult indicating if the connection is valid.
Implementation
ConnectionValidationResult canConnect({
required String targetNodeId,
required String targetPortId,
bool skipCustomValidation = false,
}) {
final temp = interaction.temporaryConnection.value;
if (temp == null) {
return const ConnectionValidationResult.deny(
reason: 'No active connection drag',
);
}
// 1. Cannot connect a port to itself
// Same node is OK (self-loops), but same port on same node is NOT
if (temp.startNodeId == targetNodeId && temp.startPortId == targetPortId) {
return const ConnectionValidationResult.deny(
reason: 'Cannot connect a port to itself',
);
}
// Get target node first (needed for direction check)
final targetNode = _nodes[targetNodeId];
if (targetNode == null) {
return const ConnectionValidationResult.deny(
reason: 'Target node not found',
);
}
// 2. Cannot connect same direction ports (output→output or input→input)
// Determine if target port is in outputPorts or inputPorts list
final targetIsOutput = targetNode.outputPorts.any(
(p) => p.id == targetPortId,
);
if (temp.isStartFromOutput == targetIsOutput) {
return ConnectionValidationResult.deny(
reason: targetIsOutput
? 'Cannot connect output to output'
: 'Cannot connect input to input',
);
}
// Get source node and port
final sourceNode = _nodes[temp.startNodeId];
if (sourceNode == null) {
return const ConnectionValidationResult.deny(
reason: 'Source node not found',
);
}
final sourcePort = sourceNode.allPorts
.where((p) => p.id == temp.startPortId)
.firstOrNull;
if (sourcePort == null) {
return const ConnectionValidationResult.deny(
reason: 'Source port not found',
);
}
// Get target port (targetNode already looked up above)
final targetPort = targetNode.allPorts
.where((p) => p.id == targetPortId)
.firstOrNull;
if (targetPort == null) {
return const ConnectionValidationResult.deny(
reason: 'Target port not found',
);
}
// 2. Both ports must be connectable
if (!sourcePort.isConnectable) {
return const ConnectionValidationResult.deny(
reason: 'Source port is not connectable',
);
}
if (!targetPort.isConnectable) {
return const ConnectionValidationResult.deny(
reason: 'Target port is not connectable',
);
}
// 3. Direction compatibility (port type check)
// If started from output, target must be able to accept input
// If started from input, target must be able to emit output
if (temp.isStartFromOutput) {
if (!targetPort.isInput) {
return const ConnectionValidationResult.deny(
reason: 'Target port cannot receive connections',
);
}
} else {
if (!targetPort.isOutput) {
return const ConnectionValidationResult.deny(
reason: 'Target port cannot emit connections',
);
}
}
// Determine actual source/target for duplicate and max connection checks
final Node<T> actualSourceNode;
final Port actualSourcePort;
final Node<T> actualTargetNode;
final Port actualTargetPort;
if (temp.isStartFromOutput) {
actualSourceNode = sourceNode;
actualSourcePort = sourcePort;
actualTargetNode = targetNode;
actualTargetPort = targetPort;
} else {
actualSourceNode = targetNode;
actualSourcePort = targetPort;
actualTargetNode = sourceNode;
actualTargetPort = sourcePort;
}
// Get existing connections for both ports
final existingSourceConnections = _connections
.where(
(conn) =>
conn.sourceNodeId == actualSourceNode.id &&
conn.sourcePortId == actualSourcePort.id,
)
.map((c) => c.id)
.toList();
final existingTargetConnections = _connections
.where(
(conn) =>
conn.targetNodeId == actualTargetNode.id &&
conn.targetPortId == actualTargetPort.id,
)
.map((c) => c.id)
.toList();
// 4. No duplicate connections
final duplicateExists = _connections.any(
(conn) =>
conn.sourceNodeId == actualSourceNode.id &&
conn.sourcePortId == actualSourcePort.id &&
conn.targetNodeId == actualTargetNode.id &&
conn.targetPortId == actualTargetPort.id,
);
if (duplicateExists) {
return const ConnectionValidationResult.deny(
reason: 'Connection already exists',
);
}
// 5. Max connections limit (only check target port since it receives the connection)
if (actualTargetPort.maxConnections != null) {
if (existingTargetConnections.length >=
actualTargetPort.maxConnections!) {
return const ConnectionValidationResult.deny(
reason: 'Target port has maximum connections',
);
}
}
// 6. Call custom validation callback if provided (skip during drag for performance)
if (!skipCustomValidation) {
final onBeforeComplete = events.connection?.onBeforeComplete;
if (onBeforeComplete != null) {
final context = ConnectionCompleteContext<T>(
sourceNode: actualSourceNode,
sourcePort: actualSourcePort,
targetNode: actualTargetNode,
targetPort: actualTargetPort,
existingSourceConnections: existingSourceConnections,
existingTargetConnections: existingTargetConnections,
);
final result = onBeforeComplete(context);
if (!result.allowed) {
return result;
}
}
}
return const ConnectionValidationResult.allow();
}