Siblings with itemArrays restrict each other

Hi team,

I’ve got one that’s proving to be quite the headache.
The short version is that I have a node template that has a content panel and that content panel has two instances of a custom diagram object as siblings.

Something like this, which is heavily abridged:

class PillsList {
  constructor(key) {
    const list = new go.Panel(go.Panel.Position, {
      name: `PillsList.${key}`,
    });

    const items = new go.Panel(go.Panel.Flow, {
      itemTemplate: new PillsListItem(),
    });

    items.bind('itemArray', `${key}.items`);
    list.add(items);
    return list;
  }
}

const Node = new go.Node();
Node.add(new PillsList('tags'));
Node.add(new PillsList('properties'));

The node data then has both a tags.items and a properties.items which are bound.
Trouble is, when both of these panels are instantiated, the first list is restricted by the length of the second.
However, when only one is present, the lists render the exact right amount of items.

See below the exact same card with only the tags, then with the properties as well.
image
image

I have tried a bunch of things so far:

  • Confirmed that each object instance is receiving the correct binding of itemArray, with tags showing Array(3) even when only rendering one
  • Gave each panel instance unique names top-to-bottom of their trees
  • Nested the panels with the bound itemArrays inside another Panel (list vs items above) just in-case it was a direct sibling relationship
  • Gave each instance its own itemTemplateMap with the key being set to the category of each item
  • Variations of PillsList/Item.copy/Template() to prevent them being considered the same object

At this point, short of making separate root objects, I’m not really sure what else I can do here.
The goal was code re-use, so really a fat copy/paste is the last thing I’d like to do.

Is this a bug? Am I missing something?
Let me know what else I can share to give needed context.

Thanks in advance,
James

It’s hard for me to tell exactly what properties each of the three Panels have, and what properties each pill item Panel has. I suppose the properties of the Panel containing the two-list-container Panel (PillsList) matters too.

But it seems odd that PillsList is of panel type “Position”. I believe part of the problem is that some pills are missing. I wonder if it is because they are overlapping.

BTW, do you want the two lists of pills to be arranged above/below each other? Use a “Vertical” Panel for that, not a “Position” Panel.

Morning Walter, hope you had a great weekend!

The actual item lists have a Flow layout panel, the position panel is wrapping the flow panel. This was done to test if the Flow panels being direct siblings were the cause. The content panel of the card itself is a vertical panel to handle stacking them vertically.

I’ll try a little more verbose to show the properties in use by each and how the template is composed. Note that the XyzConfig objects represent the defaults for the node/panel data objects.

PillsList

const PillsListConfig = {
  'pills.items': [],
  'pills.visible': false,
}

class PillsList {
  constructor(key = 'pills') {
    const pills = new go.Panel(go.Panel.Flow, {
      itemArray: PillsListConfig['pills.items'],
      itemTemplate: new PillsListItem(),
      name: `PillsListItems.${key}`,
      spacing: new go.Size(2, 0),
      stretch: go.Stretch.Horizontal,
    });

    pills.bind('itemArray', `${key}.items`);
    return pills;
  }
}

PillsListItem

const PillsListItemConfig = {
  label: '',
  tooltip: '',
  expanded: false,
  fill: palettes.brand.grey.shades[0],
  stroke: palettes.brand.grey.tints[2],
}

class PillsListItem {
  constructor() {
    const panel = new go.Panel(go.Panel.Auto, {
      name: `Pill`,
    });

    panel.add(this.background());
    panel.add(this.content());
    return panel;
  }

  background() {
    const background = new go.Shape({
      figure: 'Border',
      fill: PillsListItemConfig['fill'],
      name: 'PillBackground',
      parameter1: 4,
      spot1: new go.Spot(0, 0),
      spot2: new go.Spot(1, 1),
      strokeWidth: 0,
    });

    background.bind('fill', 'fill');
    return background;
  }

  content() {
    const content = new go.Panel(go.Panel.Position, {
      minSize: new go.Size(16, 12),
      name: 'PillContent',
    });

    content.add(this.label());
    return content;
  }

  label() {
    const label = new go.TextBlock({
      font: '600 10px Poppins, sans-serif',
      margin: new go.Margin(0, 4),
      name: 'PillText',
      spacingAbove: 2,
      spacingBelow: 2,
      textAlign: 'center',
      visible: false,
      stroke: PillsListItemConfig['stroke'],
      text: PillsListItemConfig['label'],
    });

    label.bind('stroke', 'stroke');
    label.bind('text', 'label');
    label.bind('visible', 'expanded');
    return label;
  }
}

CardTemplate

export default class CardTemplate {
  constructor() {
    const template = new go.Group(go.Panel.Spot, {
      computesBoundsAfterDrag: true,
      computesBoundsIncludingLocation: true,
      isSubGraphExpanded: CardTemplateConfig['card.items.expanded'],
      isTreeExpanded: CardTemplateConfig['tree.expanded'],
      layout: new go.GridLayout({ ... }),
      name: 'CardEntity',
      selectionAdorned: false,
      zOrder: 20,
    });

    const body = new CardBody();
    // ...
    body.slots.content.add(this.tags());
    body.slots.content.add(this.properties());
    // ...
    template.add(body);

    template.bind('isSubGraphExpanded', 'card.items.expanded');
    template.bind('isTreeExpanded', 'tree.expanded');
    return template;
  }

  // ...

 properties() {
    return new PillsList('properties', {
      margin: new go.Margin(0, 8, 4, 8),
    });
  }

  tags() {
    return new PillsList('tags', {
      margin: new go.Margin(0, 8, 4, 8),
    });
  }

  // ...
}

CardBody
Abridged, mainly just to show the panel and how the “slots” work. They’re custom defined property on the template class only used for composition. Once .copy/Template is called downstream, it’s removed.

class CardBody {
  constructor() {
    const body = new go.Panel(go.Panel.Position, {
      cursor: 'grab',
      name: 'Card',
    });

    body.slots = {};
    // ...
    body.slots.content = this.content();
    // ...

    return body;
  }

  content() {
    const content = new go.Panel(go.Panel.Vertical, {
      name: 'Content',
      width: CardBodyConfig['card.body.width'],
    });

    content.bind('width', 'card.body.width');
    return content;
  }
}

NodeData
Relevant node data for the given example role card in the OP.

{
    "key": "<ROLE_ID>",
    // ...
    "properties.items": [
        {
            "key": "CHsk2sYFoLWJSUtDKorGQ:CHuQiyavgcRcM53BXgK3P",
            "label": "OptionA",
            "tooltip": "RoleDropdownProperty: OptionA",
            "fill": "#FF3838",
            "stroke": "#FAFAFA",
            "expanded": true
        }
    ],
    "properties.visible": true,
    "tags.items": [
        {
            "key": "<ROLE_ID>:<TAG_ID_A>",
            "label": "Role Tag A",
            "tooltip": "Role Tag A",
            "fill": "#85FCB1",
            "stroke": "#333333",
            "expanded": true
        },
        {
            "key": "<ROLE_ID>:<TAG_ID_B>",
            "label": "Role Tag B",
            "tooltip": "Role Tag B",
            "fill": "#B4FAF5",
            "stroke": "#333333",
            "expanded": true
        },
        {
            "key": "<ROLE_ID>:<TAG_ID_C>",
            "label": "Role Tag C",
            "tooltip": "Role Tag C",
            "fill": "#FF9F1A",
            "stroke": "#333333",
            "expanded": true
        }
    ],
    "tags.visible": true,
    // ...
}

So I’ve tested setting the properties.visible to false, and again all 3 role tags showed up. So it’s only while both those PillsList are visible that they start to become affected.
I also tested the possibility of the pills overlapping each other by setting the PillsListItem, opacity to quite low, but there’s only the card background behind them.

Hopefully this helps give more context.

Thanks,
James

Here’s a complete sample using your model data:

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
  <button id="myTestButton2">tags.visible</button><button id="myTestButton">properties.visible</button>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/create-gojs-kit/dist/extensions/PanelLayoutFlow.js"></script>
  <script id="code">
const myDiagram =
  new 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 =
  new go.Node("Auto", { width: 200, resizable: true })  // OPTIONAL: resizable and binding on "desiredSize"
    .bindTwoWay("desiredSize", "size", go.Size.parse, go.Size.stringify)
    .add(
      new go.Shape({ fill: "white" }),
      new go.Panel("Vertical", { stretch: go.Stretch.Fill, alignment: go.Spot.TopLeft, margin: 4 })
        .add(
          new go.TextBlock({ row: 0, margin: 4 })  // replace this with your complex header panel
                .bind("text", "key"),
          new go.Panel("Table", { stretch: go.Stretch.Fill, defaultStretch: go.Stretch.Fill, alignment: go.Spot.TopLeft })
            .add(
              new go.Panel(new PanelLayoutFlow({ spacing: new go.Size(4, 4) }), {
                  row: 0, margin: 2,
                  itemTemplate: new go.Panel("Auto")
                    .add(
                      new go.Shape("RoundedRectangle", { strokeWidth: 0 })
                        .bind("fill"),
                      new go.TextBlock()
                        .bind("text", "label")
                        .bind("stroke")
                    )
                })
                .bind("itemArray", "tags.items")
                .bind("visible", "tags.visible"),
              new go.Panel(new PanelLayoutFlow({ spacing: new go.Size(4, 4) }), {
                  row: 1, margin: 2,
                  itemTemplate: new go.Panel("Auto")
                    .add(
                      new go.Shape("RoundedRectangle", { strokeWidth: 0 })
                        .bind("fill"),
                      new go.TextBlock()
                        .bind("text", "label")
                        .bind("stroke")
                    )
                })
                .bind("itemArray", "properties.items")
                .bind("visible", "properties.visible"),
            )
        )
    );

myDiagram.model = new go.GraphLinksModel(
[
  {
    "key": "<ROLE_ID>",
    // ...
    "properties.items": [
        {
            "key": "CHsk2sYFoLWJSUtDKorGQ:CHuQiyavgcRcM53BXgK3P",
            "label": "OptionA",
            "tooltip": "RoleDropdownProperty: OptionA",
            "fill": "#FF3838",
            "stroke": "#FAFAFA",
            "expanded": true
        }
    ],
    "properties.visible": true,
    "tags.items": [
        {
            "key": "<ROLE_ID>:<TAG_ID_A>",
            "label": "Role Tag A",
            "tooltip": "Role Tag A",
            "fill": "#85FCB1",
            "stroke": "#333333",
            "expanded": true
        },
        {
            "key": "<ROLE_ID>:<TAG_ID_B>",
            "label": "Role Tag B",
            "tooltip": "Role Tag B",
            "fill": "#B4FAF5",
            "stroke": "#333333",
            "expanded": true
        },
        {
            "key": "<ROLE_ID>:<TAG_ID_C>",
            "label": "Role Tag C",
            "tooltip": "Role Tag C",
            "fill": "#FF9F1A",
            "stroke": "#333333",
            "expanded": true
        }
    ],
    "tags.visible": true,
    // ...
  }
]);

document.getElementById("myTestButton").addEventListener("click", e => {
  myDiagram.model.commit(m => {
    const data = m.nodeDataArray[0];
    m.set(data, "properties.visible", !data["properties.visible"]);
  })
});

document.getElementById("myTestButton2").addEventListener("click", e => {
  myDiagram.model.commit(m => {
    const data = m.nodeDataArray[0];
    m.set(data, "tags.visible", !data["tags.visible"]);
  })
});
  </script>
</body>
</html>

It looks like we had an old version of the PanelLayoutFlow extension that was caching lineBreadths and lineLengths. Replacing it with the latest from the below has fixed the issue.

https://gojs.net/latest/extensions/PanelLayoutFlow.js