Add a node to a group which has a different layout

I am trying to add an error type node to a group that contains multiple other children task nodes with padding around the task nodes. The error node belongs to the same group as the task nodes but should have a different layout. The task nodes are arranged linearly in the middle of the group node, but the error node should be attached to the bottom of the group node and hanging over the edge, like an adornment. I need to be able to add multiple error nodes to this parent group and have them re-layout along the bottom of the group node. Every time a new task node is added or removed from the group, the error nodes should still stay on the bottom edge of the group, but the group should resize to the size of the children task nodes. Is this possible, and if so, is there a best way to approach this problem?

This is what it should look like:
06%20PM

So you are specifyng a Group.layout, where it had none before? If so, yes, the layout needs to handle those different kinds of nodes differently.

I suggest that you define a custom Layout that positions all of the tasks and then positions all of the error nodes. Have you read Extending GoJS -- Northwoods Software ?

Presumably you will want to count the number of tasks and decide how many rows you want to put them in. Then you can assign their positions.

If you count up the number of error nodes, you can figure out exactly how to position them then, since you will know how large the group will be (both width and height), so that you can position the error nodes properly starting from the bottom-right corner and proceeding leftward.

I have a placeholder for the group that re-sizes the group when a node is dropped in as a child. Right now, the error node is treated just like every other node and is arranged linearly in the group, which resizes to hold the error node. What I want to happen, is the error node is in a fixed position relative to the size of the group, always in the bottom right corner, no matter if there is 1 or 10 child nodes in the group. Can I keep the placeholder for the group and have this error node arranged like I had in the picture?

Yes, but if you have a “border” around the Placeholder, you’ll want to use a negative Margin.bottom so that the bottom of border Shape horizontally bisects those error nodes.

Don’t worry about that now – get the custom layout implemented first. Then later you can worry about the appearance of the Group.

What do you think about using GridLayout to implement a Panel/Table? I could have 2 rows, one for task nodes and one for error nodes. The mouse drop event could handle adding the task nodes from left to right in the first row and from right to left in the second row. Do you think I could still use a negative margin to get the effect of the second row being bisected by a border?

Using a GridLayout would make it hard to control which nodes go into which row.

A custom layout will let you do anything you want. In this case, it shouldn’t be hard to implement.

I have a custom layout built off of GridLayout because of past functionality I have to support. I am able to use Part.move() to move the error nodes to a different part of the group. How would I make sure that the group is large enough to hold all of the nodes within it every time I drag and drop and new node into the group, while also keeping the error nodes pinned to the bottom of the group?

If the Group’s template includes a Placeholder, then it will automatically be large enough to hold all of its member nodes. GoJS SubGraphs -- Northwoods Software

In fact, you want to make the border around the Placeholder to be shorter than it should be so that the border cuts through the middle of those error nodes. That’s why I suggested using a negative Margin.bottom.

I have the border on as a stroke on a shape. This is the basis for my navigation group with a placeholder and stroke on rounded rectangle shape. Where would I have the padding with a negative margin bottom? On the auto Panel?

   $(go.Group,
      'Spot',
      this.nodeStyle(),
      {
        computesBoundsAfterDrag: true,
        handlesDragDropForMembers: true,
        layout: $(NavigationGroupLayout, { //based on GridLayout
          wrappingWidth: Infinity,
          alignment: go.GridLayout.Position,
          cellSize: new go.Size(1, 1),
          spacing: new go.Size(10, 10),
          comparer: function(a, b) {
            // Allow nodes within the nav group to be rearranged via the x-axis
            const ax = a.location.x
            const bx = b.location.x
            if (isNaN(ax) || isNaN(bx)) return 0
            if (ax < bx) return -1
            if (ax > bx) return 1
            return 0
          }
        }),
        mouseDragEnter: function(e, obj) {
          obj.background = 'rgba(151,151,255,0.8)'
        },
        mouseDragLeave: function(e, obj) {
          obj.background = 'transparent'
        },
        memberValidation: (group, node) => {},
        mouseDrop: function(e, grp) {}
      },
      $(
        go.Panel,
        'Vertical',
        // Navigation Shape
        $(
          go.Panel,
          'Auto',
          {
            stretch: go.GraphObject.Horizontal
          },
          $(
            go.Shape,
            'RoundedRectangle',
            {
              minSize: new go.Size(this.GroupNodeWidth, this.GroupNodeHeaderHeight),
              fill: 'transparent',
              stroke: this.GroupNodeStroke,
              strokeWidth: this.GroupNodeStrokeWidth,
              strokeDashArray: [8, 2],
              parameter1: 4 // corner radius
            },
            new go.Binding('stroke', 'isSelected', (isSelected) => {
              return isSelected ? this.GroupNodeSelectedStroke : this.GroupNodeStroke
            }).ofObject(),
            new go.Binding('strokeWidth', 'isSelected', (isSelected) => {
              return isSelected ? 2 : this.GroupNodeStrokeWidth
            }).ofObject()
          ),
          // Header Panel
          $(
            go.Panel,
            'Horizontal',
            {
              alignment: go.Spot.TopLeft,
              stretch: go.GraphObject.Horizontal,
              height: this.GroupNodeHeaderHeight - 1,
              background: this.GroupNodeHeaderFill
            },
            $('SubGraphExpanderButton', {
              alignment: go.Spot.Right,
              margin: 5
            }),
            $(
              go.TextBlock,
              this.textStyle(),
              {
                textAlign: 'start',
                overflow: go.TextBlock.OverflowClip,
                editable: true
              },
              new go.Binding('text', 'label').makeTwoWay()
            )
          ),
          // Dropzone placeholder
          $(go.Placeholder, {
            padding: new go.Margin(this.GroupNodeHeaderHeight + 5, 10, 10, 10)
          })
        )
      ),
      Port.getPort(true, {
        portId: 'in',
        alignment: go.Spot.Left
      }),
      Port.getPort(false, {
        portId: 'out',
        alignment: go.Spot.Right
      })
    )
  function init() {
    var $ = go.GraphObject.make;

    myDiagram =
      $(go.Diagram, "myDiagramDiv");

    myDiagram.nodeTemplate =
      $(go.Node,
        new go.Binding("location", "loc", go.Point.parse),
        $(go.Shape,
          { strokeWidth: 0, fill: "dodgerblue", width: 45, height: 30 })
      );

    myDiagram.groupTemplate =
      $(go.Group, "Auto",
        $(go.Shape,
          { fill: "whitesmoke" }),
        $(go.Placeholder,
          new go.Binding("padding", "margin", go.Margin.parse))
      )

    myDiagram.model = new go.GraphLinksModel(
    [
      { key: 1, loc: "0 0", group: -1 },
      { key: 4, loc: "100 50", group: -1 },
      { key: -1, isGroup: true, margin: "5" },
      { key: 11, loc: "200 0", group: -10 },
      { key: 14, loc: "300 50", group: -10 },
      { key: -10, isGroup: true, margin: "5 5 -15 5" }
    ]);
  }

produces:
image

Note how the group automatically adjusts as you drag around one of its member nodes.

I’m having some trouble with the layout after I have added an error event and am now interacting with the group nodes. If I delete a group node, the group members jump to a different location.
This is the beginning:
35%20AM

And this is after I delete an error event:
52%20AM

Same with if I delete a group member:
39%20AM

Or even if I click on a group member:

I’ve included my navigation group layout below. Right now I am relying on the grid layout to deal with my regular group nodes and manually placing the event nodes. However, I don’t understand why the event nodes don’t update their position correctly when the diagram is interacted with.

function NavigationGroupLayout() {
  go.GridLayout.call(this)
}

go.Diagram.inherit(NavigationGroupLayout, go.GridLayout)

/** @override */
NavigationGroupLayout.prototype.doLayout = function(coll) {
  this.diagram.startTransaction('NavigationGroupLayout')

  let eventNodes = new go.Set()
  
  coll.memberParts.each((part) => {
    if (part.category === ErrorEvent.category) {
      part.isLayoutPositioned = false
      eventNodes.add(part)
    }
  })
  
  go.GridLayout.prototype.doLayout.call(this, coll)

  coll.ensureBounds()
  let newX = coll.actualBounds.width
  eventNodes.each(node => {
    node.move(new go.Point(newX + node.actualBounds.width, coll.actualBounds.height * 0.75))
    newX -= node.actualBounds.width
  })

  this.diagram.commitTransaction('NavigationGroupLayout')
}

If your Group template includes a Placeholder (as is likely), then the area of the Placeholder includes all of the member Nodes, including the event nodes. That’s not what you want, is it? I think you need to step through the code and look at the values.

I found the answer to my question about the moving nodes on click events and upon deletion. The error nodes location weren’t being computed correctly. By using just the navigation group members actual bounds minus the error events and using the bottom and right bounds, I was able to pin the nodes to the bottom right corner.

function NavigationGroupLayout() {
  go.GridLayout.call(this)
}

go.Diagram.inherit(NavigationGroupLayout, go.GridLayout)

/** @override */
NavigationGroupLayout.prototype.doLayout = function(coll) {
  this.diagram.startTransaction('NavigationGroupLayout')
  let navNodes = new go.Set()
  let eventNodes = new go.Set()

  coll.memberParts.each((part) => {
    if (part.category === ErrorNode.category) {
      part.isLayoutPositioned = false
      eventNodes.add(part)
    } else {
      navNodes.add(part)
    }
  })

  go.GridLayout.prototype.doLayout.call(this, coll)

  coll.ensureBounds()
  const { right: rightCoordinate, bottom: bottomCoordinate } = this.diagram.computePartsBounds(navNodes)
  eventNodes.each((node) => {
    node.move(
      new go.Point(rightCoordinate - node.actualBounds.width - 20, bottomCoordinate - node.actualBounds.height / 2)
    )
    rightCoordinate -= node.actualBounds.width
  })

  this.diagram.commitTransaction('NavigationGroupLayout')
}