Keeping Two Gojs Groups in Sync

Hello,

Suppose that I have two gojs groups: Group A and Group B. (similar to this example)

Each containing a similar set of graph objects connected by links.

I would like to design a main group that controls the other one(s).

Whenever I make changes in Group A for instance, Group B should follow and replicate the same changes. Such as adding elements, repositioning, connecting, or deleting.

What is the best way to handle this use case?

Do both Groups need to be part of the same Diagram, representing the same model?

What happens to changes to Group B or its members? Do they also get reflected in Group A?

Might there be a Group C or a Group D, and how would they participate, if at all?

Yep, both are parts of the same diagram.

I might have more groups, but there is only one main group. Im my example, “Group A“ is the main group.

Hmmm, a lot of questions remain, but presumably you know what you want and can specify all possible behaviors the way that you want.

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2026 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
  Double-click on a Node to toggle its color.
  Drag Beta into Group A and see Beta's double created in Group B.
  Drag Beta out of Group A and see Beta's double deleted.
  <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">
const myDiagram =
  new go.Diagram("myDiagramDiv", {
      mouseDrop: e => {
        e.diagram.commandHandler.addTopLevelParts(e.diagram.selection, true);
      },
      "undoManager.isEnabled": true,
      "ModelChanged": e => {
        const GroupAKey = 5;
        const GroupBKey = 7;
        if (e.change === go.ChangeType.Property) {
          // original data object in Group A
          const d = e.object;
          // G is the original Group A data object
          const g = e.model.findNodeDataForKey(GroupAKey);
          if (!g) return;
          // GC is the copied group data object
          const gc = e.model.findNodeDataForKey(GroupBKey);
          if (!gc) return;
          // now copy the property from D to DC appropriately
          const pname = e.propertyName;
          if (pname === "group" || e.modelChange === "nodeGroupKey") {
            if (e.newValue === GroupAKey) {  // added to Group A
              // DC is the copied data object in Group B
              if (d.copy === undefined) {  // not already copied
                const dc = e.model.copyNodeData(d);
                e.model.addNodeData(dc);
                // fix "loc" to be relative to group
                const p = go.Point.parse(d.loc);
                p.subtract(go.Point.parse(g.loc)).add(go.Point.parse(gc.loc));
                e.model.set(dc, "loc", go.Point.stringify(p));
                e.model.setGroupKeyForNodeData(dc, GroupBKey);
                // also indicate that D now has a double in Group B
                e.model.set(d, "copy", e.model.getKeyForNodeData(dc));
              }
            } else if (e.oldValue === GroupAKey) {  // removed from Group A
              // DC is the copied data object in Group B
              const dc = e.model.findNodeDataForKey(d.copy);
              if (!dc) return;
              if (dc.group !== GroupBKey) return;
              // is this what you want???
              e.model.removeNodeData(dc);
              e.model.set(d, "copy", undefined);  // no more double
            }
          } else {
            if (d.group !== GroupAKey) return; // not in group A?
            if (d.copy === undefined) return;  // not copied?  ignore change
            // DC is the copied data object in Group B
            const dc = e.model.findNodeDataForKey(d.copy);
            if (!dc) return;
            if (dc.group !== GroupBKey) return;
            if (pname === "loc") {
              const p = go.Point.parse(d[pname]);
              p.subtract(go.Point.parse(g.loc)).add(go.Point.parse(gc.loc));
              e.model.set(dc, pname, go.Point.stringify(p));
            } else {  // just copy all other property settings
              e.model.set(dc, pname, d[pname]);
            }
          }
        } else {  // 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("Auto", {
      doubleClick: (e, node) => {
        e.diagram.commit(diag => {
          diag.model.set(node.data, "color", node.data.color === "yellow" ? "orange" : "yellow");
        });
      }
    })
    .bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify)
    .add(
      new go.Shape({ fill: "white" })
        .bind("fill", "color"),
      new go.TextBlock({ margin: 8 })
        .bind("text")
    );

myDiagram.groupTemplate =
  new go.Group("Vertical", {
      computesBoundsAfterDrag: true,  // don't stretch the Placeholder while dragging
      mouseDrop: (e, grp) => {
        grp.addMembers(grp.diagram.selection, true);
      },
      handlesDragDropForMembers: true,  // don't need to define handlers on member Nodes and Links
    })
    .bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify)
    .add(
      new go.TextBlock()
        .bind("text"),
      new go.Panel("Auto")
        .add(
          new go.Shape({ fill: "transparent", strokeWidth: 3 })
            .bind("stroke", "color"),
          new go.Placeholder({ padding: 10 })
        )
    );

myDiagram.model = new go.GraphLinksModel({
  "class": "GraphLinksModel",
  "nodeDataArray": [
{"key":1,"text":"Alpha","color":"lightblue","loc":"0 0"},
{"key":2,"text":"Beta","color":"orange","loc":"70.28947448730469 0"},
{"key":3,"text":"Gamma","color":"lightgreen","group":5,"loc":"-3.70001220703125 96.86141424179077","copy":6},
{"key":4,"text":"Delta","color":"pink","group":5,"loc":"139.12278747558594 127.26140813827514","copy":8},
{"key":5,"text":"Group A","isGroup":true,"loc":"-13.70001220703125 86.86141424179077","copy":7},
{"key":6,"text":"Gamma","color":"lightgreen","group":7,"loc":"247.7332763671875 40.494745540618894"},
{"key":7,"text":"Group B","isGroup":true,"loc":"237.7332763671875 30.494745540618894"},
{"key":8,"text":"Delta","color":"pink","group":7,"loc":"390.5560760498047 70.89473943710327"}
],
  "linkDataArray": [
{"from":1,"to":2},
{"from":1,"to":3},
{"from":3,"to":4},
{"from":6,"to":8}
]});
  </script>
</body>
</html>