How do you line columns up with two tables stacked on each other?

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

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

    myDiagram.nodeTemplate =
      $(go.Node, "Auto",
        $(go.Shape,
          { fill: "white", strokeWidth: 2, portId: "" },
          new go.Binding("stroke", "color")),
        $(go.Panel, "Table",
          new go.Binding("itemArray", "items", function(arr, table) {
            arr.sort(function(a, b) {
              if (a.isKey && !b.isKey) return -1;
              if (!a.isKey && b.isKey) return 1;
              return 0; });
            var keys = 0;
            arr.forEach(function(item) { if (item.isKey) keys++; });
            table.getRowDefinition(keys).separatorStroke = table.part.data.color;
            return arr;
          }),
          $(go.RowColumnDefinition, { column: 1 }, new go.Binding("separatorStroke", "color")),
          {
            padding: 2,
            defaultSeparatorPadding: 2,
            defaultAlignment: go.Spot.Left,
            itemTemplate:
              $(go.Panel, "TableRow",
                $(go.TextBlock, new go.Binding("text", "name"), { column: 0 },
                  new go.Binding("isUnderline", "isKey"),
                  new go.Binding("font", "secondary", function(i) { return i ? "italic 10pt sans-serif" : "10pt sans-serif"; })),
                $(go.TextBlock, new go.Binding("text", "type"), { column: 1 })
              )
          }
        )
      );

    myDiagram.linkTemplate =
      $(go.Link,
        $(go.Shape),
        $(go.Shape, { toArrow: "OpenTriangle" })
      );

    myDiagram.model = new go.GraphLinksModel([
      {
        key: 1, text: "Alpha", color: "blue",
        items: [
          { name: "activity_id", type: "INTEGER", isKey: true },
          { name: "section_id", type: "INTEGER", isKey: true, secondary: true },
          { name: "group_id", type: "INTEGER", isKey: true, secondary: true },
          { name: "date_time", type: "DATETIME" },
          { name: "type", type: "VARCHAR" },
          { name: "details", type: "VARCHAR" },
          { name: "position", type: "VARCHAR" },
          { name: "link", type: "VARCHAR" },
          { name: "ip_address", type: "VARCHAR" },
          { name: "viewed", type: "CHAR" }
        ]
      },
      { key: 2, text: "Beta", color: "orange",
        items: [
          { name: "group_no", type: "CHAR" },
          { name: "group_name", type: "VARCHAR" },
          { name: "schema_name", type: "VARCHAR" },
          { name: "group_id", type: "INTEGER", isKey: true },
          { name: "diagram", type: "CHAR" },
          { name: "section_id", type: "INTEGER", secondary: true }
        ]
      }
    ], [
      { from: 1, to: 2 }
    ]);
  }

produces:

Fantastic! This is exactly what I was looking for!

I promise this is my last question regarding this topic. Is it possible to efficiently rebuild the node when a user right-clicks and changes the field to isKey? Currently, I am reloading the model which doesn’t support undo/redo. I have also read about using Panel.rebuildItemElements() but this apparently is inefficient and also doesn’t support undo/redo. Thoughts?

First, whenever you want to change some data property value, call Model.setDataProperty (a.k.a. Model.set for short). That will support undo/redo.

Second, is the only remaining problem that the order of the items needs to change, because of the dependence on the data.isKey and possibly other item data properties such as the data.name? I think it’s OK to call Panel.rebuildItemElements once you’ve made the calls to Model.set. I haven’t tested this, but I think undo and redo will work, because the whole state of the node depends only on the data and the data’s state is tracked by the UndoManager via the calls to Model.set.

This is fun. This time I’m including the whole page, because I’ve defined an HTML button to modify the “isKey” property of the second item of the first node.

<!DOCTYPE html>
<html>
<head>
<title>Minimal GoJS Sample</title>
<!-- Copyright 1998-2019 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="go.js"></script>
<script id="code">
  function init() {
    var $ = go.GraphObject.make;

    myDiagram =
      $(go.Diagram, "myDiagramDiv",
          {
            "undoManager.isEnabled": true
          });

    myDiagram.nodeTemplate =
      $(go.Node, "Auto",
        $(go.Shape,
          { fill: "white", strokeWidth: 2, portId: "" },
          new go.Binding("stroke", "color")),
        $(go.Panel, "Table",
          new go.Binding("itemArray", "items", function(arr, table) {
            arr.sort(function(a, b) {
              if (a.isKey && !b.isKey) return -1;
              if (!a.isKey && b.isKey) return 1;
              return 0; });
            var keys = 0;
            arr.forEach(function(item) { if (item.isKey) keys++; });
            for (var i = 0; i < arr.length; i++) table.getRowDefinition(i).separatorStroke = null;
            table.getRowDefinition(keys).separatorStroke = table.part.data.color;
            return arr;
          }),
          $(go.RowColumnDefinition, { column: 1 }, new go.Binding("separatorStroke", "color")),
          {
            name: "TABLE",
            padding: 2,
            defaultSeparatorPadding: 2,
            defaultAlignment: go.Spot.Left,
            itemTemplate:
              $(go.Panel, "TableRow",
                { fromSpot: go.Spot.LeftRightSides, toSpot: go.Spot.LeftRightSides },
                new go.Binding("portId", "name"),
                $("Button",
                  { 
                    width: 12, height: 12,
                    click: function(e, button) {
                      e.diagram.commit(function(diag) {
                        var pan = button.panel;
                        var data = pan.data;
                        var node = button.part;
                        diag.model.set(data, "isKey", !data.isKey);
                        diag.model.updateTargetBindings(node.data, "items");
                      }, "toggle isKey");
                    }
                  }),
                $(go.TextBlock, new go.Binding("text", "name"), { column: 1 },
                  new go.Binding("isUnderline", "isKey"),
                  new go.Binding("font", "secondary", function(i) { return i ? "italic 10pt sans-serif" : "10pt sans-serif"; })),
                $(go.TextBlock, new go.Binding("text", "type"), { column: 2 })
              )
          }
        )
      );

    myDiagram.linkTemplate =
      $(go.Link,
        $(go.Shape),
        $(go.Shape, { toArrow: "OpenTriangle" })
      );

    myDiagram.model = $(go.GraphLinksModel,
      {
        linkFromPortIdProperty: "fp",
        linkToPortIdProperty: "tp",
        nodeDataArray: [
        {
          key: 1, text: "Alpha", color: "blue",
          items: [
            { name: "activity_id", type: "INTEGER", isKey: true },
            { name: "section_id", type: "INTEGER", isKey: true, secondary: true },
            { name: "group_id", type: "INTEGER", isKey: true, secondary: true },
            { name: "date_time", type: "DATETIME" },
            { name: "type", type: "VARCHAR" },
            { name: "details", type: "VARCHAR" },
            { name: "position", type: "VARCHAR" },
            { name: "link", type: "VARCHAR" },
            { name: "ip_address", type: "VARCHAR" },
            { name: "viewed", type: "CHAR" }
          ]
        },
        { key: 2, text: "Beta", color: "orange",
          items: [
            { name: "group_no", type: "CHAR" },
            { name: "group_name", type: "VARCHAR" },
            { name: "schema_name", type: "VARCHAR" },
            { name: "group_id", type: "INTEGER", isKey: true },
            { name: "diagram", type: "CHAR" },
            { name: "section_id", type: "INTEGER", secondary: true }
          ]
        }
      ], 
      linkDataArray: [
        { from: 1, fp: "section_id", to: 2, tp: "section_id" }
      ]
    });
  }

  function toggle() {
    myDiagram.commit(function(diag) {
      var node = diag.nodes.first();
      diag.model.set(node.data.items[1], "isKey", !node.data.items[1].isKey);
      diag.model.updateTargetBindings(node.data, "items");
    }, "toggled some state");
  }
</script>
</head>
<body onload="init()">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
  <button onclick="toggle()">Toggle State</button>
</body>
</html>

So it turns out that calling Panel.rebuildItemElements is not sufficient – because it doesn’t cause the Binding conversion function to be called again, to re-sort the Array with the new item properties.

Instead one needs to call Model.updateTargetBindings.

I also improved that conversion function to clear out all of the RowColumnDefinition.separatorStroke values, since the row that needs the horizontal separator changes.

Then I added a button in the first column that toggles the “isKey” property on the item data for that row. The click event handler does the same thing as that HTML button click handler, except it gets the reference to the item data from the Panel.

Then I remembered what you said about having links connecting to the individual rows. So I added a Binding on GraphObject.portId to the data.name of the item, causing each row to be a port. (This does assume that the names are unique within the node.) I also had to set GraphLinksModel.linkFromPortIdProperty and linkToPortIdProperty to the property names used on the link data to remember the port identifiers.

And I set GraphObject.fromSpot and toSpot on the row, so that ports can come out of and go into the port on the closest side.

Here’s what it looks like after I clicked on the “section_id” row Button of the left node.
image

Thank you! What happens for you when you check something that wasn’t a primary key near the bottom? I get the line shifting but the fields don’t rearrange. It looks like your code worked, but I can’t seem to get it to work for me. For example, in the image below, I changed approved to have the primary key and the line shifted but the item was not shifted:
image

Here is my code, the function toggleKey is called from the context menu:

// function to toggle selected fields as primary keys
function toggleKey() {
	
	// commits changes for undo
	fileDiagram.commit(function() {

		// changes all selected fields to primary keys
		var fields = findAllSelectedFields();
		fields.forEach(function(field) {			
			var data = field.data;
			var node = field.part;
			fileDiagram.model.set(data, "isUnderline", !data.isUnderline);
			fileDiagram.model.updateTargetBindings(node.data, "fields");
		});
	});
}

// finds all selected fields
function findAllSelectedFields() {
	var fields = [];
	for (var nit = fileDiagram.nodes; nit.next(); ) {
		var node = nit.value;
		var table = node.findObject("FIELDS");
		if (table) {
			for (var iit = table.elements; iit.next(); ) {
				var fieldPanel = iit.value;
				if (fieldPanel.background !== "transparent") fields.push(fieldPanel);
			}
		}
	}
	return fields;
}

// creates the template fields which is a panel with table rows
var itemTemplate = 
$(go.Panel, "TableRow",
							
	// sets the port for binding between two node
	new go.Binding("portId", "name"),
	{
		// enables selection of port by mouse
		background: "transparent",
		
		// allows links can go from and to both sides
		fromSpot: go.Spot.LeftRightSides,  
		toSpot: go.Spot.LeftRightSides,
		
		// allows drawing links from or to this port
		fromLinkable: true,
		toLinkable: true,
		
		// allows drawing links within the same node
		fromLinkableSelfNode: true,
		toLinkableSelfNode: true,

		// creates the right click menu for interacting with the primary key field
		contextMenu: makeButtons("pk"),
		
		// detects right-mouse click events
		click: function(e, pk) { selectField(pk); },
		"contextClick": function(e, pk) { selectField(pk); }
	},
	
	// creates the field name
	$(go.TextBlock,
		{
			name: "FIELD",
			column: 0,
			margin: new go.Margin(5, 5, 4, 5),
			stretch: go.GraphObject.Horizontal,
			font: "11pt sans-serif",
			wrap: go.TextBlock.None,
			cursor: "text",
			isUnderline: false,
			editable: true
		},
		
		// sets the text to lowercase and underlines the field
		new go.Binding("text", "name", function(field) { return field.toLowerCase(); }).makeTwoWay(),
		new go.Binding("isUnderline", "isUnderline").makeTwoWay(),
		new go.Binding("font", "secondary", function(i) { return i ? "italic 10pt sans-serif" : "10pt sans-serif"; })
	),
	
	// creates the data type
	$(go.TextBlock,
		{
			name: "DATATYPE",
			column: 1,
			margin: new go.Margin(5, 5, 4, 5),
			stretch: go.GraphObject.Horizontal,
			font: "11pt sans-serif",
			wrap: go.TextBlock.None,
			cursor: "text",
			editable: true
		},

		// sets the text to uppercase
		new go.Binding("text", "dataType", function(type) { return type.toUpperCase(); }).makeTwoWay()
	)
);

// sets the fields row
$(go.Panel, "Table",

	// gets the fields sorting the primary key fields to the top
	new go.Binding("itemArray", "fields", function(fields, table) { 
		var keys = 0;
		fields.sort(function (a, b) { return b.isUnderline - a.isUnderline; } );
		fields.forEach(function(field) { if (field.isUnderline) keys++; });
		for (var i = 0; i < fields.length; i++) table.getRowDefinition(i).separatorStroke = null;
		table.getRowDefinition(keys).separatorStroke = "#3c599b";
		return fields;
	}),
	{
		name: "FIELDS",
		row: 1, 
		minSize: new go.Size(100, 10),
		stretch: go.GraphObject.Horizontal,
		defaultAlignment: go.Spot.Left,
		defaultColumnSeparatorStroke: "#3c599b",
		cursor: "pointer",
		itemTemplate: itemTemplate
	}
)

In fact, I just implemented your code and I ended up with the same issue I am experiencing, section_id is not isKey but it is still grouped above while group_id is isKey and grouped below:

Interestingly, the console indicates that the array is in the right order but doesn’t change the order at all:


This log output occurs before this line:

fileDiagram.model.updateTargetBindings(node.data, "fields");
fields.forEach(function(field) {	
	. . .
	fileDiagram.model.updateTargetBindings(node.data, "fields");
})

You are repeatedly re-creating all of the rows each time you modify a field.

This is because the user can have multiple selected fields and simultaneous update them all to be a key. So this runs for all the fields that are updated. So the fields represented here are not the fields of the node but the selected fields across one or more nodes.

I guess the problem is also that even with your code only on a separate page, I am still having the same issues.

Problem solved. Interestingly, go-debug.js works but go.js doesn’t. Thank you, thank you, thank you for all your help!

Problem is that the watermark shows up in the debug version. Why is there a difference between the two versions or than for debugging?

UPDATE: So I realized that I had an older version. I just updated go.js to the latest version and it now works; however, my product key no longer works and the watermark is displayed. How do I fix this?

UPDATE2: It appears with the new version, I need to get a new key: GoJS Deployment -- Northwoods Software

On another note, how do you check when a link is deleted and then change the secondary key to false (not italics) if this is the case? I have tried SelectionDeleting and SelectionDeleted for the addDiagramListener. I have also tried the AddChangedListener to no avail. I have the following code for adding the italics but can’t find any listener for when a link is deleted:

// checks if a link is drawn and adds italics where the link is connected to the from port
fileDiagram.addDiagramListener("LinkDrawn", function(e) {
	var node = e.subject.fromNode;
	fileDiagram.commit(function() {
		node.findLinksOutOf().each(function(link) { 
			link.fromPort.data.secondary = true;
			fileDiagram.model.updateTargetBindings(node.data, "fields");
		});
	});
});

// checks if a link is drawn and adds italics where the link is connected to the from port
fileDiagram.addDiagramListener("LinkRelinked", function(e) {
	var node = e.subject.fromNode;
	fileDiagram.commit(function() {

		// removes italics from all other fields
		node.data.fields.forEach(function(field) { 
			field.secondary = false;
		});

		//adds italics where the link is connected to the from port
		node.findLinksOutOf().each(function(link) { 
			link.fromPort.data.secondary = true;
			fileDiagram.model.updateTargetBindings(node.data, "fields");
		});
	});
});

UPDATE: I got this working but I feel there is a much more efficient way:

// removes the italics from ports where links that are deleted
fileDiagram.addDiagramListener("SelectionDeleted", function(e) {
	fileDiagram.commit(function() {

		// loops through each node
		fileDiagram.nodes.each(function(node) {

			// removes italics from all other fields
			node.data.fields.forEach(function(field) {
				field.secondary = false;
			});

			//adds italics where the link is connected to the from port
			node.findLinksOutOf().each(function(link) { 
				link.fromPort.data.secondary = true;
				fileDiagram.model.updateTargetBindings(node.data, "fields");
			});
		});
	});
});

Maybe you could use the Node.linkConnected and linkDisconnected event handlers: Node | GoJS API

Thank you. That seems to work.