Column span binding not working

Hi, I have a table layout, whose children are also table layouts following the same template inside their parents. The template has row, column and columnSpan bindings with properties in the nodeData. The row and column properties work fine, but the columnSpan property is not working.
Here is the template, for reference:

const tableLayout = $(gojs.Group, "Vertical",
    new gojs.Binding("location", "loc", gojs.Point.parse).makeTwoWay(gojs.Point.stringify),
    { 
      alignment: gojs.Spot.TopLeft,  // affects TableLayout of containing group
      margin: 4,  // affects TableLayout of containing group
      layout: $(TableLayout),  // affects arrangement of member nodes
      minSize: new gojs.Size(100, 50),  // affects this panel (i.e. whole group)
      defaultStretch: gojs.GraphObject.Horizontal  // affects elements of this panel
    },
    new gojs.Binding("row", "row"),  // these all affect TableLayout of containing Group
    new gojs.Binding('column', 'col'),
    new gojs.Binding('columnSpan', 'columnSpan'),
    // the header
    $(gojs.Panel, "Table",
      { padding: new gojs.Margin(8, 4) },
      $(gojs.Panel, "Vertical",
        $(gojs.Panel, "Horizontal",
                $(gojs.TextBlock,
                { font: "bold 16pt sans-serif", margin: new gojs.Margin(0, 4),desiredSize: new gojs.Size(250,50)},
                new gojs.Binding("text", "", data => `Type: ${data.kindName}`)
                ),
        ), 
        $(gojs.Panel, "Horizontal",
            $("SubGraphExpanderButton"),
            $(gojs.TextBlock,
            { font: "bold 12pt sans-serif", margin: new gojs.Margin(0, 4) , desiredSize: new gojs.Size(250,50)},
            new gojs.Binding("text", "", data => `Table: ${data.key}`)
            )
        )
      )
    ),
    // the body holding the member nodes
    $(gojs.Panel, "Auto",
      $(gojs.Shape, { fill: null }),
      $(gojs.Placeholder, { padding: 8, alignment: gojs.Spot.Left })
    )
  );

Note: The rendering is being performed on nodes in a backend server

For reference, here is an image. The container in the bottom left (the one named “GlobalExceptionFilter”) has a columnSpan of 5 and should be spanning the entire width of its container

If you select that group and then in the console evaluate: myDiagram.selection.first().columnSpan, does it return 5?
(Of course substitute however you need to get a reference to your Diagram.)

I suppose you should also check that its row value is 1 and its column value is zero.

Oh, I see what’s probably missing: you need to set stretch: go.GraphObject.Horizontal on the group template, so that it stretches horiziontally to fill the assigned column(s).

Separate suggestion: maybe you want to change alignment from go.Spot.Left to go.Spot.TopLeft. Although that’s just a guess on my part.

So I have verified that which you asked me to, and even though node data shows the group having columnSpan = 5, the myDiagram.selection.first().columnSpan returns 1. For reference, here is my table template on the front-end. Note that I am not doing any bindings except for th location binding here, because the idea is that the backend computes the exact location for the node and possibly where it is supposed to end as well, so I am omitting the colSpan binding on the front-end to avoid the re-computation of its size on the front-end:

const tableLayout = $(go.Group, "Vertical",

      new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
      new go.Binding("isSubGraphExpanded", "isSubGraphExpanded"),
      {defaultStretch: go.GraphObject.Horizontal, alignment: go.Spot.TopLeft},

      $(go.Panel, "Table",
        { padding: new go.Margin(8, 4), background: "#74a2f2"},
        $(go.Panel, "Vertical",
        $("SubGraphExpanderButton", {margin: new go.Margin(0,0,3,0)}),
          $(go.Panel, "Horizontal",
            $(go.TextBlock,
              { font: "bold 16pt sans-serif", margin: new go.Margin(0, 4), maxSize: new go.Size(350,50)},
              new go.Binding("text", "", data => `Type: ${data.kindName}`)
            ),
          ), 
          $(go.Panel, "Horizontal",
            
            $(go.TextBlock, 
              { font: "bold 16pt sans-serif", margin: new go.Margin(0, 4),maxSize: new go.Size(350,50)},
              new go.Binding("text", "", data => `Name: ${data.text}`)
            )
          )
        )
        
      ),
      $(go.Panel, "Auto", {background:"#fff",},
        $(go.Placeholder, { padding: 8, alignment: go.Spot.Left })
      ),
      {
        toolTip:  
          $("ToolTip",
            $(go.TextBlock, { margin: 4 },
              new go.Binding("text", "", data=>`Name: ${data.text} \n Type: ${data.kindName} \n Parent File: ${data.fileName}`))
          ) 
      }
    );

OK, so you say that on the client the Group.layout is not set. Binding the row, column and columnSpan properties on the client would therefore not be needed, since only a TableLayout would observe those property values on its member nodes.

Therefore it is incumbent on the group and node templates that there be bindings on the position (or location if it’s the same as the position) and desiredSize properties so that they can faithfully reproduce the layout that was done on the server. I see a “location” binding, but no “desiredSize” binding.

Yes, so the idea is to specify layouts on the back-end, performing the rendering part there, and on the front-end, I do not specify any layouts. I only bind location properties on the front-end, so that it is kind of like a blank canvas that is populated by nodes and groups whose locations are predetermined.
I am only binding the location properties. The desired size is not a property in the nodeData so I couldn’t figure out how to do a binding on that, so what I did is that I have specified the exact same desiredSizes in my templates on the front-end and on the back-end.

My point is that the TableLayout on the server will be setting the desiredSize on the member nodes. If you put in a TwoWay Binding on that property, the model data will be updated with the computed sizes, which the (OneWay) Binding on the client will be able to use.

That makes sense. I have added the following line below the location binding new gojs.Binding('desiredSize', 'desiredSize').makeTwoWay(gojs.Point.stringify), but the returned nodes do not have the desiredSize property to bind in the front-end template. What am I doing wrong?

You need to use matching conversion functions. You didn’t use go.Point.parse as the conversion function from source to target, to match the go.Point.stringify that you did specify as the back-conversion function from target to source.

Still no luck with new gojs.Binding('desiredSize', 'desiredSize',gojs.Point.parse).makeTwoWay(gojs.Point.stringify), Is the name of the property “desiredSize”? or do you think it is something different if we are to catch the size of the object?

The name of the data property really doesn’t matter, as long as it doesn’t start with “_” and doesn’t conflict with any other property names that you are using.

Check that the model data saved on the server have the property and that the values look reasonable.

How I interpret the binding is that when I write gojs.Binding('location', 'loc') , there is a property named “location” in modelData which I am binding to the nodeData under the name “loc”. Isn’t that how it works?

GoJS Data Binding -- Northwoods Software
Binding | GoJS API
GoJS Sized Groups -- Northwoods Software

Right, so I have read the articles and have implemented the binding as instructed, by it is not being reflected in the data that I get after rendering. Here is my template:

const tableLayout = $(gojs.Group, "Vertical",
        new gojs.Binding("location", "loc", gojs.Point.parse).makeTwoWay(gojs.Point.stringify),
        new gojs.Binding("desiredSize", "size", gojs.Size.parse).makeTwoWay(gojs.Size.stringify),
        { 
        alignment: gojs.Spot.TopLeft,  // affects TableLayout of containing group
        margin: 4,  // affects TableLayout of containing group
        layout: $(TableLayout),  // affects arrangement of member nodes
        minSize: new gojs.Size(100, 50),  // affects this panel (i.e. whole group)
        defaultStretch: gojs.GraphObject.Horizontal  // affects elements of this panel
        },
        new gojs.Binding("row", "row"),  // these all affect TableLayout of containing Group
        new gojs.Binding('column', 'col'),
        new gojs.Binding('columnSpan', 'columnSpan'),
        // the header
        $(gojs.Panel, "Table",
        { padding: new gojs.Margin(8, 4) },
        $(gojs.Panel, "Vertical",
            $("SubGraphExpanderButton", {margin: new gojs.Margin(0,0,3,0)}),
            $(gojs.Panel, "Horizontal",
                    $(gojs.TextBlock,
                    { font: "bold 16pt sans-serif", margin: new gojs.Margin(0, 4),desiredSize: new gojs.Size(250,50)},
                    new gojs.Binding("text", "", data => `Type: ${data.kindName}`)
                    ),
            ), 
            $(gojs.Panel, "Horizontal",
                $(gojs.TextBlock,
                { font: "bold 12pt sans-serif", margin: new gojs.Margin(0, 4) , desiredSize: new gojs.Size(250,50)},
                new gojs.Binding("text", "", data => `Table: ${data.key}`)
                )
            )
        )
        ),
        // the body holding the member nodes
        $(gojs.Panel, "Auto",
            $(gojs.Shape, { fill: null }),
            $(gojs.Placeholder, { padding: 8, alignment: gojs.Spot.TopLeft })
        )
    );

I do see that in your example, the binding is actually done on the shape element which is placed inside the group template. I don’t understand why that is so, as when you send the data back to the front-end, it is encapsulated inside the nodeData, where reference to internal shapes inside the template becomes irrelevant.

If you look at TableLayout.doLayout, which calls the internal TableLayout.arrangeTable, it finally does this to position and size each member node:

          child.moveTo(ar.x, ar.y);
          if (stretch !== go.GraphObject.None) {
            child.resizeObject.desiredSize = new go.Size(width, height);
          }

You haven’t set Part.resizeObjectName, and you shouldn’t, so the Part.resizeObject is just the whole Node or Group. That’s why the “desiredSize” Binding should be on the whole Node or Group.

I have added the binding on the whole group. I still don’t understand why I my nodes inside my modelData.nodeDataArray do not contain the “size” property which I can then sent to my client for a one-way binding

Your group template on the server has TwoWay Bindings of both “position” (or “location”) and of “desiredSize”, yes?

And the group template on the client has OneWay Bindings of those two properties, yes?

yes, that is correct

The following code has two models in it. One is what you’d send to the client to run without a TableLayout, and one is what you would layout on the server. The latter is what it loads by default, which means it is requiring the TableLayout.

Modify the templates to remove the Group.layout and the TwoWay Bindings and then hit the “Load” button to see what result you would get on the client.

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <!-- this model includes "loc" and "size" and "expanded" property values,
       and thus does not require Group.layout being a TableLayout nor any TwoWay Bindings -->
  <textarea id="mySavedModel" style="width:100%;height:250px">
{ "class": "GraphLinksModel",
  "nodeDataArray": [
{"key":1,"isGroup":true,"loc":"4 37.81052598953246"},
{"key":2,"isGroup":true,"group":1,"row":0,"col":0,"loc":"12 79.62105197906494","size":"220 81.61052598953248"},
{"key":3,"isGroup":true,"group":1,"row":0,"col":1,"loc":"240 79.62105197906494","size":"327 81.61052598953248"},
{"key":4,"isGroup":true,"group":1,"row":0,"col":2,"loc":"575 79.62105197906494","size":"100 50","expanded":false},
{"key":5,"isGroup":true,"group":1,"row":0,"col":3,"loc":"683 79.62105197906494","size":"115 81.61052598953248"},
{"key":6,"isGroup":true,"group":1,"row":0,"col":4,"loc":"806 79.62105197906493","size":"346 131.92105197906494"},
{"key":7,"isGroup":true,"group":6,"row":0,"col":0,"loc":"814 121.43157796859742","size":"221 81.61052598953248"},
{"key":8,"isGroup":true,"group":6,"row":0,"col":1,"loc":"1043 121.43157796859741","size":"100 50","expanded":false},
{"key":9,"isGroup":true,"group":1,"row":1,"col":0,"loc":"12 219.54210395812987","size":"220 81.61052598953248"},
{"key":10,"isGroup":true,"group":1,"row":1,"col":1,"colSpan":3,"loc":"240 219.54210395812987","size":"558 50","expanded":false},
{"group":2,"row":0,"col":0,"key":-11,"loc":"20 87.62105197906494"},
{"group":2,"row":0,"col":1,"key":-12,"loc":"125 87.62105197906494"},
{"group":3,"row":0,"col":0,"key":-13,"loc":"248 87.62105197906494"},
{"group":3,"row":0,"col":1,"key":-14,"loc":"354 87.62105197906494"},
{"group":3,"row":0,"col":2,"key":-15,"loc":"460 87.62105197906494"},
{"group":4,"row":0,"col":0,"key":-16,"loc":"583 87.62105197906494"},
{"group":4,"row":0,"col":1,"key":-17,"loc":"689 87.62105197906494"},
{"group":4,"row":0,"col":2,"key":-18,"loc":"795 87.62105197906494"},
{"group":4,"row":0,"col":3,"key":-19,"loc":"901 87.62105197906494"},
{"group":5,"row":0,"col":0,"key":-20,"loc":"691 87.62105197906494"},
{"group":7,"row":0,"col":0,"key":-21,"loc":"822 129.43157796859742"},
{"group":7,"row":0,"col":1,"key":-22,"loc":"928 129.43157796859742"},
{"group":8,"row":0,"col":0,"key":-23,"loc":"1051 129.43157796859742"},
{"group":8,"row":0,"col":1,"key":-24,"loc":"1157 129.43157796859742"},
{"group":8,"row":0,"col":2,"key":-25,"loc":"1263 129.43157796859742"},
{"group":9,"row":0,"col":0,"key":-26,"loc":"20 227.54210395812987"}
],
  "linkDataArray": []}
  </textarea>
  <div>
    <button id="myLoadButton">Load</button>
    <button id="mySaveButton">Save</button>
  </div>

  <script src="go.js"></script>
  <script src="../extensions/TableLayout.js"></script>
  <script id="code">
const $ = go.GraphObject.make;

const myDiagram =
  $(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 =
  $(go.Node, "Auto",
    {
      alignment: go.Spot.Left,
      margin: 4
    },
    new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
    new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
    new go.Binding("row", "row"),  // these all affect TableLayout of containing Group
    new go.Binding('column', 'col'),
    new go.Binding('columnSpan', 'colSpan'),
    $(go.Shape,
      { fill: "white" },
      new go.Binding("fill", "color")),
    $(go.TextBlock,
      { margin: 8 },
      new go.Binding("text", "key", k => `Node key: ${k}`))
  );

myDiagram.groupTemplate =
  $(go.Group, "Vertical",
    { 
      alignment: go.Spot.TopLeft,  // affects TableLayout of containing group
      stretch: go.GraphObject.Horizontal,
      margin: 4,  // affects TableLayout of containing group
      layout: $(TableLayout),  // affects arrangement of member nodes
      minSize: new go.Size(100, 50),  // affects this panel (i.e. whole group)
      defaultStretch: go.GraphObject.Horizontal  // affects elements of this panel
    },
    new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
    new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
    new go.Binding("row", "row"),  // these all affect TableLayout of containing Group
    new go.Binding("column", "col"),
    new go.Binding("columnSpan", "colSpan"),
    new go.Binding("isSubGraphExpanded", "expanded").makeTwoWay(),
    // the header
    $(go.Panel, "Table",
      { padding: new go.Margin(8, 4), background: "green" },
      new go.Binding("background", "color"),
      $(go.Panel, "Horizontal",
        $("SubGraphExpanderButton"),
        $(go.TextBlock,
          { font: "bold 12pt sans-serif", margin: new go.Margin(0, 4) },
          new go.Binding("text", "", data => `Table: ${data.key}`)
        )
      )
    ),
    // the body holding the member nodes
    $(go.Panel, "Auto",
      $(go.Shape, { fill: null }),
      $(go.Placeholder, { padding: 8, alignment: go.Spot.Left })
    )
  );

// this requires Group.layout being a TableLayout:
myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, isGroup: true },
  { key: 2, isGroup: true, group: 1, row: 0, col: 0 },
  { key: 3, isGroup: true, group: 1, row: 0, col: 1  },
  { key: 4, isGroup: true, group: 1, row: 0, col: 2  },
  { key: 5, isGroup: true, group: 1, row: 0, col: 3  },
  { key: 6, isGroup: true, group: 1, row: 0, col: 4  },
  { key: 7, isGroup: true, group: 6, row: 0, col: 0  },
  { key: 8, isGroup: true, group: 6, row: 0, col: 1  },
  { key: 9, isGroup: true, group: 1, row: 1, col: 0  },
  { key: 10, isGroup: true, group: 1, row: 1, col: 1, colSpan: 3  },
  { group: 2, row: 0, col: 0 },
  { group: 2, row: 0, col: 1 },
  { group: 3, row: 0, col: 0 },
  { group: 3, row: 0, col: 1 },
  { group: 3, row: 0, col: 2 },
  { group: 4, row: 0, col: 0 },
  { group: 4, row: 0, col: 1 },
  { group: 4, row: 0, col: 2 },
  { group: 4, row: 0, col: 3 },
  { group: 5, row: 0, col: 0 },
  { group: 7, row: 0, col: 0 },
  { group: 7, row: 0, col: 1 },
  { group: 8, row: 0, col: 0 },
  { group: 8, row: 0, col: 1 },
  { group: 8, row: 0, col: 2 },
  { group: 9, row: 0, col: 0 },
]);

// save a model to and load a model from Json text, displayed below the Diagram
function save() {
  var str = myDiagram.model.toJson();
  document.getElementById("mySavedModel").value = str;
}
document.getElementById("mySaveButton").addEventListener("click", save);

function load() {
  var str = document.getElementById("mySavedModel").value;
  myDiagram.model = go.Model.fromJson(str);
}
document.getElementById("myLoadButton").addEventListener("click", load);
  </script>
</body>
</html>

So it has taken me quite a while in making comparisons between your sample above and my templates to pinpoint the issue, and I have found that in the absence of stretch: go.GraphObject.Horizontal, property in my templates, the size binding is not working. Looks weird to me that this might happen, but it took a long while of doing trial and error to spot this. I am also noticing that the size property is still absent for my level1 table-layout groups (the nested table-layout groups that follow the same template, do contain the size property).

Coming back to my original issue at hand (the column span not working), now that I am able to send size for my nodes from the backend to the client, and even after binding that size on the front-end template, the groups with column span of 5 still appear to only stretch for a single column. I have noticed, however, that their size on the diagram is consistent with the size that was calculated and returned from the backend, as a property of the node object.

Yes, each Node or Part has the option of stretching horizontally and/or vertically to fill its cell, or range of spanned cells.

The sample I provided has a node with colSpan: 3, and that seems to stretch correctly. I also tried one with colSpan: 5, and that worked correctly too.

But maybe the column-spanning extends over a column that has no nodes in that column, causing the column to have zero width? So it looks like the group stretches horizontally over fewer columns than it should, even though it actually does because one (or more) columns are empty and thus do not occupy any space.