Multiple Unique Instance for a Node

I am using a palette and a diagram with Angular. Both are having their own model as below:

Palette Model Template

palette.nodeTemplateMap.add(‘Question’,

$(go.Node, 'vertical', nodeStyle(),

$(go.Panel, 'Auto',

$(go.Shape, 'Rectangle',

  {

    width: 55,

    height: 30,

    stroke: null

  },

  new go.Binding('fill', 'color')

),

$(go.TextBlock, { margin: 8,

  maxSize: new go.Size(160, NaN),

  wrap: go.TextBlock.WrapFit },

  new go.Binding('text')))

));

Diagram Model Template:

dia.nodeTemplateMap.add(‘Question’,

$(go.Node, 'vertical', nodeStyle(),

$(go.Panel, 'Spot',

$(go.Shape, 'Rectangle',

  {

    width: 100,

    height: 40,

    stroke: null

  },

  new go.Binding('fill', 'color')

),

$(go.TextBlock, { margin: 8,

  maxSize: new go.Size(160, NaN),

  wrap: go.TextBlock.WrapFit,

  editable: true },

  new go.Binding('text').makeTwoWay()),

  // Ports

  makePort('t', go.Spot.TopCenter, true, true),

  makePort('l', go.Spot.LeftCenter, true, true),

  makePort('r', go.Spot.RightCenter, true, true),

  makePort('b', go.Spot.BottomCenter, true, true)

)));

and below is initial Palette NodeData for Question:

{ text: ‘Question’, category: ‘Question’, color: ‘#85C1E9’, data:

  { questionText: ' ',

  placeholder: 'Enter question text',

  answerType: [

      {type: 'SingleOption', isSelected: false},

      {type: 'MultipleOptions', isSelected: false},

      {type: 'SingleLineText', isSelected: false},

      {type: 'MultiLineText', isSelected: false},

      {type: 'Date', isSelected: false}

    ],
  helpUrl: ' ',

  isMandatoryAnswer: true,

  displayText: ' ' } }

Now as you can see here that Node is having custom data property which I am using to show as a popup data once I drag the node from the Palette to Diagram. The issue which I am facing here is when I am updating one of the properties inside the node’s data object say(questionText) it’s getting replicated in other nodes as well if there is any same node present in the diagram.

Below is the drag event which I am using inside ngAfterViewInit():

this.myDiagramComponent.diagram.addDiagramListener(‘ChangedSelection’, function(e) {

  if (e.diagram.selection.count === 0) {

    appComp.selectedNode = null; // or this.selectedNode = null;

  }

  const node = e.diagram.selection.first();

  if (node instanceof go.Node) {

    appComp.selectedNode = node; // or this.selectedNode = node;

    
  }

});

Below is sample HTML code which I am using to bind data on popup and this is poping up once I am clicking a node:

  <fieldset>

    <legend>Basic</legend>

    Question Text

  <bt-inputtext [(ngModel)]="selectedNode?.data.data.questionText" placeholder="Enter question text"></bt-inputtext>

Issues I am facing with this way are:

  • Once I update any item(say questionText) for one of the node it is getting updated for all of the nodes even though boh the nodes are having unique key with them.

  • Have seen that palette nodeData is getting updated as well if I am updating any of the field of data property inside a node in my diagram.

Is two way binding is an issue here? Or I am missing something. If copyNodeDataFucntion is an option here please suggest how to add this function in angular(didn’t get proper reference).

@walter Please help here.

Sorry, I cannot answer while I am sleeping.

Yes, I think you have identified the problem. The default data copying function only does a shallow copy.

So the first question is why you need to have a data.data property. Could you just move up those nested properties up to the node data object? If you can, that would simplify your code too.

But that does leave you with the “answerType” property which is an Array. Do you expect to have that Array be different for different nodes? If not, then it’s OK to share that Array. Otherwise you will need to copy that Array when the node is copied.

One way to do that when the “answerType” property is not nested is to set the Model.copiesArrays property and the Model.copiesArrayObjects property to true.

But if the Array is on a nested Object, or if you have some Arrays that you do want to keep shared, then setting those two properties is insufficient and you will need to provide your own custom data copying function: Model | GoJS API

Thanks for the response Walter.

In my case, you can consider data inside Node as an angular model as below:

export class Question{

questionText: string;

placeholder: string;

answerType: Answer[];

helpUrl: string;

isMandatoryAnswer: boolean;

displayText: string;

}

export class Answer{

type: string;

isSelected: boolean;

}

I have initialized and object as below:

questionNodeData: Question = {
questionText: ’ ',

placeholder: 'Enter question text',

answerType: [

  {type: 'SingleOption', isSelected: false},

  {type: 'MultipleOptions', isSelected: false},

  {type: 'SingleLineText', isSelected: false},

  {type: 'MultiLineText', isSelected: false},

  {type: 'Date', isSelected: false}

],
helpUrl: ' ',

isMandatoryAnswer: true,

displayText: ' '

};

Then I tried assigning it to node data as below:

{ text: ‘Question’, category: ‘Question’, color: ‘#85C1E9’, data: this.questionNodeData}

I have tried copyArrays and copyArrayObjects value as true but facing similar issue. Multiple instances of the node inside Diagram still having the same value for the question custom model.

Please suggest a code snippet/sample if any available for copuNodeDataFunction. Should i use it inside ngAfterViewInIt() or it will be a separate function?? How i will pass argument to the function??

I was trying as below:

let dia = $(go.Diagram, {

  'undoManager.isEnabled': true,

  model: $(go.GraphLinksModel,

    {

      nodeDataArray: [],

      linkDataArray: [],

      linkToPortIdProperty: 'toPort',

      linkFromPortIdProperty: 'fromPort',

      linkKeyProperty: 'key',

      makeUniqueKeyFunction: generateGuid, // I have a function to generate Guid

       copyNodeDataFunction: copyNodeData
    }

  )

});

//My Guid Function
function generateGuid(): string {

let k = Guid.create();

return k.toString();

}

Similar as per syntax of copyNodeDataFucntion I tried as below:

function copyNodeData() : any {
// assign values
}

but as per syntax of copyNodeDataFunction it accepts data and model as argument. Here comes my doubt from where i should call this method so that i can pass model and data as well. Is this method will get called whenver a new instance of node is getting dragged to the diagram?

Why can’t you have each node data object be an instance of Question?

But never mind that – if you really want the data to be structured the way it is, you just need to set Model.copyNodeDataFunction each time you create or load a model. That function needs to return a new Object with whatever properties you want, which seems to include a copy of Question and a copy of the answerType Array and its items.

Something like:

  const model = ...;
  model.copyNodeDataFunction = function(obj) {
      var copy = {};
      copy.text = obj.text;
      copy.category = obj.category;
      copy.color = obj.color;
      copy.data = {
        questionText: obj.data.questionText,
        placeholder: obj.data.placeholder,
        answerType: ... copy the Array with copies of its items ...,
        helpUrl: obj.data.helpUrl,
        isMandatoryAnswer: obj.data.isMandatoryAnswer,
        displayText: obj.data.displayText
      };
      return copy;
    };
  myDiagram.model = model;

Kept node data Object as instance of Question. Further getting error as below while using function mentioned by you:

var copy = {};

  copy.text = obj.text;

  copy.category = obj.category;

  copy.color = obj.color;

It says text/category/color doesn’t exist on type {}. Is there a different way to initialize empty object in typescript? Moreover on doubt will this function execute every time when I drag a node from pallete to diagram irrespective of node category.

Well, you could do something like:

var copy = { ...obj };
copy.data = copyQuestion(obj.data);
return copy;

Thanks much walter. Able to proceed :)