Expandable and Collapsible Ports

[EDIT: updated to fix the vertical alignment of expanded ports]

<!DOCTYPE html>
<html>
<head>
  <title>Tethered, Expandable, Labeled Ports</title>
  <!-- Copyright 1998-2022 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:700px"></div>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
const $ = go.GraphObject.make;

const myDiagram =
  $(go.Diagram, "myDiagramDiv",
    {
      layout: $(go.LayeredDigraphLayout),
      "undoManager.isEnabled": true
    });

const EW = 30;
const PW = 13;
const PH = 13;
const PLW = 13;
const PLH = 32;

function portStyle() {
  return { width: PW-1, height: PH-1, fill: "transparent" };
}

PortTemplateSimple =
  $(go.Panel, "Spot",
    $(go.Shape, portStyle(),
      new go.Binding("portId", "id")),
    new go.Binding("position", "itemIndex", simplePosition).ofObject()
  );
function simplePosition(i, panel) {
  if (panel.panel.itemArray.length <= 3) return new go.Point(0, i*PH);
  return new go.Point(0, 0);
}

PortTemplateExpanded =
  $(go.Panel,
    $(go.Shape, portStyle(),
      new go.Binding("portId", "id"),
      new go.Binding("position", "itemIndex", i => new go.Point(0, i*PH)).ofObject()),
    $(go.Shape, { strokeDashArray: [2, 2] },
      new go.Binding("geometry", "itemIndex", indexToGeo).ofObject()),
  );
function indexToGeo(i, shape) {
  const tot = shape.panel.panel.itemArray.length;
  return new go.Geometry().add(new go.PathFigure(0, 0)
          .add(new go.PathSegment(go.PathSegment.Move, EW+PW, tot*PH))
          .add(new go.PathSegment(go.PathSegment.Move, EW+PW, tot*PH/2))
          .add(new go.PathSegment(go.PathSegment.Line, PW, i*PH + PH/2)));
}

PortTemplateExpandedLabeled =
  $(go.Panel,
    $(go.Shape, portStyle(),
      new go.Binding("portId", "id"),
      new go.Binding("position", "itemIndex", i => new go.Point(0, i*PLH)).ofObject()),
    $(go.TextBlock, { alignment: new go.Spot(0.5, 0.5, 0, 1) },
      new go.Binding("text", "id"),
      new go.Binding("position", "itemIndex", i => new go.Point(0, i*PLH + PH + 1)).ofObject()),
    $(go.Shape, { strokeDashArray: [2, 2] },
      new go.Binding("geometry", "itemIndex", indexToGeoL).ofObject()),
  );
function indexToGeoL(i, shape) {
  const tot = shape.panel.panel.itemArray.length;
  return new go.Geometry().add(new go.PathFigure(0, 0)
          .add(new go.PathSegment(go.PathSegment.Move, EW+PLW, tot*PLH))
          .add(new go.PathSegment(go.PathSegment.Move, EW+PLW, tot*PLH/2))
          .add(new go.PathSegment(go.PathSegment.Line, PLW, i*PLH + PH/2)));
}

myDiagram.nodeTemplate =
  $(go.Node, "Spot",
    { locationSpot: go.Spot.Center, locationObjectName: "BODY",
      layoutConditions: go.Part.LayoutStandard & ~go.Part.LayoutNodeSized },
    $(go.Panel, "Table",
      { name: "BODY", alignmentFocusName: "SHAPE" },
      $(go.Shape, { name: "SHAPE", row: 0, width: 50, height: 50, fill: "lightblue", strokeWidth: 0, portId: "" }),
      $(go.TextBlock, { row: 1, stretch: go.GraphObject.Horizontal, maxLines: 1, overflow: go.TextBlock.OverflowEllipsis },
        new go.Binding("text"))
    ),
    $(go.Panel, "Spot",
      { alignment: go.Spot.Left, alignmentFocus: go.Spot.Right },
      $(go.Panel,
        new go.Binding("itemArray", "ports"),
        { itemTemplate: PortTemplateSimple },
        new go.Binding("itemTemplate", "", data => data.expanded
                                                   ? (data.labeled ? PortTemplateExpandedLabeled : PortTemplateExpanded)
                                                   : PortTemplateSimple)
      ),
      $(go.TextBlock, { visible: false, alignment: new go.Spot(0.5, 0.5, 0, 1) },
        new go.Binding("text", "ports", a => a.length.toString()),
        new go.Binding("visible", "", data => !data.expanded && data.ports.length > 3))
    )
  );

  myDiagram.model = $(go.GraphLinksModel,
    {
      linkFromPortIdProperty: "fpid",
      linkToPortIdProperty: "tpid",
      nodeDataArray: [
        {
          key: 1, text: "Just 2",
          ports: [ { id: "a" }, { id: "b" } ]
        },
        {
          key: 2, text: "No Labs", expanded: true,
          ports: [ { id: "a" }, { id: "b" }, { id: "c" } ]
        },
        {
          key: 3, text: "Labels", expanded: true, labeled: true,
          ports: [ { id: "a" }, { id: "b" }, { id: "c" } ]
        },
        {
          key: 4, text: "Collapsed", expanded: false,
          ports: [ { id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }, { id: "e" }, { id: "f" }, { id: "g" } ]
        },
        {
          key: 5, text: "Expanded", expanded: true,
          ports: [ { id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }, { id: "e" }, { id: "f" }, { id: "g" } ]
        }
      ],
      linkDataArray: [
        { from: 1, to: 2, tpid: "a" },
        { from: 1, to: 3, tpid: "c" },
        { from: 1, to: 4, tpid: "c" },
        { from: 1, to: 5, tpid: "e" }
      ]
    });
  </script>
</body>
</html>

produces: