Temporary Ports with No Real Port

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>