Creating dynamic table and field grouping structure

I need to be able to allow creating structures like this -

example

There is a top level structure which can have editable title (other visual features like background, stroke color, etc.). Let’s call it container.

This top level structure will have multiple fields inside it. Each field must be linkable to each other and structures outside the top level container structure. But they should not exist outside of the container structure i.e. cannot be dragged out of the container structure.

The fields can be grouped together and the group can have its own bindings like title (other visual features like background, etc.)

The container, fields and field groups should all be selectable on their own and have unique ports.

If a field is dragged and dropped into an existing internal fields group it should become part of it and if it is dragged out into a non grouped section of the container it should become ungrouped again.


Previously we did not need the field grouping functionality so a table with fields like Record Mapper example and positioned custom ports on the table rows sufficed for us. Now with the dynamic nature of the field grouping and ungrouping what is the ideal way for handling this -

A panel with table (using nested itemsArray of some sort) or making each field it’s own node and the other parts as group categories. Or any other way that I haven’t thought of.

The one drawback of using Groups, I found is the need to create multiple separate nodes for this whole structure that always stays together. Especially when dragging and dropping them from a HTML based palette.

Any help is greatly appreciated. Thanks.

Would something like Dragging Fields Between Records help? Of course you would need to split up the node into two separate lists of fields (i.e. two itemArrays) and make other changes in the decorations of the node.

Also it is not clear to me whether you might require the functionality demonstrated in Dragging a Field from a Record onto an HTML Element

Making two item arrays or multiple item arrays will help manage the internal grouping but how do I represent the ungrouped ones in my data?

Also can each part of the nodes made selectable on their own? Like if I click on the field node itself that should trigger SelectionChanged with that specific node as subject?

You have to decide if you need to implement each field as a separate node within a group, or as separate elements within a node. Either way should be feasible, but some things can be more difficult one way than the other.

If you have elements within a node, then those elements will not be Parts, so one cannot set Part.isSelected on them. However one can implement functionality that is similar to the selection of Parts: Mapping Selectable Fields of Records

I like the making each field a node and the container and field groups as Groups approach. But my concern with that is creating of the whole container group and inner field groups and fields on the right location when dragging and dropping from an HTML palette. This is a major reason that I am not sure which approach is the correct one.

When the drop happens you will want to add node data to the model and then locate the corresponding Node after converting the drop point into document coordinates.

It isn’t clear to me what the problem would be. If you need to create a Group with some member nodes in it, instead of a simple node, you can do that just by adding multiple node data objects to the model.

I decided to use the grouping structure to represent the node. But, now I am having difficulty in designing the node. How do I get the members of a group to stretch horizontally to their parent size?

Like in the image in the original post.

Normally it would depend on the layouts to determine the size and position of the nodes. I can give you a sample later today.

That will be very helpful. Thanks.

<!DOCTYPE html>
<html>
<head>
<title>Uniform Column Layout</title>
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="go.js"></script>
<script id="code">
  function UniformColumnLayout() {
    go.Layout.call(this);
    this.verticalSpacing = 10;
    // these properties are only used for the Diagram.layout, not for each Group.layout:
    this.overallWidth = 500;
    this.nestingWidthDiff = 26;  // ??? depends on Group template details!
  }
  go.Diagram.inherit(UniformColumnLayout, go.Layout);

  UniformColumnLayout.prototype.doLayout = function(coll) {
    var diagram = this.diagram;
    diagram.startTransaction("Layout");

    if (this.group === null) {
      // now set all widths based on Part.findSubGraphLevel()
      var it = diagram.nodes.iterator;
      while (it.next()) {
        var node = it.value;
        node.width = this.overallWidth - node.findSubGraphLevel() * this.nestingWidthDiff;
      }
    }

    this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
    var allparts = this.collectParts(coll);

    var x = this.arrangementOrigin.x;
    var y = this.arrangementOrigin.y;
    var it = allparts.iterator;
    while (it.next()) {
      var part = it.value;
      if (part instanceof go.Link) continue;
      part.moveTo(x, y);
      y += this.verticalSpacing + part.actualBounds.height;
    }

    diagram.commitTransaction("Layout");
  }


  function init() {
    var $ = go.GraphObject.make;

    myDiagram =
      $(go.Diagram, "myDiagramDiv",
        {
          allowMove: false,
          layout: $(UniformColumnLayout, { overallWidth: 600 })
        });

    myDiagram.nodeTemplate =
      $(go.Node, "Auto",
        $(go.Shape,
          { fill: "white", portId: "" },
          new go.Binding("fill", "color")),
        $(go.TextBlock,
          { margin: 8, editable: true },
          new go.Binding("text").makeTwoWay())
      );

    myDiagram.groupTemplate =
      $(go.Group, "Auto",
        { layout: $(UniformColumnLayout) },
        $(go.Shape, { fill: "white", stroke: "steelblue", strokeWidth: 3 }),
        $(go.Panel, "Table",
          { stretch: go.GraphObject.Horizontal },
          $(go.RowColumnDefinition, { row: 0, background: "steelblue" }),
          $("SubGraphExpanderButton", { row: 0, column: 0, margin: 2 }),
          $(go.TextBlock,
            { row: 0, column: 1, stretch: go.GraphObject.Horizontal, stroke: "white", editable: true },
            new go.Binding("text").makeTwoWay()),
          $("Button", { row: 0, column: 2, alignment: go.Spot.Right, margin: 2 },
            $(go.TextBlock, "button"),
            //{ click: . . . }
          ),
          $(go.Placeholder, { row: 1, column: 0, columnSpan: 3, alignment: go.Spot.TopLeft, padding: 10 })
        )
      );

    myDiagram.model = new go.GraphLinksModel(
      [
        { key: 1, text: "Alpha", color: "lightblue", group: 13 },
        { key: 2, text: "Beta", color: "orange", group: 13 },
        { key: 3, text: "Gamma", color: "lightgreen", group: 14 },
        { key: 4, text: "Delta", color: "pink", group: 12 },
        { key: 11, text: "Group 1", isGroup: true },
        { key: 12, text: "Group 2", isGroup: true, group: 11 },
        { key: 13, text: "Group 3", isGroup: true, group: 12 },
        { key: 14, text: "Group 4", isGroup: true },
      ]);
  }
</script>
</head>
<body onload="init()">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
</body>
</html>

I applied the layout class to group only not the diagram. But the nodes inside the group did not follow the layout. Any reason why?

Look at what UniformColumnLayout.doLayout does.

Had a slightly custom setup of this working. Thanks.