@walter I did my best to create a reproducible example outside of our project, I didn’t manage to do that.
However, the code below looks very similar to our code.
And what I’ve managed to nail down — If I remove line this.temporaryFromPort = createTemporaryPort(); — issue disappears. So, most probably the problem is in overriding temporaryFromPort, probably we are doing it incorrectly. What do you think?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>GoJS Flow Diagram – Multiple Ports</title>
<!-- GoJS from CDN -->
<script src="https://unpkg.com/gojs/release/go.js"></script>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
background: #f5f7fa;
font-family: Arial, sans-serif;
color: #333;
}
.page {
min-height: 100%;
padding: 32px;
box-sizing: border-box;
}
.container {
max-width: 1100px;
margin: 0 auto;
}
h1 {
margin-bottom: 24px;
font-size: 32px;
}
.card {
background: #ffffff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
margin-bottom: 24px;
}
.card h2 {
margin-top: 0;
margin-bottom: 8px;
font-size: 18px;
}
.card ul {
margin: 0;
padding-left: 18px;
font-size: 14px;
}
.card li {
margin-bottom: 6px;
}
#diagramDiv {
width: 100%;
height: 600px;
background: #ffffff;
border: 2px solid #ddd;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
</style>
</head>
<body>
<div class="page">
<div class="container">
<h1>GoJS Flow Diagram with Multiple Ports</h1>
<div class="card">
<h2>Features</h2>
<ul>
<li>8 connection ports per node (edges + corners)</li>
<li>Edge ports span full side minus padding</li>
<li>Grid snapping while dragging nodes</li>
<li>Relinkable connections with custom temporary ports</li>
<li>Each port has a distinct color</li>
<li>Custom relinking tool with live port visualization</li>
</ul>
</div>
<div id="diagramDiv"></div>
</div>
</div>
<script>
const $ = go.GraphObject.make;
const PORT_SIZE = 10;
const NODE_WIDTH = 140;
const NODE_HEIGHT = 100;
function createStepPort(
alignment,
id,
connectionSpot,
fromLinkable = true,
toLinkable = true,
visualShape
) {
return $(
go.Panel,
"Spot",
{
alignmentFocus: alignment instanceof go.Spot ? alignment : go.Spot.Center,
portId: id,
fromLinkable,
toLinkable,
toLinkableDuplicates: toLinkable,
cursor: "pointer",
...(alignment instanceof go.Spot
? {
alignment,
toSpot: connectionSpot || alignment,
fromSpot: connectionSpot || alignment
}
: {
toSpot: connectionSpot || go.Spot.Center,
fromSpot: connectionSpot || go.Spot.Center
})
},
...(alignment instanceof go.Binding ? [alignment] : []),
visualShape ||
$(go.Shape, {
width: PORT_SIZE,
height: PORT_SIZE,
stroke: null,
fill: "transparent"
})
);
}
function createGoTemporaryLinkTemplate() {
return $(
go.Link,
{
routing: go.Routing.Normal,
curve: go.Curve.JumpOver,
corner: 5,
toShortLength: 4
},
$(go.Shape, {
isPanelMain: true,
stroke: "#FF8000",
strokeWidth: 2,
strokeDashArray: [4, 4]
}),
$(go.Shape, {
toArrow: "Standard",
stroke: null,
fill: "#FF8000",
scale: 1.5
}),
$(go.Shape, "Circle", {
width: 9,
height: 9,
fill: "#FF8000",
segmentIndex: 0,
segmentFraction: 0
}),
$(
go.TextBlock,
{
alignment: go.Spot.Center,
segmentFraction: 0.5,
segmentOrientation: go.Orientation.Upright,
font: "bold 13px sans-serif",
stroke: "black"
},
new go.Binding("text", "label")
)
);
}
function createLinkHandle(options) {
return $(go.Shape, {
desiredSize: new go.Size(8, 8),
fill: "lightblue",
stroke: "dodgerblue",
cursor: options.cursor,
segmentIndex: options.segmentIndex
});
}
function createTemporaryPort() {
const port = $(
go.Panel,
"Spot",
$(go.Shape, "RoundedRectangle", { name: "TO_PORT_SHAPE" })
);
resetTemporaryPort(port);
return port;
}
function resetTemporaryPort(port) {
const shape = port.findObject("TO_PORT_SHAPE");
if (!shape) return;
shape.margin = 1;
shape.figure = "RoundedRectangle";
shape.stroke = "white";
shape.strokeWidth = 2;
shape.fill = "rgba(255,172,76,0.7)";
}
function customizeTemporaryPort(port, targetPort) {
const shape = port.findObject("TO_PORT_SHAPE");
if (!shape || !targetPort) return;
shape.strokeWidth = 1;
shape.margin = 0;
shape.figure = targetPort.width === targetPort.height ? "Circle" : "RoundedRectangle";
const b = targetPort.getDocumentBounds();
port.width = b.width;
port.height = b.height;
}
class FlowDesignerGoJSRelinkingTool extends go.RelinkingTool {
constructor() {
super();
this.portGravity = 50;
this.fromHandleArchetype = createLinkHandle({ cursor: "move", segmentIndex: 0 });
this.toHandleArchetype = createLinkHandle({ cursor: "move", segmentIndex: -1 });
this.temporaryFromPort = createTemporaryPort();
this.temporaryToPort = createTemporaryPort();
this.temporaryLink = createGoTemporaryLinkTemplate();
}
isReconnectingStart() {
return this.handle && this.handle.segmentIndex === 0;
}
doMouseMove() {
super.doMouseMove();
if (!this.isActive) return;
const staticPort = this.isReconnectingStart() ? this.temporaryToPort : this.temporaryFromPort;
const originalPort = this.isReconnectingStart() ? this.originalToPort : this.originalFromPort;
customizeTemporaryPort(staticPort, originalPort);
const target = this.findTargetPort(true);
if (target) {
customizeTemporaryPort(
this.isReconnectingStart() ? this.temporaryFromPort : this.temporaryToPort,
target
);
} else {
resetTemporaryPort(
this.isReconnectingStart() ? this.temporaryFromPort : this.temporaryToPort
);
}
}
}
const diagram = $(go.Diagram, "diagramDiv", {
"undoManager.isEnabled": true,
initialContentAlignment: go.Spot.Center,
"grid.visible": true,
"grid.gridCellSize": new go.Size(20, 20),
"draggingTool.isGridSnapEnabled": true
});
diagram.toolManager.relinkingTool = new FlowDesignerGoJSRelinkingTool();
diagram.nodeTemplate = $(
go.Node,
"Spot",
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
$(go.Shape, "Rectangle", {
width: NODE_WIDTH,
height: NODE_HEIGHT,
fill: "lightblue",
stroke: "#4a90e2",
strokeWidth: 2
}, new go.Binding("fill", "color")),
$(go.TextBlock, { font: "bold 14px Arial" }, new go.Binding("text", "text")),
createStepPort(go.Spot.Left, "left", go.Spot.Left, true, true,
$(go.Shape, "Rectangle", { width: PORT_SIZE, height: NODE_HEIGHT - 20, fill: "#ff6b6b" })
),
createStepPort(go.Spot.Top, "top", go.Spot.Top, true, true,
$(go.Shape, "Rectangle", { width: NODE_WIDTH - 20, height: PORT_SIZE, fill: "#4ecdc4" })
),
createStepPort(go.Spot.Right, "right", go.Spot.Right, true, true,
$(go.Shape, "Rectangle", { width: PORT_SIZE, height: NODE_HEIGHT - 20, fill: "#95e1d3" })
),
createStepPort(go.Spot.Bottom, "bottom", go.Spot.Bottom, true, true,
$(go.Shape, "Rectangle", { width: NODE_WIDTH - 20, height: PORT_SIZE, fill: "#f38181" })
),
createStepPort(go.Spot.TopLeft, "topleft", go.Spot.TopLeft, true, true,
$(go.Shape, "Circle", { width: PORT_SIZE, height: PORT_SIZE, fill: "#aa96da" })
),
createStepPort(go.Spot.TopRight, "topright", go.Spot.TopRight, true, true,
$(go.Shape, "Circle", { width: PORT_SIZE, height: PORT_SIZE, fill: "#fcbad3" })
),
createStepPort(go.Spot.BottomLeft, "bottomleft", go.Spot.BottomLeft, true, true,
$(go.Shape, "Circle", { width: PORT_SIZE, height: PORT_SIZE, fill: "#ffffd2" })
),
createStepPort(go.Spot.BottomRight, "bottomright", go.Spot.BottomRight, true, true,
$(go.Shape, "Circle", { width: PORT_SIZE, height: PORT_SIZE, fill: "#a8d8ea" })
)
);
diagram.linkTemplate = $(
go.Link,
{
relinkableFrom: true,
relinkableTo: true,
reshapable: true,
routing: go.Routing.AvoidsNodes,
curve: go.Curve.JumpOver,
corner: 20
},
$(go.Shape, { stroke: "#4a90e2", strokeWidth: 3 }),
$(go.Shape, { toArrow: "Standard", fill: "#4a90e2", scale: 1.5 }),
$(go.TextBlock, {
margin: 4,
background: "white",
font: "10pt Arial"
}, new go.Binding("text", "text"))
);
const model = new go.GraphLinksModel(
[
{ key: 1, text: "Step 1", color: "lightblue", loc: "100 150" },
{ key: 2, text: "Step 2", color: "lightgreen", loc: "350 150" },
{ key: 3, text: "Step 3", color: "lightyellow", loc: "225 300" }
],
[
{ from: 1, to: 2, fromPort: "right", toPort: "left" },
{ from: 1, to: 3, fromPort: "bottom", toPort: "top" },
{ from: 2, to: 3, fromPort: "bottomleft", toPort: "topright" }
]
);
model.linkFromPortIdProperty = "fromPort";
model.linkToPortIdProperty = "toPort";
diagram.model = model;
</script>
</body>
</html>
The only way currently to solve the problem I’ve found:
diagram.addDiagramEventListener("LinkRelinked", () => {
diagram.rebuildParts();
});