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


#1

I have a TABLE panel with two embedded TABLE panels. The embedded TABLE panels have TABLEROWs that are filled in with columns of field name and data type. I would like to get the columns to line up between the two TABLE panels. I am not set on using TABLE panels if there is another option, but I must have the border lines as identified. See the image below:

image

Thanks in advance for any help.


#2

Each “Table” Panel has its own notion of rows and columns.

So you could combine both tables into a single table. In your example one table would have four rows of two columns each.

If they have to be separate for some reason, then you could fix the width of the left column in both tables to have the same width. You could do that by adding this to each table:

    $(go.RowColumnDefinition, { column: 0, width: 100 })

You would need to decide on some suitable value for the width. That RowColumnDefinition.width property could be data bound too, if you don’t want to hard-code the fixed width.


#3

Thanks for the response. Yes, I need to have them be two separate tables so the top table has each row underlined. It looks like the second solution might work. Do you have any ideas on how I can data bind it based on the size of both tables?


#4

Could you please describe more precisely what you want?
Presumably you know about data binding.


#5

I have created two templates, one for the primary keys and one for the regular fields. I want the primary keys to be underlined with no border show between key attributes. Then I want a border to show between the primary key attributes and regular attributes. So I created a fieldTemplate to handle that. However, when I add both templates, the border doesn’t line up because each table is recognize separately. Here is my code.

// creates the template for primary key fields which is a panel with table rows
var pkTemplate =
$(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
	},
	
	// allows the user to select fields
	{
		click: function(e, item) {
		  
			// assume "transparent" means not "selected", for items
			var oldskips = item.diagram.skipsUndoManager;
			item.diagram.skipsUndoManager = true;
			item.background = item.background === "transparent" ? "dodgerblue" : "transparent";
			item.diagram.skipsUndoManager = oldskips;
		}
	},

	// creates the primary key field name
	$(go.TextBlock,
		{
			name: "FIELD",
			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 lowercase and underlines the field
		new go.Binding("text", "name", function(field) { return field.toLowerCase() }).makeTwoWay(),
		new go.Binding("isUnderline", "name", function(pk) { return true }),
	),
	
	// creates the primary key data type
	$(go.TextBlock,
		{
			name: "DATATYPE",
			column: 2,
			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()
	)
);

// creates the template for regular fields which is a panel with table rows
var fieldTemplate =
$(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
	},
	
	// allows the user to select fields
	{
		click: function(e, item) {
		  
			// assume "transparent" means not "selected", for items
			var oldskips = item.diagram.skipsUndoManager;
			item.diagram.skipsUndoManager = true;
			item.background = item.background === "transparent" ? "dodgerblue" : "transparent";
			item.diagram.skipsUndoManager = oldskips;
		}
	},

	// creates the regular field name
	$(go.TextBlock,
		{
			name: "FIELD",
			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 lowercase
		new go.Binding("text", "name", function(field) { return field.toLowerCase() }).makeTwoWay()
	),

	// creates the field data type
	$(go.TextBlock,
		{
			name: "DATATYPE",
			column: 2,
			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()
	)
);

// creates the node template of type auto
fileDiagram.nodeTemplate =
$(go.Node, "Auto",
	{
		selectionAdorned: true,
		layoutConditions: go.Part.LayoutStandard & ~go.Part.LayoutNodeSized,
		fromSpot: go.Spot.AllSides,
		toSpot: go.Spot.AllSides,
	},

	// binds the location of the node
	new go.Binding("location", "location").makeTwoWay(),

	// sets the fill and stroke of the outer node
	$(go.Shape, "Rectangle", {
		fill: "#eaeffd",
		stroke: "#3c599b",
		strokeWidth: 1.5
	}),

	// creates the node table
	$(go.Panel, "Table",
		$(go.Panel, "Auto",					

			// stretches the header of the table to fill the node
			{
				stretch: go.GraphObject.Horizontal
			},
			
			// sets the fill and stroke of the header of the node
			$(go.Shape,
				{
					fill: "#3c599b",
					stroke: null
				}
			),

			// sets the header text style and grabs the text from the binding
			$(go.TextBlock,
				{
					alignment: go.Spot.Center,
					margin: 3,
					stroke: "white",
					textAlign: "center",
					font: "bold 12pt sans-serif",
					cursor: "text",
					editable: true,
				},
				new go.Binding("text", "key", function(key) { return key.toUpperCase() }).makeTwoWay(),
			)
		),
		
		// separates each row of the table with a line
		{defaultRowSeparatorStroke: "#3c599b"},
		
		// sets the primary key row
		$(go.Panel, "Table",
			new go.Binding("itemArray", "pks"),
			{ 
				name: "PKS",
				row: 1,
				minSize: new go.Size(100, 10),
				stretch: go.GraphObject.Horizontal,
				defaultAlignment: go.Spot.Left,
				defaultColumnSeparatorStroke: "#3c599b",
				cursor: "pointer",
				itemTemplate: pkTemplate
			}
		),
		
		// sets the fields row
		$(go.Panel, "Table",
			new go.Binding("itemArray", "fields"),
			{ 
				name: "FIELDS",
				row: 2,
				minSize: new go.Size(100, 10),
				stretch: go.GraphObject.Horizontal,
				defaultAlignment: go.Spot.Left,
				defaultColumnSeparatorStroke: "#3c599b",
				cursor: "pointer",
				itemTemplate: fieldTemplate
			}
		)
	)
);

#6

Did you try what I first suggested? Add this line to each “Table” Panel:


#7

I am trying but can’t figure it out. I will let you know what I find out. If you have any ideas as I work on this, I would appreciate it. I should add that this just added a column to the existing structure.


#8

I didn’t realize your columns were 1-based rather than 0-based.


#9

Thanks. I used to have zero-based but got rid of a column. Sorry for the confusion. I have added what you said, but it seems to have shifted everything 300 (I used 300 instead for testing) but the borders stayed the same:
image


#10

And you added that RowColumnDefinition to both “Table” Panels?


#11

Sweet! It worked. Thank you!
Last question, how would you suggest binding the data so the width dynamically expands based on each table? Or in other words, that whether the data in the first table is wider than the second or the other way around, I want the column width to be the width of the largest data point.


#12

There’s no easy way to do that.

But you could try what I first suggested: just have a single “Table” Panel and a single data Array of field descriptors. The Panel.itemTemplateMap would have both of your current templates in them, and you would need to specify a way for it to choose which template to use for each field. https://gojs.net/latest/intro/templateMaps.html#ExampleOfTableHeaderShowingItemData


#13

Okay, thanks for the ideas. I will see what I can figure out.


#14

So I have implemented the itemTemplateMap per your suggestion and it seems to be working great! I am wondering if it is possible to get all items in a template map to show up before any other items? Also, can you put borders between template maps?


#15

Sure, just order the items in the Array the way that you want.

If your Panel is of type “Table”, then you can specify separators: https://gojs.net/latest/intro/tablePanels.html#SeparatorsAndRowColumnPadding, either for all rows or for an individual row.


#16

How do you sort the JSON array within go.js based on fields with a type of PK?

Here is another attempt I have at separating the pks from the regular fields; however, I want the line separator to dynamically appear depending on how many pk fields there are. The current implementation adds a line separator after line 2. Is there a way to dynamically separate the two?

// creates the node table
$(go.Panel, "Table",
	$(go.Panel, "Auto",					

		// stretches the header of the table to fill the node
		{
			stretch: go.GraphObject.Horizontal
		},
		
		// sets the fill and stroke of the header of the node
		$(go.Shape,
			{
				cursor: "move",
				fill: "#3c599b",
				stroke: null
			}
		),

		// $(go.RowColumnDefinition, { row: 1, separatorStroke: "#3c599b" }),
		// sets the header text style and grabs the text from the binding
		$(go.TextBlock,
			{
				name: "RELATION",
				row: 0,
				alignment: go.Spot.Center,
				margin: 3,
				stroke: "white",
				textAlign: "center",
				font: "bold 12pt sans-serif",
				cursor: "text",
				editable: true
			},
			new go.Binding("text", "key", function(key) { return key.toUpperCase() }).makeTwoWay()
		)
	),

	// sets the fields row
	$(go.Panel, "Table",
		new go.Binding("pkArray", "pks"),
		{ 
			name: "PK",
			row: 1,
			minSize: new go.Size(100, 10),
			stretch: go.GraphObject.Horizontal,
			defaultAlignment: go.Spot.Left,
			defaultColumnSeparatorStroke: "#3c599b",
			cursor: "pointer",
			itemTemplate: pkTemplate
		},
		$(go.RowColumnDefinition, { row: 2, separatorStroke: "#3c599b" }),
		new go.Binding("itemArray", "fields"),
		{ 
			name: "FIELDS",
			row: 3,
			minSize: new go.Size(100, 10),
			stretch: go.GraphObject.Horizontal,
			defaultAlignment: go.Spot.Left,
			defaultColumnSeparatorStroke: "#3c599b",
			cursor: "pointer",
			itemTemplate: fieldTemplate
		}
	)
)

#17

Well, the simplest would be to use a binding conversion function that sorted the source Array (fields) according to whatever criteria you wish.

Oh, and that conversion function could also be responsible for identifying particular items that want to be displayed differently – either setting data.category on the item data or just setting regular data-bound properties on the item data.


#18

I am really struggling to understand your last comment. I have searched for the past 20 minutes with no great enlightenment. Do you have an example where sorting of an itemArray occurs before use?

Also, I am setting data.category to “PK” for PKs and “” for all other fields. Now, I just need to figure out how to put a dynamic divider between template parts of the templateMap.


#19
new go.Binding("itemArray", "fields", function(a) { a.sort(. . .); return a; })

#20

Thank you, I think I am getting closer. I have two issues now. First, the sorted array doesn’t bring the ports with it so it ruins the linking between field to field. Second, can I bind the row number of go.RowColumnDefinition? It doesn’t appear I can. I currently have the following:

// sets the fields row
$(go.Panel, "Table",
	new go.Binding("itemArray", "fields", function(fields) { 
		function countInArray(array, key, value) {
			var count = 0;
			for (var i = 0; i < array.length; i++) {
				if (array[i][key] === value) {
					count++;
				}
			}
			return count;
		}
		rowNum = countInArray(fields, "isUnderline", true);
		fields = fields.sort(function (a, b) { return b.isUnderline - a.isUnderline; } );
		return fields;
	}).makeTwoWay(),
	{
		name: "FIELDS",
		row: 1, 
		minSize: new go.Size(100, 10),
		stretch: go.GraphObject.Horizontal,
		defaultAlignment: go.Spot.Left,
		defaultColumnSeparatorStroke: "#3c599b",
		cursor: "pointer",
		itemTemplateMap: itemTemplateMap
	}, 
	$(go.RowColumnDefinition, { row: 0, separatorStroke: "#3c599b" },
		new go.Binding("row", "row", function() {return rowNum;})
	)
)

Ultimately, this is what I want to end up with:
image

Essentially, I want all the underlined fields to show up at the top with a line after the last underlined field