I modified the code I gave you folks before, adding context menu support for adding and removing ports. I haven’t encountered any problems with undo or redo after adding or removing ports. But note that the call to updateTargetBindings happens immediately with the added or removed port. Here’s the updated code:
```
<!DOCTYPE html>
<html>
<head>
<title>Tethered, Expandable, Labeled Ports</title>
<!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
</head>
<body>
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:700px"></div>
<script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
<script id="code">
const myDiagram =
new go.Diagram("myDiagramDiv", {
layout: new go.LayeredDigraphLayout(),
"undoManager.isEnabled": true
});
const EW = 30;
const PW = 13;
const PH = 13;
const PLW = 13;
const PLH = 32;
function portStyle(port) {
port.set({ width: PW - 1, height: PH - 1, fill: "transparent", toLinkable: true });
}
PortTemplateSimple =
new go.Panel("Spot")
.bindObject("position", "itemIndex", simplePosition)
.add(
new go.Shape()
.apply(portStyle)
.bind("portId", "id")
);
function simplePosition(i, panel) {
if (panel.panel.itemArray.length <= 3) return new go.Point(0, i * PH);
return new go.Point(0, 0);
}
PortTemplateExpanded =
new go.Panel()
.add(
new go.Shape()
.apply(portStyle)
.bind("portId", "id")
.bindObject("position", "itemIndex", i => new go.Point(0, i * PH)),
new go.Shape({ strokeDashArray: [2, 2] })
.bindObject("geometry", "itemIndex", indexToGeo)
);
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 =
new go.Panel()
.add(
new go.Shape()
.apply(portStyle)
.bind("portId", "id")
.bindObject("position", "itemIndex", i => new go.Point(0, i * PLH)),
new go.TextBlock({ alignment: new go.Spot(0.5, 0.5, 0, 1) })
.bind("text", "id")
.bindObject("position", "itemIndex", i => new go.Point(0, i * PLH + PH + 1)),
new go.Shape({ strokeDashArray: [2, 2] })
.bindObject("geometry", "itemIndex", indexToGeoL)
);
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 =
new go.Node("Spot", {
locationSpot: go.Spot.Center, locationObjectName: "BODY",
layoutConditions: go.Part.LayoutStandard & ~go.Part.LayoutNodeSized,
contextMenu:
go.GraphObject.build("ContextMenu")
.add(
go.GraphObject.build("ContextMenuButton", {
click: (e, but) => {
const node = but.part.adornedPart;
let arr = node.data.ports;
e.diagram.model.commit(m => {
if (!arr) {
arr = [];
m.set(node.data, "ports", arr);
}
m.addArrayItem(arr, { id: arr.length.toString() });
node.updateTargetBindings("ports");
});
}
})
.add(new go.TextBlock("Add Port")),
go.GraphObject.build("ContextMenuButton", {
click: (e, but) => {
const node = but.part.adornedPart;
const arr = node.data.ports;
if (arr.length > 0) {
e.diagram.model.commit(m => {
m.removeArrayItem(arr, arr.length-1);
node.updateTargetBindings("ports");
});
}
}
})
.add(new go.TextBlock("Remove Last Port"))
)
})
.add(
new go.Panel("Table", { name: "BODY", alignmentFocusName: "SHAPE" })
.add(
new go.Shape({ name: "SHAPE", row: 0, width: 50, height: 50, fill: "lightblue", strokeWidth: 0, portId: "", fromLinkable: true }),
new go.TextBlock({ row: 1, stretch: go.GraphObject.Horizontal, maxLines: 1, overflow: go.TextBlock.OverflowEllipsis })
.bind("text")
),
new go.Panel("Spot", { alignment: go.Spot.Left, alignmentFocus: go.Spot.Right })
.add(
new go.Panel({ itemTemplate: PortTemplateSimple })
.bind("itemArray", "ports")
.bind("itemTemplate", "", data => data.expanded
? (data.labeled ? PortTemplateExpandedLabeled : PortTemplateExpanded)
: PortTemplateSimple),
new go.TextBlock({ visible: false, alignment: new go.Spot(0.5, 0.5, 0, 1) })
.bind("text", "ports", a => a.length.toString())
.bind("visible", "", data => !data.expanded && data.ports.length > 3)
)
);
myDiagram.model = new 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>
```