Adding multiple text blocks

Hi -
Can you tell me how to add multiple text blocks (preferably in one call). Currently we are using goModel.addNodeDataCollection() to add multiple text blocks but it only adds one text block to the diagram, not all of the text blocks that we want to add. I assume this is because text blocks are not nodes.
Thanks,
Kathleen

That’s correct – a Node can have multiple TextBlocks in them, if you want. Model.addNodeDataCollection adds a number of nodes to the diagram. But it’s quite reasonable for your Nodes to only have one TextBlock each.

Could you please show us what you want to build and what your model data is?

Hi Walter,
I don’t want to add multiple text blocks into a node, I want to add multiple text blocks onto the diagram / canvas. There are no nodes involved in my scenario. Currently to add text blocks to the canvas we are calling Model.addNodeDataCollection and passing an array of text blocks, each having an individual keys. I would like to know how to add an array of 3 text blocks into the diagram.
Thanks,
Kathleen

You still haven’t shown a screenshot or sketch of what you want to accomplish.

Perhaps it’s not clear that with Models you can only add data objects (plain JavaScript Objects) to the model, not GraphObjects such as TextBlocks or Nodes. For each Object added to a Model a Node is automatically added to the Diagram. What that Node might look like depends on the node template that you use. It defaults to a Node holding just a single TextBlock, but hopefully you have defined your own Diagram.nodeTemplate.

[EDIT – I have added this complete stand-alone sample]

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Editor</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
  <div style="width: 100%; display: flex; justify-content: space-between">
    <div style="display: flex; flex-direction: column; margin: 0 2px 0 0">
      <div id="myPaletteDiv" style="flex-grow: 1; width: 100px; background-color: floralwhite; border: solid 1px black"></div>
      <div id="myOverviewDiv" style="margin: 2px 0 0 0; width: 100px; height: 100px; background-color: whitesmoke; border: solid 1px black"></div>
    </div>
    <div id="myDiagramDiv" style="flex-grow: 1; height: 400px; border: solid 1px black"></div>
  </div>
  <div>
    <button id="myLoadButton">Load</button>
    <button id="mySaveButton">Save</button>
  </div>
  <textarea id="mySavedModel" style="width:100%;height:200px">
{ "class": "go.GraphLinksModel",
  "nodeDataArray": [
{"key":1, "text":"hello", "color":"green", "location":"0 0"},
{"key":2, "text":"world", "color":"red", "location":"70 0"}
  ],
  "linkDataArray": [
{"from":1, "to":2}
  ]}
  </textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
// initialize main Diagram
const myDiagram =
  new go.Diagram("myDiagramDiv",
    {
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  new go.Node({ locationSpot: go.Spot.Center })
    .bindTwoWay("location", "location", go.Point.parse, go.Point.stringify)
    .add(
      new go.TextBlock({
          margin: new go.Margin(5, 5, 3, 5), font: "10pt sans-serif",
          minSize: new go.Size(16, 16), maxSize: new go.Size(120, NaN),
          editable: true
        })
        .bindTwoWay("text")
        .bind("stroke", "color")
    );

// initialize Palette
myPalette =
  new go.Palette("myPaletteDiv",
    {
      nodeTemplateMap: myDiagram.nodeTemplateMap,
      model: new go.GraphLinksModel([
        { text: "red node", color: "red" },
        { text: "green node", color: "green" },
        { text: "blue node", color: "blue" },
        { text: "orange node", color: "orange" }
      ])
    });

// initialize Overview
myOverview =
  new go.Overview("myOverviewDiv",
    {
      observed: myDiagram,
      contentAlignment: go.Spot.Center
    });

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

function load() {
  const str = document.getElementById("mySavedModel").value;
  myDiagram.model = go.Model.fromJson(str);
}
document.getElementById("myLoadButton").addEventListener("click", load);

load();
  </script>
</body>
</html>

Hi Walter,
Here are some screen shots of what we want to do. In this case we have 3 text blocks in the model, not within nodes. We want to select those three text blocks, copy and paste them. When we try to add them to the diagram / model / canvas using the call to Model.addNodeDataCollection only one text block gets added.

Screen Shot 2024-05-08 at 9.35.39 AM

Screen Shot 2024-05-08 at 9.35.54 AM

This is the code snippet where we are trying to add those text blocks, we are erroneously adding those text blocks as nodesToAdd :

Screen Shot 2024-05-08 at 9.44.23 AM

Thanks,
Kathleen

I think this is what we need to do to add new text blocks

We are trying to create these text blocks as nodes and then add them. I think I am perhaps answering my question. I think our real misunderstanding is that we are treating text blocks as if they are instances of nodes but they are not. Is this a true statement, “Text blocks are not instances of nodes”?

Thanks,
Kathleen

Yes, the TextBlock class is different from the Node class, and they do not inherit from each other either.

Try this:

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Editor</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
  <div style="width: 100%; display: flex; justify-content: space-between">
    <div style="display: flex; flex-direction: column; margin: 0 2px 0 0">
      <div id="myPaletteDiv" style="flex-grow: 1; width: 100px; background-color: floralwhite; border: solid 1px black"></div>
      <div id="myOverviewDiv" style="margin: 2px 0 0 0; width: 100px; height: 100px; background-color: whitesmoke; border: solid 1px black"></div>
    </div>
    <div id="myDiagramDiv" style="flex-grow: 1; height: 400px; border: solid 1px black"></div>
  </div>
  <div>
    <button id="myLoadButton">Load</button>
    <button id="mySaveButton">Save</button>
    <button id="myTestButton">Add</button>
    Or double-click in the diagram background to add several nodes at that point.
  </div>
  <textarea id="mySavedModel" style="width:100%;height:200px">
{ "class": "go.GraphLinksModel",
  "nodeDataArray": [
{"key":1, "text":"hello", "color":"lightgreen", "location":"0 0"},
{"key":2, "text":"world", "color":"pink", "location":"140 30"}
  ],
  "linkDataArray": [] }
  </textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
// initialize main Diagram
const myDiagram =
  new go.Diagram("myDiagramDiv",
    {
      doubleClick: e => addMulti(e.documentPoint),
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  new go.Node("Auto", { locationSpot: go.Spot.Center })
    .bindTwoWay("location", "location", go.Point.parse, go.Point.stringify)
    .add(
      new go.Shape("RoundedRectangle", { fill: 'white', stroke: 'gray' })
        .bind("fill", "color"),
      new go.TextBlock({
          margin: new go.Margin(5, 5, 3, 5),
          font: "11pt sans-serif",
          editable: true
        })
        .bindTwoWay("text")
    );

// initialize Palette
myPalette =
  new go.Palette("myPaletteDiv",
    {
      initialAutoScale: go.AutoScale.Uniform,
      nodeTemplateMap: myDiagram.nodeTemplateMap,
      model: new go.GraphLinksModel([
        { text: "red node", color: "pink" },
        { text: "green node", color: "lightgreen" },
        { text: "orange node", color: "orange" },
        { text: "lavender node", color: "lavender" }
      ])
    });

// initialize Overview
myOverview =
  new go.Overview("myOverviewDiv",
    {
      observed: myDiagram,
      contentAlignment: go.Spot.Center
    });

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

function load() {
  const str = document.getElementById("mySavedModel").value;
  myDiagram.model = go.Model.fromJson(str);
}
document.getElementById("myLoadButton").addEventListener("click", load);

var Start = new go.Point(0, 0);
function addMulti(pt, prefix, color) {
  if (!prefix) prefix = "new";
  if (!color) color = go.Brush.randomColor();
  myDiagram.model.commit(m => {
    const p = pt.copy();
    const arr = [];
    for (let i = 0; i < 3; i++) {
      arr.push({ text: prefix + " " + i, location: go.Point.stringify(p), color: color });
      p.x += 20;
      p.y += 20;
    }
    m.addNodeDataCollection(arr);
  });
}
document.getElementById("myTestButton").addEventListener("click",
  () => {
    addMulti(Start, "button");
    Start.x -= 50;
    Start.y += 50;
  });

load();
  </script>
</body>
</html>

Oh wait. Now that I read your reply I see what you are saying and we actually are adding plain javascript objects.

The most recent sample, that I just posted and edited, does call Model.addNodeDataCollection.

The node template is more than just a TextBlock – it’s an “Auto” Panel that has a border Shape around the TextBlock.

Clicking the Add button or double-clicking in the diagram background will call the addMulti function to call addNodeDataCollection.

OK, I see the addMulti function that you created. The interesting thing is when we call addNodeDataCollection with plain javascript objects that represent nodes we can add multiple nodes without having any type of addMulti function. But when we call addNodeDataCollection with plain javascript objects that represent text blocks we only add one new text block.

I will gather data so I can show you what is in the plain javascript object for a new node and the plain javascript object for a new text block.

This might be the best way to show what we are adding when we call addNodeDataCollection with an array of objects.
The plain javascript object for a Text Block is defined by this -

import type { CommentToolkit } from '@ayx/workflow-toolkit';

import type { INodeData } from './nodeData';
import NodeData from './nodeData';

export const textBoxShapeArray = ['RoundedRectangle', 'Rectangle', 'Ellipse', 'None'] as const;
export interface ITextBoxNodeData extends INodeData {
  bgImage?: string;

  fillColor: string;

  fontName: string;

  fontSize: number;

  fontStyle: number;

  height: number;

  justification: number;

  shape: number;

  text: string;

  textColor: string;

  width: number;
}

/**
 * Gojs representation of a textbox node
 */
export default class TextBoxNodeData extends NodeData {
  public width: number;

  public height: number;

  public text: string;

  public fontName: string;

  public fontSize: number;

  public fontStyle: number;

  public textColor: string;

  public fillColor: string;

  public justification: number;

  public bgImage?: string;

  public shape: number;

  public constructor(node: CommentToolkit, zIndex: number) {
    super(node, zIndex);

    this.getTextBoxProperties(node);
  }

  private getTextBoxProperties(node: CommentToolkit) {
    this.category = 'TextBox';

    this.width = node.width;
    this.height = node.height;
    this.text = node.text;

    this.fontName = node.fontName;

    this.fontSize = node.fontSize;

    this.fontStyle = node.fontStyle;

    this.fillColor = node.fillColor;

    this.textColor = node.textColor;

    this.justification = node.justification;

    this.shape = node.shape;

    this.bgImage = node.bgImage ? `data:image/png;base64,${node.bgImage}` : undefined;
  }
}

The plain javascript object for a basic Node is defined by this -

import type { StaticConfig, WorkflowProperties } from '@ayx/workflow-schemas';
import type { NodeToolkit } from '@ayx/workflow-toolkit';
import { WorkflowToolkit } from '@ayx/workflow-toolkit';

import AlteryxPlugin from '../plugins/alteryxPlugin';

import unsupportedToolIcon from './icons/unsupportedToolIcon.svg';
import type LinkDataItem from './linkDataItem';
import { annotationDisplayMode } from './modelConstants';
import type { INodeData } from './nodeData';
import NodeData from './nodeData';
import Service from './service';

export interface IToolNodeData extends INodeData {
  connectionProgress: string;
  downstreamConnections: LinkDataItem[];
  error: string;
  inInterface: Service[];
  inputConnections: Service[];
  label: string;
  outInterface: Service[];
  outputConnections: Service[];
  progress: number;
  source: string | typeof import('*.svg');
  status: { source: string | typeof import('*.svg') } | undefined;
  toolConfig: any;
  upstreamConnections: LinkDataItem[];
}

/**
 * Go js representation of a tool node.
 */
export default class ToolNodeData extends NodeData {
  public inInterface: Service[];

  public outInterface: Service[];

  public outputConnections: Service[];

  public inputConnections: Service[];

  public progress = 0;

  public connectionProgress: string;

  public downstreamConnections: LinkDataItem[] = [];

  public upstreamConnections: LinkDataItem[] = [];

  // image to use for icon

  public source: string | typeof import('*.svg') = unsupportedToolIcon;

  public label: string | undefined;

  public toolConfig = {};

  // error message
  public error = '';

  public status: IToolNodeData['status'] = undefined;

  public constructor(node: NodeToolkit, properties: WorkflowProperties, zIndex: number, locale: string) {
    super(node, zIndex);

    this.getToolProperties(node, properties, locale);

    this.category = `${properties.LayoutType}Node`;
  }

  public static getToolAnnotation(node: NodeToolkit, properties: WorkflowProperties) {
    if (properties.Annotation['@on'] === 'True') {
      switch (node.annotationDisplayMode) {
        case annotationDisplayMode.always:
        case annotationDisplayMode.inherit: {
          return node.annotationDisplay;
        }
        default: {
          return '';
        }
      }
    }

    return '';
  }

  public static createInputConnections(toolConfig: StaticConfig, locale: string): Service[] {
    const needsAnchorLabel = toolConfig.inputConnections?.length > 1;

    return (toolConfig.inputConnections || []).map(connection => {
      const anchorLabel = needsAnchorLabel ? connection.anchorLabel(locale) : '';
      return new Service(
        'INPUT_CONNECTION',
        connection.name,
        anchorLabel,
        undefined,
        connection.connectionType || '',
        connection.multiple,
        connection.optional,
        connection.isDefault,
      );
    });
  }

  public static createOutputConnections(toolConfig: StaticConfig, locale: string): Service[] {
    const needsAnchorLabel = toolConfig.outputConnections?.length > 1;

    return (toolConfig.outputConnections || []).map(connection => {
      const anchorLabel = needsAnchorLabel ? connection.anchorLabel(locale) : '';
      return new Service(
        'OUTPUT_CONNECTION',
        connection.name,
        anchorLabel,
        undefined,
        connection.connectionType || '',
        connection.multiple,
        connection.optional,
        connection.isDefault,
      );
    });
  }

  public static createInputInterfaces(toolConfig: StaticConfig): Service[] {
    return (toolConfig.inputServices || []).map(
      connection => new Service('INPUT_SERVICE', connection.name, undefined, !!connection.interfacePort),
    );
  }

  public static createOutputInterfaces(toolConfig: StaticConfig): Service[] {
    return (toolConfig.outputServices || []).map(
      connection => new Service('OUTPUT_SERVICE', connection.name, undefined, !!connection.interfacePort),
    );
  }

  private getToolProperties(node: NodeToolkit, properties: WorkflowProperties, locale: string) {
    const pluginId = node.getPluginId();

    this.showonLeft = node.annotationDisplayOnLeft;
    this.annotation = ToolNodeData.getToolAnnotation(node, properties);
    // tool not found, create a blank placeholder
    const toolConfig: StaticConfig = WorkflowToolkit.getStaticDefinitionForPluginId(pluginId) || new AlteryxPlugin();

    const toolIconSet = toolConfig.icons;
    if (toolIconSet) {
      this.source = toolIconSet.coloredWithShape;
    }

    this.inputConnections = ToolNodeData.createInputConnections(toolConfig, locale);
    this.outputConnections = ToolNodeData.createOutputConnections(toolConfig, locale);
    this.inInterface = ToolNodeData.createInputInterfaces(toolConfig);
    this.outInterface = ToolNodeData.createOutputInterfaces(toolConfig);

    this.toolConfig = node.configuration;
  }
}

I can also send you the definition for NodeData which both ToolNodeData and TextBoxNodeData inherit from if you would like to see that.

I see this in the description for addNodeDataCollection -

    /**
     * Add to this model all of the node data held in an Array or in an Iterable of node data objects.
     * @param {Iterable.<Object>|Array.<Object>} coll a collection of node data objects to add to the #nodeDataArray
     * @since 1.3
     */
    addNodeDataCollection(coll: Iterable<ObjectData> | Array<ObjectData>): void;

"Add to this model all of the Node data held in an array …
This to me says that adding an array of Text Block data should be added some other way.

How you define your model data really doesn’t matter to GoJS. OK, you aren’t using plain old JavaScript Objects, but instances of a class that extends Object (which is how I assume your NodeData is defined). The point is that the class is not GraphObject or a class derived from GraphObject.

We added Model.addNodeDataCollection as a convenience function so that you don’t have to iterate calling Model.addNodeData when you have a bunch of node data objects to add to the model.

OK, so what is happening is I add the array of Text Blocks and they actually get added to the model but only one of them is visible. They all have separate locations / goJs points and they each have individual keys so I’m not sure why only one of the Text Blocks is visible.

What is your node template?

This is the node template -

import type { InputEvent, Part } from 'gojs';
import {
  Binding,
  Map as GoMap,
  GraphObject,
  Margin,
  Node,
  Panel,
  Picture,
  Point,
  Shape,
  Size,
  Spot,
  TextBlock,
} from 'gojs';

import type { ToolNodeData } from '../adapter/models';
import { HORIZONTAL_NODE_CATEGORY, VERTICAL_NODE_CATEGORY } from '../adapter/models';
import type { IGlobalSettings } from '../common/globalSettings';
import AnnotationHTMLInfo from '../go/textEditors/annotationHTMLInfo';
import { beginAnnotationEdit, selectionChanged } from '../util/nodeHelpers';

import {
  ANNOTATION_MAX_LINES,
  OPACITY_DISABLED,
  OPACITY_ENABLED,
  OPACITY_PROCESSING,
  Severity,
  TOOL_SELECTION_BACKGROUND_COLOR,
} from './constants';
import alertCircle from './icons/alert_circle.svg';
import alertTriangle from './icons/alert_triangle.svg';
import anchorIconToOptional from './icons/Arrow_Optional_In.svg';
import circle from './icons/circle.svg';
import anchorIconTo from './icons/Input.svg';
import anchorIconToSelected from './icons/Input_Selected.svg';
import anchorIconMultipleIn from './icons/MultipleInput.svg';
import anchorIconMultipleInSelected from './icons/MultipleInput_Selected.svg';
import anchorIconFrom from './icons/Output.svg';
import anchorIconFromOptional from './icons/Output_Optional_Out.svg';
import anchorIconFromSelected from './icons/Output_Selected.svg';
import spinner from './icons/spinner.svg';

const { make: $ } = GraphObject;
const name = 'toolNode';
const getToolIconPanel = (): Panel =>
  $(
    Panel,
    Panel.Spot,
    {
      name: 'toolIconPanel',
    },
    // all objects will be positioned around this rectangle
    // the height and width here is to clip off the extra space around the icon svgs
    $(
      Shape,
      'RoundedRectangle',
      {
        fill: 'transparent',
        height: 44,
        name: 'rectangle',
        strokeDashArray: [4, 2],
        strokeWidth: 0,
        width: 44,
      },
      new Binding('stroke', 'selectionColor').ofModel(),
      new Binding('strokeWidth', 'isSelected', s => (s ? 2 : 0)).ofObject(),
      new Binding('fill', 'isSelected', s => (s ? TOOL_SELECTION_BACKGROUND_COLOR : 'transparent')).ofObject(),
    ),
    $(
      Picture,
      {
        alignment: Spot.Center,
        alignmentFocus: Spot.Center,
        height: 46,
        name: 'nodeIcon',
        width: 46,
        // imageAlignment: Spot.Center
      },
      new Binding('source'),
      // if a tool is processing then change the opacity so the spinner on top of the tool is easy to see
      new Binding('opacity', '', node => {
        const { diagram, data } = node;
        return diagram && data?.inFlight ? OPACITY_PROCESSING : OPACITY_ENABLED;
      }).ofObject(),
    ),
    $(
      Panel,
      'Auto',
      {
        alignment: new Spot(0.5, 0.5),
        name: 'toolProgress',
        visible: false,
      },
      new Binding('visible', 'progress', (progress: number) => progress > 0),
      $(
        Shape,
        'Rectangle',
        {
          strokeWidth: 1,
        },
        new Binding('fill', 'toolProgressBackground').ofModel(),
        new Binding('stroke', 'toolProgressStroke').ofModel(),
      ),
      $(
        TextBlock,
        {
          margin: new Margin(2, 2, 0.25, 2),
          textAlign: 'center',
        },
        new Binding(
          'font',
          '',
          (modelData: IGlobalSettings): string => `${modelData.toolProgressFontSize} ${modelData.font}`,
        ).ofModel(),
        new Binding(
          'text',
          '',
          (node: Node) =>
            (node.diagram &&
              node.diagram.model.modelData.intl.formatNumber(node.data.progress / 100, { style: 'percent' })) ||
            '',
        ).ofObject(),
      ),
    ),
  );

/**
 * positioning of error will always appear below the tool node,
 * this needs reexamined if we support vertical nodes
 *
 * @param isInAnnotationPanel
 */
const getErrorMessagePanel = (isInAnnotationPanel: boolean): Panel =>
  $(
    Panel,
    'Vertical',
    {
      margin: new Margin(2, 0, 0, 0),
      name: 'errorMessagePanel',
    },
    $(
      TextBlock,
      {
        editable: false,
        margin: new Margin(5, 5, 0, 5),
        maxLines: 75,
        name: 'errorMessageText',
        overflow: TextBlock.OverflowEllipsis,
        shadowVisible: true,
        textAlign: 'center',
        width: 70,
        wrap: TextBlock.WrapFit,
      },
      new Binding(
        'font',
        '',
        (modelData: IGlobalSettings): string => `${modelData.errorFontSize} ${modelData.font}`,
      ).ofModel(),
      new Binding('stroke', '', (node: Node): string => {
        const { data } = node;
        return data.severity >= Severity.ERROR
          ? `${node.diagram.model.modelData.errorStroke}`
          : `${node.diagram.model.modelData.warningStroke}`;
      }).ofObject(),
      new Binding('text', '', (data: ToolNodeData): string => data.error || ''),
    ),
    new Binding('background', 'annotationBackground').ofModel(),
    new Binding('visible', '', (node: Node): boolean => {
      const { isSelected, data } = node;
      if (isInAnnotationPanel) {
        return isSelected && !!data.error && !data.showonLeft;
      }
      return isSelected && !!data.error && !isInAnnotationPanel;
    }).ofObject(),
  );

const getAnnotationTextPanel = (): Panel =>
  $(
    Panel,
    'Vertical',
    {
      margin: new Margin(2, 0, 0, 0),
      name: 'nodeAnnotationPanel',
      width: 70,
    },
    $(
      TextBlock,
      {
        editable: false,
        mouseEnter: (e: InputEvent, node: GraphObject) => {
          const tb = node.part.findObject('nodeAnnotationText');
          // Show entire Annotation on hover
          (tb as TextBlock).maxLines = Infinity;
        },
        mouseLeave: (e: InputEvent, node: GraphObject) => {
          const tb = node.part.findObject('nodeAnnotationText');
          (tb as TextBlock).maxLines = ANNOTATION_MAX_LINES;
        },
        name: 'nodeAnnotationText',
        overflow: TextBlock.OverflowEllipsis,
        shadowVisible: true,
        textAlign: 'center',
        textEditor: new AnnotationHTMLInfo(),
        width: 70,
        wrap: TextBlock.WrapFit,
      },
      new Binding('maxLines', '', (node: Node): number =>
        node.diagram.selection.count === 1 && node.isSelected ? Infinity : ANNOTATION_MAX_LINES,
      ).ofObject(),
      new Binding('text', 'annotation').makeTwoWay(),
      new Binding(
        'font',
        '',
        (modelData: IGlobalSettings): string => `${modelData.annotationFontSize} ${modelData.font}`,
      ).ofModel(),
      new Binding('stroke', 'annotationStroke').ofModel(),
    ),
    new Binding('background', 'annotationBackground').ofModel(),
  );

const getAnnotationPanel = (orientation: string): Panel =>
  $(
    Panel,
    'Vertical',
    {
      name: 'annotationPanel',
      width: 70,
    },
    // new Binding('background', 'annotationBackground').ofModel(),
    new Binding('alignment', '', (data: ToolNodeData): Spot => {
      if (orientation === 'Horizontal') {
        return data.showonLeft ? Spot.TopCenter : new Spot(0.5, 1);
      }
      return data.showonLeft ? Spot.LeftCenter : Spot.RightCenter;
    }),
    new Binding('alignmentFocus', '', (data: any): Spot => {
      if (orientation === 'Horizontal') {
        return data.showonLeft ? Spot.BottomCenter : Spot.TopCenter;
      }
      return data.showonLeft ? Spot.RightCenter : Spot.LeftCenter;
    }),
    getErrorMessagePanel(true),
    getAnnotationTextPanel(),
    // TODO annotation visibility based on settings
    new Binding('visible', '', (data: ToolNodeData): boolean => !!data.annotation),
  );

const getLabelTextPanel = (): Panel =>
  $(
    Panel,
    'Vertical',
    {
      name: 'nodeLabelPanel',
      width: 70,
    },
    $(
      TextBlock,
      {
        editable: false,

        name: 'nodeLabelTextPanel',
        opacity: 0.65,
        overflow: TextBlock.OverflowEllipsis,
        textAlign: 'center',
        wrap: TextBlock.WrapFit,
      },
      new Binding(
        'font',
        '',
        (modelData: IGlobalSettings): string => `${modelData.labelFontSize} ${modelData.font}`,
      ).ofModel(),
      new Binding('text', 'label'),
      new Binding('stroke', '', () => '#0A204A'),
    ),
  );

const getLabelPanel = (orientation: string): Panel =>
  $(
    Panel,
    'Vertical',
    {
      name: 'labelPanel',
      width: 70,
    },
    new Binding('alignment', '', (): Spot => new Spot(0.5, -0.25)),
    new Binding('alignmentFocus', '', (data: any): Spot => {
      if (orientation === 'Horizontal') {
        return data.showonLeft ? Spot.BottomCenter : Spot.TopCenter;
      }
      return data.showonLeft ? Spot.RightCenter : Spot.LeftCenter;
    }),
    getLabelTextPanel(),
    new Binding('visible', '', (data: ToolNodeData): boolean => !!data.label),
  );

export const getAnchorIcon = (node: any, anchorArrayType: string): string => {
  if (anchorArrayType === 'inputConnections') {
    if (node.multiple && node.anchorSelected) {
      return anchorIconMultipleInSelected;
    }

    if (node.multiple) {
      return anchorIconMultipleIn;
    }

    if (node.optional && node.anchorSelected) {
      return anchorIconToSelected;
    }

    if (node.optional) {
      return anchorIconToOptional;
    }

    return node.anchorSelected ? anchorIconToSelected : anchorIconTo;
  }

  if (anchorArrayType === 'outputConnections' && node.optional) {
    return anchorIconFromOptional;
  }

  if (anchorArrayType === 'outputConnections' && node.anchorSelected) {
    return anchorIconFromSelected;
  }

  return anchorIconFrom;
};

const getAnchorPanel = (orientation: string, anchorArrayType: string): Panel => {
  const isHorizontal = orientation === 'Horizontal';
  const isInAnchors = anchorArrayType === 'inputConnections';

  let alignment: Spot;
  let alignmentFocus: Spot;
  let anchorSpot: Spot;

  if (isHorizontal) {
    alignment = isInAnchors ? Spot.LeftCenter : Spot.RightCenter;
    alignmentFocus = isInAnchors ? Spot.Right : Spot.Left;
    anchorSpot = isInAnchors ? Spot.Left : Spot.Right;
  } else {
    alignment = isInAnchors ? Spot.TopCenter : Spot.BottomCenter;
    alignmentFocus = isInAnchors ? Spot.BottomCenter : Spot.TopCenter;
    anchorSpot = isInAnchors ? Spot.Top : Spot.Bottom;
  }
  return $(Panel, isHorizontal ? 'Vertical' : 'Horizontal', new Binding('itemArray', anchorArrayType), {
    alignment,
    alignmentFocus,
    itemTemplate: $(
      Panel,
      Panel.Spot,
      {
        click: (e: InputEvent, _panel: GraphObject): void => {
          const panel = _panel as Panel;
          const { model } = panel.diagram;
          const { modelData } = model;
          model.setDataProperty(modelData, 'selectedAnchor', {
            external: false,
            portId: panel.portId,
            portType: isInAnchors ? 'INPUT_CONNECTION' : 'OUTPUT_CONNECTION',
            toolId: e.targetObject.part.key,
          });
        },
        cursor: 'pointer',
        fromLinkable: !isInAnchors,
        fromSpot: anchorSpot,
        isClipping: true,
        toLinkable: isInAnchors,
        toSpot: anchorSpot,
      },
      new Binding('portId', 'portId'),
      new Binding('toMaxLinks', '', (m: any): number => (m.multiple ? Infinity : 1)),
      // the height and width here is to clip off the extra space around the icon svgs
      $(
        Shape,
        'Rectangle',
        {
          height: isHorizontal ? 11 : 10,
          width: isHorizontal ? 10 : 11,
        },
        new Binding('width', '', data => (data.anchorSelected ? 14 : 10)),
        new Binding('height', '', data => (data.anchorSelected ? 15 : 11)),
      ),
      $(
        Picture,
        {
          angle: isHorizontal ? 0 : 90,
          background: 'transparent',
          desiredSize: isHorizontal ? new Size(14, 15) : new Size(15, 14),
          height: isHorizontal ? 15 : 14,
          margin: 0,
          name: 'anchorSource',
          width: isHorizontal ? 14 : 15,
        },
        new Binding('source', '', node => getAnchorIcon(node, anchorArrayType)),
        new Binding('desiredSize', '', data => (data.anchorSelected ? new Size(18, 19) : new Size(14, 15))),
      ),
      // TODO - color may need to change w/ dark mode
      $(
        TextBlock,
        {
          alignment: isInAnchors ? new Spot(0.5, 0.5, 1, 0) : new Spot(0.5, 0.5, -1, 0),
          font: '6pt Lato',
          textAlign: 'center',
        },
        new Binding('text', 'text'),
        new Binding('stroke', '', node => (node.optional && !node.anchorSelected ? '#606F8C' : 'white')),
      ),
    ),
    name: anchorArrayType,
  });
};
const createErrorShape = (): Panel =>
  $(
    Panel,
    $(
      Shape,
      'Circle',
      {
        height: 14,
        stroke: 'transparent',
        strokeWidth: 1,
        width: 14,
      },
      new Binding('fill', 'errorBackground').ofModel(),
      new Binding('visible', '', (node: Node): boolean => {
        const { data } = node;
        return data.severity >= Severity.ERROR;
      }).ofObject(),
    ),
    $(
      Shape,
      'Triangle',
      {
        height: 13,
        stroke: 'transparent',
        strokeWidth: 1,
        width: 14,
      },
      new Binding('fill', 'errorBackground').ofModel(),
      new Binding('visible', '', (node: Node): boolean => {
        const { data } = node;
        return data.severity < Severity.ERROR;
      }).ofObject(),
    ),
  );
const createErrorIconPicture = (): Picture =>
  $(
    Picture,
    {
      background: 'transparent',
      desiredSize: new Size(15, 15),
      height: 15,
      name: 'errorIcon',
      // Initially empty source
      source: '',
      width: 15,
    },
    new Binding('source', '', (node: Node) => {
      const { data } = node;
      if (data.severity < Severity.ERROR) return alertTriangle;
      return alertCircle;
    }).ofObject(),
  );
const getErrorIconPanel = (): Panel =>
  $(
    Panel,
    'Spot',
    {
      alignment: new Spot(0.5, 0.95),
    },
    new Binding('visible', '', (node: Node): boolean => {
      const { diagram, data } = node;
      return (diagram && !diagram.model.modelData.isReadOnly && !!data.error && !!data.severity) || false;
    }).ofObject(),
    createErrorShape(),
    createErrorIconPicture(),
  );

const createSpinnerShape = (): Panel =>
  $(
    Panel,
    $(
      Shape,
      'Circle',
      {
        fill: 'transparent',
        height: 20,
        stroke: null,
        strokeWidth: 1,
        width: 20,
      },
      new Binding('visible', '', (node: Node): boolean => {
        const { diagram, data } = node;
        return (diagram && data.inFlight) || false;
      }).ofObject(),
    ),
  );
const createSpinnerIconPicture = (): Picture =>
  $(
    Picture,
    {
      angle: 0,
      background: 'transparent',
      desiredSize: new Size(20, 20),
      height: 20,
      name: 'spinner',
      // Initially empty source
      source: '',
      width: 20,
    },
    new Binding('source', '', () => spinner).ofObject(),
    new Binding('visible', '', (node: Node): boolean => {
      const { diagram, data } = node;
      return (diagram && data.inFlight) || false;
    }).ofObject(),
  );

const getSpinnerIconPanel = (): Panel =>
  $(
    Panel,
    'Spot',
    {
      alignment: new Spot(0.5, 0.5),
    },
    new Binding('visible', '', (node: Node): boolean => {
      const { diagram, data } = node;
      return (diagram && data.inFlight) || false;
    }).ofObject(),

    createSpinnerShape(),
    createSpinnerIconPicture(),
  );
const getStatusIconPanel = (): Panel =>
  $(
    Panel,
    'Spot',
    {
      alignment: new Spot(0.5, 0.95),
    },
    new Binding('visible', '', (node: Node): boolean => {
      const { diagram, data } = node;
      return (diagram && !!data.status) || false;
    }).ofObject(),
    $(
      Picture,
      {
        background: 'transparent',
        desiredSize: new Size(15, 15),
        height: 15,
        name: 'status_icon',
        width: 15,
      },
      new Binding('source', '', node => node.data.status.source).ofObject(),
    ),
  );

const createToolIdShape = (): Panel =>
  $(
    Panel,
    $(
      Shape,
      'Circle',
      {
        height: 18,
        stroke: 'transparent',
        strokeWidth: 1,
        width: 18,
      },
      new Binding('fill', 'errorBackground').ofModel(),
      new Binding('visible', '', (node: Node): boolean => {
        const { diagram } = node;
        return (diagram && diagram.model.modelData.isDebugIconsEnabled) || false;
      }).ofObject(),
    ),
  );
const createToolIdPicture = (): Picture =>
  $(
    Picture,
    {
      background: 'transparent',
      desiredSize: new Size(25, 25),
      height: 25,
      name: 'toolId',
      // Initially empty source
      source: '',
      width: 25,
    },
    new Binding('source', '', () => circle).ofObject(),
  );
const createToolIdTextBlock = (): TextBlock =>
  $(
    TextBlock,
    {
      alignment: new Spot(0.5, 0.5, 0.2, 0),
      font: '6pt Lato',
      textAlign: 'center',
    },
    new Binding('text', '', (data: ToolNodeData): string => data.key || ''),
    new Binding('stroke', 'annotationStroke').ofModel(),
  );
const getToolIdPanel = (): Panel =>
  $(
    Panel,
    'Spot',
    {
      alignment: new Spot(0.5, -0.1),
    },
    new Binding('visible', '', (node: Node): boolean => {
      const { diagram } = node;
      return (diagram && diagram.model.modelData.isDebugIconsEnabled) || false;
    }).ofObject(),
    createToolIdShape(),
    createToolIdPicture(),
    createToolIdTextBlock(),
  );
export const getNodeTemplate = (orientation: string): Node =>
  $(
    Node,
    {
      cursor: 'move',
      doubleClick: ({ diagram }: InputEvent, node: GraphObject) => beginAnnotationEdit(diagram, node.part as Node),
      isShadowed: false,
      locationObjectName: 'rectangle',
      locationSpot: Spot.Center,
      name,
      selectionAdorned: false,
      selectionChanged: (thisPart: Part) => selectionChanged(thisPart as Node),
      selectionObjectName: 'toolIconPanel',
      shadowVisible: false,
    },
    new Binding('location', 'location', Point.parse).makeTwoWay(Point.stringify),
    new Binding('opacity', '', node => {
      const { diagram, data } = node;
      if ((diagram && data?.status?.disabled) || data.isParentDisabled) {
        return OPACITY_DISABLED;
      }
      return OPACITY_ENABLED;
    }).ofObject(),
    new Binding('selectable', 'isParentDisabled', exp => !exp),
    new Binding('pickable', 'isParentDisabled', exp => !exp),
    new Binding('zOrder', 'zIndex'),
    $(
      Panel,
      Panel.Spot,
      { name: 'layoutPanel' },
      getToolIconPanel(),
      $(
        Panel,
        'Vertical',
        { alignment: new Spot(0.5, 1), alignmentFocus: Spot.TopCenter },
        getErrorMessagePanel(false),
      ),
      getAnnotationPanel(orientation),
      getLabelPanel(orientation),
      getErrorIconPanel(),
      getStatusIconPanel(),
      getSpinnerIconPanel(),
      getToolIdPanel(),
      getAnchorPanel(orientation, 'inputConnections'),
      getAnchorPanel(orientation, 'outputConnections'),
    ),
  );

const getNodeTemplateMap = (): GoMap<string, Node> => {
  const horizontalTemplate = getNodeTemplate('Horizontal');
  const verticalTemplate = getNodeTemplate('Vertical');
  const map = new GoMap<string, Node>();
  map.add(HORIZONTAL_NODE_CATEGORY, horizontalTemplate);
  map.add(VERTICAL_NODE_CATEGORY, verticalTemplate);
  return map;
};

export default getNodeTemplateMap;

This is the Text Block template -

import type { InputEvent } from 'gojs';
import {
  Adornment,
  Binding,
  GraphObject,
  Margin,
  Node,
  Panel,
  Picture,
  Placeholder,
  Point,
  Shape,
  Size,
  Spot,
  TextBlock,
} from 'gojs';

import type { TextBoxNodeData } from '../adapter/models';
import { textBoxShapeArray } from '../adapter/models/textBoxNodeData';
import { CanvasTooltipOpenedEvent, canvasTooltipTypes } from '../events';
import { propagateEvent } from '../go/propagateEvent';
import CommentHTMLInfo from '../go/textEditors/commentHTMLInfo';
import { partSelectionChanged } from '../util/templateHelpers';

import { COMMENT_CONFIG_PANEL_BUTTON, OPACITY_DISABLED, OPACITY_ENABLED } from './constants';
import moreVertical from './icons/MoreVertical.svg';

const { make: $ } = GraphObject;

const name = 'textBoxNode';

const tbPadding = 15;
const shapePadding = 1;

const textBoxStyles = {
  bold: 1,
  italic: 2,
  none: 0,
  strikeout: 8,
  underline: 4,
} as const;

const textBoxJustification = {
  BottomCenter: 7,
  BottomLeft: 6,
  BottomRight: 8,
  Center: 1,
  Left: 0,
  Right: 2,
  TopCenter: 4,
  TopLeft: 3,
  TopRight: 5,
} as const;

export const getFont = (node: TextBoxNodeData): string => {
  const bold = node.fontStyle === textBoxStyles.bold ? 'bold' : 'normal';
  const italic = node.fontStyle === textBoxStyles.italic ? 'italic' : 'normal';
  return `${italic} normal ${bold} ${node.fontSize}pt ${node.fontName}`;
};

type TAlignment = {
  // should allow emptyString?
  textAlign: 'left' | 'center' | 'right' | '';
  // should allow emptyString?
  verticalAlign: 'Top' | 'Center' | 'Bottom' | '';
};

const getJustification = (justification: number): TAlignment => {
  switch (justification) {
    case textBoxJustification.Left:
      return { textAlign: 'left', verticalAlign: 'Center' };
    case textBoxJustification.Center:
      return { textAlign: 'center', verticalAlign: 'Center' };
    case textBoxJustification.Right:
      return { textAlign: 'right', verticalAlign: 'Center' };
    case textBoxJustification.TopLeft:
      return { textAlign: 'left', verticalAlign: 'Top' };
    case textBoxJustification.TopRight:
      return { textAlign: 'right', verticalAlign: 'Top' };
    case textBoxJustification.TopCenter:
      return { textAlign: 'center', verticalAlign: 'Top' };
    case textBoxJustification.BottomLeft:
      return { textAlign: 'left', verticalAlign: 'Bottom' };
    case textBoxJustification.BottomCenter:
      return { textAlign: 'center', verticalAlign: 'Bottom' };
    case textBoxJustification.BottomRight:
      return { textAlign: 'right', verticalAlign: 'Bottom' };
    default:
      // maybe default should be the same as TopLeft? instead of empty strings?
      return { textAlign: '', verticalAlign: '' };
  }
};

export const getTextBoxWidth = ({ width }: TextBoxNodeData): number => width - tbPadding;

export const getShape = ({ shape }: TextBoxNodeData): string => textBoxShapeArray[shape];

export const getShapeWidth = ({ width }: TextBoxNodeData): number => width - shapePadding;

export const getShapeHeight = ({ height }: TextBoxNodeData): number => height - shapePadding;

export const getTextAlign = ({ justification }: TextBoxNodeData): string => getJustification(justification).textAlign;

export const getVerticalAlignment = ({ justification }: TextBoxNodeData): Spot => {
  const verticalAlignment = getJustification(justification).verticalAlign;
  switch (verticalAlignment) {
    case 'Bottom': {
      return Spot.Bottom;
    }
    case 'Center': {
      return Spot.Center;
    }
    case 'Top': {
      return Spot.Top;
    }
    default:
      return Spot.Top;
  }
};

export const getStrikethrough = ({ fontStyle }: TextBoxNodeData) => fontStyle === textBoxStyles.strikeout;

export const getUnderline = ({ fontStyle }: TextBoxNodeData) => fontStyle === textBoxStyles.underline;

const getFillColor = ({ shape, fillColor }: TextBoxNodeData) =>
  textBoxShapeArray[shape] === 'None' ? 'transparent' : fillColor;

const getOutline = ({ shape }: TextBoxNodeData) => (textBoxShapeArray[shape] === 'None' ? 'Transparent' : 'Black');

const getBackgroundShape = () =>
  $(
    Shape,
    { background: 'transparent', name: 'textBoxBackground' },
    new Binding('width', '', getShapeWidth),
    new Binding('height', '', getShapeHeight),
    new Binding('fill', '', getFillColor),
    new Binding('figure', '', getShape),
    new Binding('stroke', '', getOutline),
  );

const getBackgroundImage = () =>
  $(
    Picture,
    { imageStretch: GraphObject.Uniform, name: 'bgImage' },
    new Binding('visible', '', (data: any) => !!data.bgImage),
    new Binding('source', 'bgImage'),
    new Binding('width', 'width'),
    new Binding('height', 'height'),
  );

const getText = () =>
  $(
    TextBlock,
    {
      background: 'transparent',
      editable: true,
      margin: new Margin(2, 5, 0, 5),
      name: 'textBoxTextBlock',
      // This just propagates an event to the dc-client to populate a texteditor
      textEditor: new CommentHTMLInfo(),
    },
    // bind text to model and allow editing mode to update the text (two-way)
    new Binding('text', 'text').makeTwoWay(),
    new Binding('width', '', getTextBoxWidth),
    new Binding('height', 'height'),
    new Binding('stroke', 'textColor'),
    new Binding('font', '', getFont),
    new Binding('isUnderline', '', getUnderline),
    new Binding('isStrikethrough', '', getStrikethrough),
    new Binding('textAlign', '', getTextAlign),
    new Binding('verticalAlignment', '', getVerticalAlignment),
  );

const getCommentConfigPanelButton = () =>
  $(
    Panel,
    'Vertical',
    { alignment: Spot.Right, alignmentFocus: Spot.Left },
    $(
      'Button',
      {
        'ButtonBorder.fill': 'rgba(0,0,0,0)',
        'ButtonBorder.stroke': 'rgba(0,0,0,0)',
        'ButtonBorder.strokeWidth': 0,
        // eslint-disable-next-line prettier/prettier
        '_buttonFillOver': 'rgba(0,0,0,0)',

        // eslint-disable-next-line prettier/prettier
        '_buttonFillPressed': 'rgba(0,0,0,0)',
        mouseEnter: (e: InputEvent, thisObj: GraphObject) => {
          propagateEvent(
            e.diagram.div.id,
            new CanvasTooltipOpenedEvent(e, thisObj.diagram.div, canvasTooltipTypes.COMMENT_TOOLTIP),
          );
        },
        name: COMMENT_CONFIG_PANEL_BUTTON,
      },
      // TODO: Dark Mode Color Scheme
      // new Binding('ButtonBorder.stroke', 'toolProgressStroke').ofModel(),
      // new Binding('ButtonBorder.fill', 'groupBackground').ofModel(),
      $(
        Picture,
        { cursor: 'pointer', desiredSize: new Size(12, 12), height: 12, width: 12 },
        new Binding('source', '', () => moreVertical),
      ),
    ),
  );

const getTextBoxTemplate = (): Node =>
  $(
    Node,
    {
      background: 'transparent',
      cursor: 'move',
      locationObjectName: name,
      name,
      resizable: true,
      selectionAdornmentTemplate: $(
        Adornment,
        'Spot',
        $(
          Panel,
          'Auto',
          $(
            Shape,
            'Rectangle',
            {
              fill: null,
              strokeDashArray: [4, 2],
              strokeWidth: 2,
            },
            new Binding('stroke', 'selectionColor').ofModel(),
          ),
          $(Placeholder),
        ),
        getCommentConfigPanelButton(),
      ),
      selectionChanged: partSelectionChanged,
    },
    new Binding('position', 'location', Point.parse).makeTwoWay(Point.stringify),
    new Binding('width', 'width').makeTwoWay(),
    new Binding('height', 'height').makeTwoWay(),
    new Binding('opacity', 'isParentDisabled', exp => (exp ? OPACITY_DISABLED : OPACITY_ENABLED)),
    new Binding('selectable', 'isParentDisabled', exp => !exp),
    new Binding('pickable', 'isParentDisabled', exp => !exp),
    new Binding('zOrder', 'zIndex'),
    getBackgroundShape(),
    getBackgroundImage(),
    getText(),
  );

export default getTextBoxTemplate;

I wonder if this has something to do with the fact that when we add a Text Block we make it immediately editable.

This is our custom text editor for Text Blocks -

import type { Diagram, GraphObject, Tool } from 'gojs';
import { HTMLInfo } from 'gojs';

import type { TextBoxNodeData } from '../../adapter/models';
import { CommentTextEditorOpenedEvent } from '../../events';
import { isTextBlock } from '../../util/partIsType';
import { propagateEvent } from '../propagateEvent';

export default class CommentHTMLInfo extends HTMLInfo {
  constructor() {
    super();

    this.mainElement = null;
  }

  show = (textBlock: GraphObject, diagram: Diagram, tool: Tool) => {
    if (!diagram || !diagram.div) return;
    if (!isTextBlock(textBlock)) return;
    // if (this.tool !== null) return; // Only one at a time.

    // this.tool = tool as TextEditingTool;

    textBlock.panel.part.opacity = 0;
    textBlock.panel.part.isSelected = false;
    propagateEvent(
      diagram.div.id,
      new CommentTextEditorOpenedEvent(textBlock, diagram.div, textBlock.part.data as TextBoxNodeData),
    );
    tool.doCancel();
  };

  hide = () => {};

  valueFunction = () => '';
}

Your templates are fairly complex, so I may have missed some details.

Does each of the node data Objects in the Array that you pass to Model.addNodeDataCollection have a “category” property specifying the node template that it wants to use as its representation as a Node? If the property is undefined or if it is not a known template name, it uses the default template, which is named by the empty string.

Also if you don’t want to use the property name “category”, set Model.nodeCategoryProperty.
Model | GoJS API