Try this:
<!DOCTYPE html>
<html>
<head>
<title>Minimal GoJS Sample</title>
<!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
</head>
<body>
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
<button id="myTestButton">Test</button>
<textarea id="mySavedModel" style="width:100%;height:250px"></textarea>
<script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
<script id="code">
class CustomLinkingTool extends go.LinkingTool {
constructor(init) {
super();
this._hasTempPort = null;
if (init) Object.assign(this, init);
}
// note: tempport is on the LinkingBaseTool.temporaryToNode, not on a real Node
copyPortProperties(realnode, realport, tempnode, tempport, toend) {
this.diagram.model.commit(m => {
// if targeting the same node, don't need to remove any temporary port
if (this._hasTempPort !== realnode) {
this.cleanupAugmentedNode();
}
let tempid = "";
if (this._hasTempPort === null) {
const data = realnode.data;
if (data) {
tempid = "abcdefghijklmnopqrstuvwxyz"[data.ports.length];
this.diagram.model.commit(m => {
m.addArrayItem(data.ports, tempid);
}, null);
this._hasTempPort = realnode;
}
}
if (this._hasTempPort !== null) {
const data = this._hasTempPort.data;
tempid = "abcdefghijklmnopqrstuvwxyz"[data.ports.length-1];
const port = realnode.findPort(tempid);
if (port !== null) {
tempnode.location = port.getDocumentPoint(go.Spot.Center);
}
}
}, null);
super.copyPortProperties(realnode, realport, tempnode, tempport, toend);
}
setNoTargetPortProperties(tempnode, tempport, toend) {
const augmentedNode = this._hasTempPort;
if (augmentedNode !== null) {
this._hasTempPort = null;
const data = augmentedNode.data;
if (data && data.ports.length > 0) {
this.diagram.model.commit(m => {
m.removeArrayItem(data.ports, data.ports.length-1);
}, null);
}
}
super.setNoTargetPortProperties(tempnode, tempport, toend);
}
cleanupAugmentedNode() {
const augmentedNode = this._hasTempPort;
if (augmentedNode !== null) {
this._hasTempPort = null;
const data = augmentedNode.data;
if (data && data.ports.length > 0) {
this.diagram.model.commit(m => {
m.removeArrayItem(data.ports, data.ports.length-1);
}, null);
}
}
}
insertLink(fromnode, fromport, tonode, toport) {
this.cleanupAugmentedNode();
if (tonode && tonode.data) {
const data = tonode.data;
const tempid = "abcdefghijklmnopqrstuvwxyz"[data.ports.length];
if (toport.portId === tempid) {
this.diagram.model.addArrayItem(data.ports, tempid);
}
}
return super.insertLink(fromnode, fromport, tonode, toport);
}
doNoLink(fromnode, fromport, tonode, toport) {
this.cleanupAugmentedNode();
super.doNoLink(fromnode, romport, tonode, toport);
}
}
const myDiagram =
new go.Diagram("myDiagramDiv", {
layout: new go.LayeredDigraphLayout(),
linkingTool: new CustomLinkingTool(),
"undoManager.isEnabled": true,
"ModelChanged": e => { // just for demonstration purposes,
if (e.isTransactionFinished) { // show the model data in the page's TextArea
document.getElementById("mySavedModel").textContent = e.model.toJson();
}
}
});
myDiagram.nodeTemplate =
new go.Node("Table", {
layoutConditions: go.LayoutConditions.Standard & ~go.LayoutConditions.NodeSized
})
.add(
new go.Panel("Auto", { column: 1, stretch: go.Stretch.Vertical, minSize: new go.Size(50, 50) })
.add(
new go.Shape({ fill: "white" })
.bind("fill", "color"),
new go.TextBlock({ margin: 8 })
.bind("text")
),
new go.Panel("Vertical", {
column: 0,
itemTemplate:
new go.Panel({
margin: new go.Margin(1, 0),
toLinkable: true, toSpot: go.Spot.Left
})
.bind("portId", "")
.add(
new go.Shape({ width: 8, height: 8, fill: "gray" })
)
})
.bind("itemArray", "ports"),
new go.Shape({
column: 2, width: 8, height: 8, fill: "gray",
portId: "out", fromLinkable: true, fromSpot: go.Spot.Right, cursor: "pointer"
})
);
myDiagram.linkTemplate =
new go.Link({
fromPortId: "out", corner: 6,
relinkableFrom: true, relinkableTo: true
})
.add(new go.Shape());
myDiagram.model = new go.GraphLinksModel({
linkToPortIdProperty: "tid",
nodeDataArray:
[
{ key: 1, text: "Alpha", color: "lightblue", ports: ["a", "b", "c"] },
{ key: 2, text: "Beta", color: "orange", ports: ["a", "b", "c"] },
{ key: 3, text: "Gamma", color: "lightgreen", ports: ["a"] },
{ key: 4, text: "Delta", color: "pink", ports: ["a", "b", "c", "d"] }
],
linkDataArray:
[
{ from: 1, to: 2, tid: "a" },
{ from: 3, to: 4, tid: "c" }
]
});
document.getElementById("myTestButton").addEventListener("click", e => {
});
</script>
</body>
</html>