Containers expand in a different location when they contain child components

My application allows users to create diagrams with expandable nodes that can contain children. When a node with a child element is moved and then expanded, it expands close to the previous location it was in.

I am binding the ‘location’ of all elements with a variable ‘loc’ in the model. I use group templates to handle the different states of the elements. When elements are minimized, they have a ‘groupMinimized’ template added to them using setCategoryForNodeData. They have another group template added when they are expanded.

I know the locations are getting updated because I log them out in the ‘selectionMoved’ handler. (For both parent and child when I move the parent)

Ideally, I’d like for containers with children in them to expand in the same location where they are at.

Initial location of node

Node after dragging

Node on expand goes back to the initial position of the node

I am using a custom extension arrangingLayout that uses underlying gridLayouts. I am also using ReactDiagram. The locationSpot for all nodes are go.Spot.Center. The group templates use a placeholder that also has a two way binding on position.

What is the Group.layout? How is it defined?

  $(ArrangingLayout, {
    primaryLayout: new go.LayeredDigraphLayout({
      isOngoing: false,
      isInitial: initial,
      layerSpacing: 500,
      layeringOption: go.LayeredDigraphLayout.LayerOptimalLinkLength,
      aggressiveOption: go.LayeredDigraphLayout.AggressiveMore,
    }),
    arrangingLayout: new go.GridLayout({
      isOngoing: false,
      isInitial: initial,
      wrappingColumn: 1,
      spacing: new go.Size(50, 50),
    }),
    sideLayout: new go.GridLayout({
      isOngoing: false,
      isInitial: initial,
      wrappingColumn: 5,
      spacing: new go.Size(50, 50),
    }),
    isOngoing: false,
    isInitial: initial,
  })

It is added to the group using:

layout: DiaLayout(true).copy()

Where DiaLayout returns the above layout.

I’ll check to see if ArrangingLayout is properly supporting Layout.isOngoing.

This issue is sporadic. It happens for a little under 50% of the time.

I think that only the isOngoing on the ArrangingLayout matters – if it does get invalidated and then performed again, it ignores the setting of isOngoing on each of its three (primary, arranging, and side) layouts.

But you have set isOngoing to false on each ArrangingLayout that you assign to Group.layout, so it ought to be the case that collapsing and expanding a Group won’t cause the member nodes to move relative to each other.

I’m wondering if it’s due to your swapping the templates – I believe that any newly created Group instance will cause its new layout to be invalid and thus be performed towards the end of the transaction. Note that Layout.isInitial only applies to when a diagram gets a new Diagram.model (or by a call to Diagram.delayInitialization), so isInitial should have no effect on new instantiations of Groups.

Can you change to using a single Group template that, depending on the Group.isSubGraphExpanded property, shows what you want?

Here’s a complete stand-alone sample that you can try. I have not encountered any problems. But I’m sure I haven’t implemented all of your requirements. (I’m assuming that the details and styling of the nodes and groups don’t really matter too much.)

<!DOCTYPE html>
<html>
<head>
  <title>Simple Groups</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  Double-click on Groups to collapse or expand them.
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
const myDiagram =
  new go.Diagram("myDiagramDiv", {
      "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("Auto", { width: 200, height: 100, locationSpot: go.Spot.Center })
    .bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify)
    .add(
      new go.Shape({ fill: "white" })
        .bind("fill", "color"),
      new go.TextBlock()
        .bind("text")
    );

myDiagram.groupTemplate =
  new go.Group("Auto", {
      locationSpot: go.Spot.Center,
      doubleClick: (e, grp) => {
        if (grp.isSubGraphExpanded) {
          e.diagram.commandHandler.collapseSubGraph(grp);
         } else {
          e.diagram.commandHandler.expandSubGraph(grp);
        }
      }
    })
    .bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify)
    .bindTwoWay("isSubGraphExpanded", "exp")
    .add(
      new go.Shape({ fill: "transparent", strokeWidth: 2, strokeDashArray: [8, 2] })
        .bindObject("fill", "", grp => grp.isSubGraphExpanded ? "transparent" : (grp.data.color || "transparent")),
      new go.Panel("Spot")
        .add(
          new go.Placeholder({ padding: 20, minSize: new go.Size(200, 80) }),
          new go.TextBlock({ margin: 4, font: "bold 12pt sans-serif" })
            .bindObject("alignment", "isSubGraphExpanded", x => x ? go.Spot.Top : go.Spot.Center)
            .bindObject("alignmentFocus", "isSubGraphExpanded", x => x ? go.Spot.Bottom : go.Spot.Center)
            .bind("text")
        )
    );

myDiagram.model = new go.GraphLinksModel([
{"key":0,"isGroup":true,"text":"Outer","color":"whitesmoke","loc":"276.5 226.12105197906496","exp":true},
{"key":1,"isGroup":true,"group":0,"text":"Alpha Group","color":"lightblue","loc":"59 86.62105197906493","exp":true},
{"key":11,"group":1,"text":"Alpha","color":"lightblue","loc":"59 86.62105197906493"},
{"key":2,"isGroup":true,"group":0,"text":"Beta Group","color":"orange","loc":"514 89.62105197906493","exp":false},
{"key":21,"group":2,"text":"Beta","color":"orange","loc":"514 89.62105197906493"},
{"key":3,"isGroup":true,"group":0,"text":"Gamma Group","color":"lightgreen","loc":"261 416.9315779685974","exp":false},
{"key":31,"group":3,"text":"Gamma","color":"lightgreen","loc":"261 416.9315779685974"},
{"key":4,"isGroup":true,"group":0,"text":"Delta Group","color":"pink","loc":"404 317.9315779685974","exp":true},
{"key":41,"group":4,"text":"Delta","color":"pink","loc":"404 317.9315779685974"}
]);
  </script>
</body>
</html>

My functionality between templates are different enough to warrant separate templates. Would it make sense saving the location values of the minimized template and manually adding it to the expanded template when I switch?

Also, this behavior is displayed only for nodes with children. I can’t recreate the issue on nodes without children elements. (Nodes with empty placeholders)

OK, I’ve changed my sample to use two separate Group templates, “” for expanded, and “Collapsed”. It seems to work well, although I’m not sure what kinds of operations I should try.

<!DOCTYPE html>
<html>
<head>
  <title>Simple Groups</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  Double-click on Groups to collapse or expand them.
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
const myDiagram =
  new go.Diagram("myDiagramDiv", {
      "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("Auto", { width: 200, height: 100, locationSpot: go.Spot.Center })
    .bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify)
    .add(
      new go.Shape({ fill: "white" })
        .bind("fill", "color"),
      new go.TextBlock()
        .bind("text")
    );

myDiagram.groupTemplate =
  new go.Group("Auto", {
      locationSpot: go.Spot.Center,
      isSubGraphExpanded: true,
      doubleClick: (e, grp) => {
        e.diagram.model.commit(m => {
          m.setCategoryForNodeData(grp.data, "Collapsed");
        });
      }
    })
    .bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify)
    .add(
      new go.Shape({ fill: "transparent", strokeWidth: 2, strokeDashArray: [8, 2] }),
      new go.Panel("Spot")
        .add(
          new go.Placeholder({ padding: 20, minSize: new go.Size(200, 80) }),
          new go.TextBlock({
              margin: 4,
              font: "bold 12pt sans-serif",
              alignment: go.Spot.Top,
              alignmentFocus: go.Spot.Bottom
            })
            .bind("text")
        )
    );

myDiagram.groupTemplateMap.add("Collapsed",
  new go.Group("Auto", {
      locationSpot: go.Spot.Center,
      isSubGraphExpanded: false,
      doubleClick: (e, grp) => {
        e.diagram.model.commit(m => {
          m.setCategoryForNodeData(grp.data, "");
        });
      }
    })
    .bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify)
    .add(
      new go.Shape({ fill: "white", strokeWidth: 2, strokeDashArray: [8, 2] })
        .bind("fill", "color"),
      new go.Panel("Spot")
        .add(
          new go.Placeholder({ padding: 20, minSize: new go.Size(200, 80) }),
          new go.TextBlock({ margin: 4, font: "bold 12pt sans-serif" })
            .bind("text")
        )
    ));

myDiagram.model = new go.GraphLinksModel([
{"key":0,"isGroup":true,"category":"","text":"Outer","color":"whitesmoke","loc":"276.5 226"},
{"key":1,"isGroup":true,"category":"","group":0,"text":"Alpha Group","color":"lightblue","loc":"59 87"},
{"key":11,"group":1,"text":"Alpha","color":"lightblue","loc":"59 87"},
{"key":2,"isGroup":true,"category":"Collapsed","group":0,"text":"Beta Group","color":"orange","loc":"514 90"},
{"key":21,"group":2,"text":"Beta","color":"orange","loc":"514 90"},
{"key":3,"isGroup":true,"category":"Collapsed","group":0,"text":"Gamma Group","color":"lightgreen","loc":"261 417"},
{"key":31,"group":3,"text":"Gamma","color":"lightgreen","loc":"261 417"},
{"key":4,"isGroup":true,"category":"","group":0,"text":"Delta Group","color":"pink","loc":"424 318"},
{"key":41,"group":1,"text":"Delta","color":"pink","loc":"184 218"}
]);
  </script>
</body>
</html>

I converted my tool to use a single template. I still see the issue.
Could it be related to the placeholder? I build a new placeholder everytime. Is it possible to bind the placeholder position value to the model like we do the location for nodes?

Note that in my second sample, I used two Group templates, each of which had a Placeholder. (I suppose the “Collapsed” template really doesn’t need a Placeholder, but I haven’t tried that.) So new Placeholders are being created each time the group’s category changes.

If a Group (or Adornment) has a Placeholder, it is always the object in which the location is determined. It would be really confusing otherwise.

Does my second sample work basically as you want? It’s pretty simple.

Thanks Walter
Your second sample works on it’s own. I tried implementing your templates in my tool and I still see the issue on my end.

When a category changes and a new placeholder is created, won’t that give it a new position? Can I carry over the old placeholder’s position to the newly created one?

I am using an older version of goJS (2.3.12). Could that be adding to the issue?

Walter’s sample should work in 2.3 as well.

Are you saying that after drag and expansion, the group is still moving back to its original position? Did you use Walter’s templates directly, or adjust them to your application?

I’m wondering if it’s still something to do with your layout configuration or if it could be stale data in your React state affecting things.

When I expand a node after dragging it, it opens up relatively close (Not exactly at) its original position only when it contains child elements.

I changed my Node and Group templates to what Walter had posted earlier.

Ok, we need to figure out what is different in your app that is causing this.

Do you still have a layout on your Diagram? How are you updating your React state?

I am using diagram layout as well as a group layout. The nodes in the screenshot above are in the 2nd level. They are part of the subGraph of a group that has the “expanded” template.
The ReactDiagram component is re-rendered using a useMemo.

I am using the GuidedDraggingTool extension as well. I did notice that this behavior is negligible when I disable it and use the default dragging tool.

Ok, it sounds to me like stale React state that’s being reintroduced. I imagine you’ve seen our gojs-react-basic sample? It also uses the GuidedDraggingTool. You’ll want to make sure your onModelChange function is properly updating state and that you’re skipping GoJS model updates when needed via skipsDiagramUpdate.