Scrolling Table

Hi

I’m working on a ScrollingTable, but when I replicate your example:

But I’m getting this error:

“Error: GraphObject.make requires a class function or GoJS class name or name of an object builder, not: ScrollingTable”

I already have implemented the Selectable Fields example, and i want to insert scroll, but its being difficult.

Have you included ScrollingTable.js in your file? That’s where the builder for ScrollingTable is located.

Yes. It is included. Maybe the problem is that the project is Angular?

Im not including the file goSamples.js, could be the problem related to this?

EDIT 1:

We are importing the gojs elements that we need in this way (Angular 5):

import { Panel, Binding, Spot, Diagram, GraphObject, TextBlock, Placeholder,
Shape, Adornment, Point, Margin, Link, GraphLinksModel, Size, Node } from 'gojs'

But when we try to import ScrollingTable or another extension like ColumnResizing, is not working.
We have try this:

import * as scrollingTable from 'gojs/extensions/scrollingTable' 

But the same error appears.

No, please do not use our samples framework in your app!

You need to import ScrollingTable from .../extensionsTS/ScrollingTable, not from .../extensions/... The JS files in extensions are just for loading in a script tag.

Thanks! I finally got it. By the import way it was impossible, same error occurs. We achieve it by injecting the ScrollingTable.js before calling our code.

Now we have ScrollingTable, but we have lost many styles that we had without the scroll. Next, we attach two screenshots of the before and the after.

Without scrolling:

With scrolling

This is our code:

 const $ = GraphObject.make;  // for conciseness in defining templates
// This is the function with the ScrollingTable.js code
this.loadScrollingTable();
this.diagram =
  $(Diagram, 'relationsDiagram',
    {
      initialDocumentSpot: Spot.Center,
      initialViewportSpot: Spot.Center,
      initialAutoScale: Diagram.Uniform,
      validCycle: Diagram.CycleNotDirected,  // don't allow loops
      'undoManager.isEnabled': true

    });
// this.diagram.toolManager.mouseDownTools.add(new ColumnResizingTool());
const fieldTemplate =
  $(Panel, 'TableRow',  // this Panel is a row in the containing Table
    new Binding('portId', 'name'),  // this Panel is a "port"
    {
      background: 'transparent',  // so this port's background can be picked by the mouse
      // allow drawing links from or to this port:
      fromLinkable: true, toLinkable: true
    },
    $(TextBlock,
      {
        margin: new Margin(0, 2), column: 0, font: 'bold 14px Roboto',
        overflow: TextBlock.OverflowEllipsis,
        wrap: TextBlock.None,
        stretch: GraphObject.Horizontal,
        // and disallow drawing links from or to this text:
        fromLinkable: true, toLinkable: false
      },
      new Binding('text', 'name')),
    $(TextBlock,
      {
        margin: new Margin(0, 2),
        column: 1,
        overflow: TextBlock.OverflowEllipsis,
        stretch: GraphObject.Horizontal,
        font: 'bold 14px Roboto'
      },
      new Binding('text', 'columnType')),
  );

this.diagram.nodeTemplate =
$(Node, "Vertical",
  {
    selectionObjectName: "SCROLLER",
    resizable: true, resizeObjectName: "SCROLLER",
    portSpreading: Node.SpreadingNone
  },
  new Binding("location").makeTwoWay(),
  $(TextBlock,
    { font: "bold 14px sans-serif" },
    new Binding("text", "key")),
  $(Panel, "Auto",
    $(Shape, { fill: "white" }),
    $("ScrollingTable",
      {
        name: "SCROLLER",
        desiredSize: new Size(NaN, 60),  // fixed width
        stretch: GraphObject.Fill        // but stretches vertically
      },
      this.makeSubBinding('TABLE.itemArray', 'columnDefinitions'),
      new Binding("TABLE.column", "left", function(left) { return left ? 2 : 0; }),
      new Binding("desiredSize", "size").makeTwoWay(),
      {
        "TABLE.itemTemplate":
          $(Panel, "TableRow",
            {
              defaultStretch: GraphObject.Horizontal,
              fromSpot: Spot.LeftRightSides, toSpot: Spot.LeftRightSides,
              fromLinkable: true, toLinkable: true
            },
            new Binding("portId", "name"),
            $(TextBlock, { column: 0 }, new Binding("text", "name")),
            $(TextBlock, { column: 1 }, new Binding("text", "columnType"))
          ),
        "TABLE.defaultColumnSeparatorStroke": "grey",
        "TABLE.defaultColumnSeparatorStrokeWidth": 0.5,
        "TABLE.defaultRowSeparatorStroke": "grey",
        "TABLE.defaultRowSeparatorStrokeWidth": 0.5,
        "TABLE.defaultSeparatorPadding": new Margin(1, 3, 0, 3)
      }
    )
  )
);


this.diagram.linkTemplate =
  $(Link,
    {
      reshapable: false, resegmentable: false,
      relinkableFrom: true, relinkableTo: true,
      adjusting: Link.Stretch,
      fromSpot: Spot.LeftRightSides,
      toSpot: Spot.LeftRightSides
    },
    {
      selectionAdornmentTemplate:
        $(Adornment,
          $(Shape,
            { isPanelMain: true, stroke: '#009fe3', strokeWidth: 2 }),
          $(Shape,
            { toArrow: 'Standard', fill: '#009fe3', stroke: null, scale: 2.5 })
        )  // end Adornment
    },
    $(Shape, { stroke: '#646363', strokeWidth: 1.5 }),
    $(Shape, { toArrow: 'Standard', stroke: null },
    )
  );


this.diagram.model =
  $(GraphLinksModel,
    {
      archetypeNodeData: {},
      linkFromPortIdProperty: 'sourceColumn',
      linkFromKeyProperty: 'source',
      linkToPortIdProperty: 'destinationColumn',
      linkToKeyProperty: 'destination',
      nodeDataArray: this.visualOrigins,
      linkDataArray: this.relations
    });


const tempfromnode =
  $(Node,
    { layerName: 'Tool' },
    $(Shape, 'Rectangle',
      {
        stroke: '#009fe3', strokeWidth: 3, fill: null,
        portId: '', width: 1, height: 1
      })
  );
const temptonode =
  $(Node,
    { layerName: 'Tool' },
    $(Shape, 'Rectangle',
      {
        stroke: '#009fe3', strokeWidth: 3, fill: null,
        portId: '', width: 1, height: 1
      })
  );

this.diagram.toolManager.linkingTool.temporaryFromNode = tempfromnode;
this.diagram.toolManager.linkingTool.temporaryFromPort = tempfromnode.port;
this.diagram.toolManager.linkingTool.temporaryToNode = temptonode;
this.diagram.toolManager.linkingTool.temporaryToPort = temptonode.port;
this.diagram.toolManager.relinkingTool.temporaryFromNode = tempfromnode;
this.diagram.toolManager.relinkingTool.temporaryFromPort = tempfromnode.port;
this.diagram.toolManager.relinkingTool.temporaryToNode = temptonode;
this.diagram.toolManager.relinkingTool.temporaryToPort = temptonode.port;

this.diagram.toolManager.linkingTool.temporaryLink =
  $(Link,
    { layerName: 'Tool' },
    $(Shape,
      { stroke: '#646363', strokeWidth: 2 })
  );

// SUbbinding function
makeSubBinding(targetname, sourcename) {
    const bind = new Binding(targetname, 'origin');
    bind.converter = function (details, target) {
    const value = details[sourcename];
    if (value !== undefined) {
       return value;
    }
   };
   return bind;
  }

The loadScrollingTable() is exactly the same as ScrollingTable.js

The problems we have are this:

  1. Our blue header and the white background (now is white) of the table get lost. I cannot find where to change it.
  2. We notice and strange behavior that we cannot resolve. We start with the contracted table:

Next, we expand it:

We click at the down scroll until the last record of the table:

We click at the up scroll until the scroll bar dissapear:

We contract the table again:

At this point, the scroll bar never returns and the user could not scroll through the table.

In summary, we want our initial styles table with the scrolling mode.

Thanks in advance, and sorry for the extension.

It should have worked if you had just done:

import "ScrollingTable";

in your TypeScript file, perhaps with a different path, in order to load “extensionsTS/ScrollingTable”.

Or you could require “ScrollingTable”.

I don’t know what your loadScrollingTable() does, but it might not work because loading JavaScript could be happening asynchronously, which means that the following code might not be working correctly.

I have try multiple combinations of the import with no sucess.

The loadScrollingTable() only has all the code of your extension, and I call it before configure the table. I have test your example at my environment (using the loadScrollingTable instead of the import) and the scroll is working ok. I’m missing some configuration option…

Thanks.

I still don’t know what you are doing in loadScrollingTable. Is that code also doing an import of the individual classes from “gojs”?

Because if it is not, then it is using the “go” namespace, which means there are two separate definitions for Panel, GraphObject, Shape, et al. And those definitions won’t work interchangeably.

It might not even be enough to make sure that you are loading the code from the extensionsTS directory – I haven’t tried it that way.

I understand you. I’m also importing the individual classes from gojs, but not the extension (the extension is in the method).

This is the simplified component, I think you will gett a better idea:

import {
Panel, Binding, Spot, Diagram, GraphObject, TextBlock, Placeholder,
Shape, Adornment, Point, Margin, Link, GraphLinksModel, Size, Node, RowColumnDefinition    
} from 'gojs'

@Component({
selector: 'dashboard-creation-relations',
templateUrl: './creation-relations.component.html',
styleUrls: ['./creation-relations.component.scss']
})
export class DashboardCreationRelationsComponent implements OnInit, OnDestroy {
   diagram: any;
constructor() {

}


ngOnInit() {
    this.dashboardCreationService.setSelectedStep(this.stepId);
    this.visualOrigins = [];
    // this.origins = this.dashboardCreationService.dashboardCreation.origins;
    this.visualOrigins = this.dashboardCreationService.dashboardCreation.originsVisual;
    const $ = GraphObject.make;  // for conciseness in defining templates
    this.loadScrollingTable();
    const $ = GraphObject.make;  // for conciseness in defining templates
    // This is the function with the ScrollingTable.js code
    this.loadScrollingTable();
    this.diagram =
        $(Diagram, 'relationsDiagram',
            {
                initialDocumentSpot: Spot.Center,
                initialViewportSpot: Spot.Center,
                initialAutoScale: Diagram.Uniform,
                validCycle: Diagram.CycleNotDirected,  // don't allow loops
                'undoManager.isEnabled': true

            });
    // this.diagram.toolManager.mouseDownTools.add(new ColumnResizingTool());
    const fieldTemplate =
        $(Panel, 'TableRow',  // this Panel is a row in the containing Table
            new Binding('portId', 'name'),  // this Panel is a "port"
            {
                background: 'transparent',  // so this port's background can be picked by the mouse
                // allow drawing links from or to this port:
                fromLinkable: true, toLinkable: true
            },
            $(TextBlock,
                {
                    margin: new Margin(0, 2), column: 0, font: 'bold 14px Roboto',
                    overflow: TextBlock.OverflowEllipsis,
                    wrap: TextBlock.None,
                    stretch: GraphObject.Horizontal,
                    // and disallow drawing links from or to this text:
                    fromLinkable: true, toLinkable: false
                },
                new Binding('text', 'name')),
            $(TextBlock,
                {
                    margin: new Margin(0, 2),
                    column: 1,
                    overflow: TextBlock.OverflowEllipsis,
                    stretch: GraphObject.Horizontal,
                    font: 'bold 14px Roboto'
                },
                new Binding('text', 'columnType')),
        );

    this.diagram.nodeTemplate =
        $(Node, "Vertical",
            {
                selectionObjectName: "SCROLLER",
                resizable: true, resizeObjectName: "SCROLLER",
                portSpreading: Node.SpreadingNone
            },
            new Binding("location").makeTwoWay(),
            $(TextBlock,
                { font: "bold 14px sans-serif" },
                new Binding("text", "key")),
            $(Panel, "Auto",
                $(Shape, { fill: "white" }),
                $("ScrollingTable",
                    {
                        name: "SCROLLER",
                        desiredSize: new Size(NaN, 60),  // fixed width
                        stretch: GraphObject.Fill        // but stretches vertically
                    },
                    this.makeSubBinding('TABLE.itemArray', 'columnDefinitions'),
                    new Binding("TABLE.column", "left", function (left) { return left ? 2 : 0; }),
                    new Binding("desiredSize", "size").makeTwoWay(),
                    {
                        "TABLE.itemTemplate":
                            $(Panel, "TableRow",
                                {
                                    defaultStretch: GraphObject.Horizontal,
                                    fromSpot: Spot.LeftRightSides, toSpot: Spot.LeftRightSides,
                                    fromLinkable: true, toLinkable: true
                                },
                                new Binding("portId", "name"),
                                $(TextBlock, { column: 0 }, new Binding("text", "name")),
                                $(TextBlock, { column: 1 }, new Binding("text", "columnType"))
                            ),
                        "TABLE.defaultColumnSeparatorStroke": "grey",
                        "TABLE.defaultColumnSeparatorStrokeWidth": 0.5,
                        "TABLE.defaultRowSeparatorStroke": "grey",
                        "TABLE.defaultRowSeparatorStrokeWidth": 0.5,
                        "TABLE.defaultSeparatorPadding": new Margin(1, 3, 0, 3)
                    }
                )
            )
        );


    this.diagram.linkTemplate =
        $(Link,
            {
                reshapable: false, resegmentable: false,
                relinkableFrom: true, relinkableTo: true,
                adjusting: Link.Stretch,
                fromSpot: Spot.LeftRightSides,
                toSpot: Spot.LeftRightSides
            },
            {
                selectionAdornmentTemplate:
                    $(Adornment,
                        $(Shape,
                            { isPanelMain: true, stroke: '#009fe3', strokeWidth: 2 }),
                        $(Shape,
                            { toArrow: 'Standard', fill: '#009fe3', stroke: null, scale: 2.5 })
                    )  // end Adornment
            },
            $(Shape, { stroke: '#646363', strokeWidth: 1.5 }),
            $(Shape, { toArrow: 'Standard', stroke: null },
            )
        );


    this.diagram.model =
        $(GraphLinksModel,
            {
                archetypeNodeData: {},
                linkFromPortIdProperty: 'sourceColumn',
                linkFromKeyProperty: 'source',
                linkToPortIdProperty: 'destinationColumn',
                linkToKeyProperty: 'destination',
                nodeDataArray: this.visualOrigins,
                linkDataArray: this.relations
            });


    const tempfromnode =
        $(Node,
            { layerName: 'Tool' },
            $(Shape, 'Rectangle',
                {
                    stroke: '#009fe3', strokeWidth: 3, fill: null,
                    portId: '', width: 1, height: 1
                })
        );
    const temptonode =
        $(Node,
            { layerName: 'Tool' },
            $(Shape, 'Rectangle',
                {
                    stroke: '#009fe3', strokeWidth: 3, fill: null,
                    portId: '', width: 1, height: 1
                })
        );

    this.diagram.toolManager.linkingTool.temporaryFromNode = tempfromnode;
    this.diagram.toolManager.linkingTool.temporaryFromPort = tempfromnode.port;
    this.diagram.toolManager.linkingTool.temporaryToNode = temptonode;
    this.diagram.toolManager.linkingTool.temporaryToPort = temptonode.port;
    this.diagram.toolManager.relinkingTool.temporaryFromNode = tempfromnode;
    this.diagram.toolManager.relinkingTool.temporaryFromPort = tempfromnode.port;
    this.diagram.toolManager.relinkingTool.temporaryToNode = temptonode;
    this.diagram.toolManager.relinkingTool.temporaryToPort = temptonode.port;

    this.diagram.toolManager.linkingTool.temporaryLink =
        $(Link,
            { layerName: 'Tool' },
            $(Shape,
                { stroke: '#646363', strokeWidth: 2 })
        );
}

makeSubBinding(targetname, sourcename) {
    const bind = new Binding(targetname, 'origin');
    bind.converter = function (details, target) {
        const value = details[sourcename];
        if (value !== undefined) {
            return value;
        }
    };
    return bind;
}


loadScrollingTable() {
    GraphObject.defineBuilder("AutoRepeatButton", function (args) {
        var $ = GraphObject.make;
        // some internal helper functions for auto-repeating
        function delayClicking(e, obj) {
            endClicking(e, obj);
            if (obj.click) {
                obj._timer =
                    setTimeout(function () { repeatClicking(e, obj); },
                        500);  // wait 0.5 seconds before starting clicks
            }
        }
        function repeatClicking(e, obj) {
            if (obj._timer) clearTimeout(obj._timer);
            if (obj.click) {
                obj._timer =
                    setTimeout(function () {
                        if (obj.click) {
                            (obj.click)(e, obj);
                            repeatClicking(e, obj);
                        }
                    },
                        100);  // 0.1 seconds between clicks
            }
        }
        function endClicking(e, obj) {
            if (obj._timer) {
                clearTimeout(obj._timer);
                obj._timer = undefined;
            }
        }

        return $("Button",
            { actionDown: delayClicking, actionUp: endClicking });
    });

    // Create a scrolling Table Panel, whose name is given as the optional first argument.
    // If not given the name defaults to "TABLE".
    // Example use:
    //   $("ScrollingTable", "TABLE",
    //     new Binding("TABLE.itemArray", "someArrayProperty"),
    //     ...)
    // Note that if you have more than one of these in a Part,
    // you'll want to make sure each one has a unique name.
    GraphObject.defineBuilder("ScrollingTable", function (args) {
        var $ = GraphObject.make;
        var tablename = GraphObject.takeBuilderArgument(args, "TABLE");

        // an internal helper function for actually performing a scrolling operation
        function incrTableIndex(obj, i) {
            var diagram = obj.diagram;
            var table = obj.panel.panel.panel.findObject(tablename);
            if (i === +Infinity || i === -Infinity) {  // page up or down
                var tabh = table.actualBounds.height;
                var rowh = table.elt(table.topIndex).actualBounds.height;  // assume each row has same height?
                if (i === +Infinity) {
                    i = Math.max(1, Math.ceil(tabh / rowh) - 1);
                } else {
                    i = -Math.max(1, Math.ceil(tabh / rowh) - 1);
                }
            }
            var idx = table.topIndex + i;
            if (idx < 0) idx = 0;
            else if (idx >= table.rowCount - 1) idx = table.rowCount - 1;
            if (table.topIndex !== idx) {
                if (diagram !== null) diagram.startTransaction("scroll");
                table.topIndex = idx;
                var node = table.part;  // may need to reroute links if the table contains any ports
                if (node instanceof Node) node.invalidateConnectedLinks();
                updateScrollBar(table);
                if (diagram !== null) diagram.commitTransaction("scroll");
            }
        }

        function updateScrollBar(table) {
            var bar = table.panel.elt(1);  // the scrollbar is a sibling of the table
            if (!bar) return;
            var idx = table.topIndex;

            var up = bar.findObject("UP");
            if (up) up.opacity = (idx > 0) ? 1.0 : 0.3;

            var down = bar.findObject("DOWN");
            if (down) down.opacity = (idx < table.rowCount - 1) ? 1.0 : 0.3;

            var tabh = bar.actualBounds.height;
            var rowh = table.elt(idx).actualBounds.height;  //?? assume each row has same height?
            if (rowh === 0 && idx < table.rowCount - 2) rowh = table.elt(idx + 1).actualBounds.height;
            var numVisibleRows = Math.max(1, Math.ceil(tabh / rowh) - 1);
            var needed = idx > 0 || idx + numVisibleRows <= table.rowCount;
            bar.opacity = needed ? 1.0 : 0.0;
        }

        return $(Panel, "Table",
            {
                _updateScrollBar: updateScrollBar
            },
            // this actually holds the item elements
            $(Panel, "Table",
                {
                    name: tablename,
                    column: 0,
                    stretch: GraphObject.Fill,
                    background: "whitesmoke",
                    rowSizing: RowColumnDefinition.None,
                    defaultAlignment: Spot.Top
                }),

            // this is the scrollbar
            $(RowColumnDefinition,
                { column: 1, sizing: RowColumnDefinition.None }),
            $(Panel, "Table",
                { column: 1, stretch: GraphObject.Vertical, background: "#DDDDDD" },
                // the scroll up button
                $("AutoRepeatButton",
                    {
                        name: "UP",
                        row: 0,
                        alignment: Spot.Top,
                        "ButtonBorder.figure": "Rectangle",
                        "ButtonBorder.fill": "lightblue",
                        click: function (e, obj) { incrTableIndex(obj, -1); }
                    },
                    $(Shape, "TriangleUp",
                        { stroke: null, desiredSize: new Size(6, 6) })),
                // (someday implement a thumb here and support dragging to scroll)
                // the scroll down button
                $("AutoRepeatButton",
                    {
                        name: "DOWN",
                        row: 2,
                        alignment: Spot.Bottom,
                        "ButtonBorder.figure": "Rectangle",
                        "ButtonBorder.fill": "lightgray",
                        click: function (e, obj) { incrTableIndex(obj, +1); }
                    },
                    $(Shape, "TriangleDown",
                        { stroke: null, desiredSize: new Size(6, 6) }))
            )
        );
    });
}
}

Thanks again, i really appreciate your dedication.

Ah, so you copied the code and removed all of the “go.” prefixes. That should be OK.

BTW, you are defining $ twice and calling loadScrollingTable twice. I assume that’s just a copy-edit error.

Notice how the TextBlocks seemed to get a different font, in addition to the header not having a blue background. The font would need to be specified in the itemTemplate. It’s clear that your nodeTemplate doesn’t specify a font in the itemTemplate, so that is why it is using the default font:

        $(TextBlock, { column: 0 }, new Binding("text", "name")),
        $(TextBlock, { column: 1 }, new Binding("text", "columnType"))

Yes, it was a copy-paste error. We have managed to customize some styles, still we need to change the font-family of the text blocks. It is possible to use “Roboto”? When we use something like this “bold 16px Roboto Regular”, is using the default font-family.

Also, im trying to draw a black line (red line at the attached screenshot) at the left of the scrollbar, but its being difficult. Any advice??

Thanks!

For a black separator line, you need to realize that you have nested Table Panels.

The outer one is the “ScrollingTable” itself. That table has two columns, the “TABLE” content and the scrollbar.

The inner one is the content and is named “TABLE”, and the sample sets a lot of its properties and adds some bindings on its properties.

So you just need to set the separator stroke on the “ScrollingTable”:

          $("ScrollingTable",
            {
              name: "SCROLLER",
              desiredSize: new go.Size(NaN, 60),  // fixed width
              stretch: go.GraphObject.Fill,       // but stretches vertically
              defaultColumnSeparatorStroke: "black",
              defaultColumnSeparatorStrokeWidth: 0.5
            },
            . . .

For the font, how do you load the “Roboto” font on your page? Make sure it’s loaded before constructing the diagram.

If you are not going to specify a web safe font, you really should provide a fallback font that is web safe:

font: 'bold 14px Roboto, serif'

Thank you! That works perfect.