Dynamic ports on groups using context menu

Hello,

I am trying to add a context menu to groups with options like “add port on left”, “add a port on right”, etc. which adds the port to the respective side of the group boundary.

Here is my group template,

myDiagram.groupTemplate =
    $(go.Group, "Auto",
      {
        selectionObjectName: "PANEL",  // selection handle goes around shape, not label
        ungroupable: true,  // enable Ctrl-Shift-G to ungroup a selected Group
        mouseDragEnter: (e, grp, prev) => highlightGroup(e, grp, true),
        mouseDragLeave: (e, grp, next) => highlightGroup(e, grp, false),
        computesBoundsAfterDrag: true,
        mouseDrop: finishDrop,
        handlesDragDropForMembers: true,
      },

      $(go.Shape, "RoundedRectangle",
        {
          fill: "rgba(128,128,128,0.2)",
          strokeWidth: 3,
          stroke: "gray",
          strokeWidth: 3,
          portId: "", cursor: "pointer",  // the Shape is the port, not the whole Node
          // allow all kinds of links from and to this port
          fromLinkable: true, fromLinkableSelfNode: false, fromLinkableDuplicates: false,
          toLinkable: true, toLinkableSelfNode: false, toLinkableDuplicates: false
        },
        // new go.Binding("stroke", "color"),
        // new go.Binding("fill", "color", go.Brush.lighten)
      ),
      $(go.TextBlock,
        {
          alignment: go.Spot.Top,
          font: "bold 19px sans-serif",
          margin: new go.Margin(4, 10, 0, 20),
          isMultiline: false,  // don't allow newlines in text
          editable: true  // allow in-place editing by user
        },
        new go.Binding("text", "text").makeTwoWay(),
        new go.Binding("stroke", "color")
      ),
      $(go.Placeholder, { alignment: go.Spot.TopLeft, padding: 50 }),
      $("SubGraphExpanderButton", { alignment: go.Spot.TopLeft }),
      {
        minSize: new go.Size(50, 50),
        subGraphExpandedChanged: function (grp) {
          if (!grp.isSubGraphExpanded) return;
          shiftNodes(grp);
        },
        selectionChanged: function (grp) {
          grp.diagram.commit(function (diag) {
            var lay = grp.isSelected ? "Foreground" : "";
            grp.layerName = lay;
            grp.findSubGraphParts().each(function (x) { x.layerName = lay; });
          }, null);
        },
        toolTip:
          $("ToolTip",
            $(go.TextBlock, { margin: 14 },
              // bind to tooltip, not to Group.data, to allow access to Group properties
              new go.Binding("text", "", groupInfo).ofObject())
          ),
        // the same context menu Adornment is shared by all groups
        contextMenu: partContextMenu

      }
    );

Any help/suggestions are appreciated. Thanks in advance.

The Dynamic Ports sample has such buttons. You’ll probably want to structure your group in a similar manner.

Hello,
Thanks for the reply. I tried following this sample but for some reason my code is getting stuck in finite loop in the while loop of addPort function. I tried to debug but couldn’t figure out the issue. Could you please guide me what might be the possible cause? I am modified my group template referring to the node template from the sample.

Here’s a minimal example:

<!DOCTYPE html>
<html>
<head>
  <title>Groups with Ports</title>
  <!-- Copyright 1998-2022 by Northwoods Software Corporation. -->
  <meta name="description" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

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

myDiagram =
  new go.Diagram("myDiagramDiv",
    {
      layout: $(go.TreeLayout),
      "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.groupTemplate =
  $(go.Group, "Table",
    $(go.Panel, "Vertical",
      new go.Binding("itemArray", "I"),
      {
        column: 0,
        itemTemplate:
          $(go.Panel,
            { margin: 1 },
            $(go.Shape,
              { toLinkable: true, strokeWidth: 0, width: 8, height: 8 },
              new go.Binding("portId", "id"))
          )
      }
    ),
    $(go.Panel, "Auto",
      { column: 1 },
      $(go.Shape, { fill: "transparent" }),
      $(go.Placeholder, { padding: 10 })
    ),
    $(go.Panel, "Vertical",
      new go.Binding("itemArray", "O"),
      {
        column: 2,
        itemTemplate:
          $(go.Panel,
            { margin: 1 },
            $(go.Shape,
              { fromLinkable: true, strokeWidth: 0, width: 8, height: 8 },
              new go.Binding("portId", "id"))
          )
      }
    ),
    {
      contextMenu: $("ContextMenu",
          $("ContextMenuButton",
            $(go.TextBlock, "Add Input"),
            { click: (e, obj) => addPort("I") }),
          $("ContextMenuButton",
            $(go.TextBlock,"Add Output"),
            { click: (e, obj) => addPort("O") })
        )
    }
  );

// Add a port to the specified side of the selected nodes.
function addPort(side) {
  myDiagram.startTransaction("addPort");
  myDiagram.selection.each(node => {
    // skip any selected Links
    if (!(node instanceof go.Node)) return;
    // compute the next available index number for the side
    let i = 1;
    while (node.findPort(side + i.toString()) !== node) i++;
    // now this new port name is unique within the whole Node because of the side prefix
    const name = side + i.toString();
    // get the Array of port data to be modified
    const arr = node.data[side];
    if (arr) {
      // create a new port data object
      const newportdata = { id: name };
      // and add it to the Array of port data
      myDiagram.model.addArrayItem(arr, newportdata);
    }
  });
  myDiagram.commitTransaction("addPort");
}


myDiagram.model =
  new go.GraphLinksModel(
    {
      copiesArrays: true,
      copiesArrayObjects: true,
      linkToPortIdProperty: "topid",
      linkFromPortIdProperty: "frompid",
      nodeDataArray: [
        { key: 1, isGroup: true,
          I: [
            { id: "I1" },
            { id: "I2" }
          ],
          O: [
            { id: "O1" }
          ]
        },
        { key: 2, isGroup: true,
          I: [
            { id: "I1" },
            { id: "I2" }
          ],
          O: [
            { id: "O1" }
          ]
        },
        { group: 1 },
        { group: 1 },
        { group: 1 },
        { group: 2 }
      ],
      linkDataArray: [
        { from: 1, frompid: "O1", to: 2, topid: "I2" }
      ]
    });
  </script>
</body>
</html>

Thanks! This works fine but for some reason, if I integrate this group template with my code, it creates a port for all the groups in the diagram while creating for one group. I tried to debug the addPort method, but it seems it is being called only once. Still, all the groups in the diagram get a new port added.

Ah, that’s probably because you copied some nodes, and unless you make arrangements for the Arrays to be copied too, they end up being shared by multiple nodes. The easiest solution is to set Model.copiesArrays and Model.copiesArrayObjects to true, as I have done in the edited solution.