diagram.model.removeLinkData not working

Hi,
I have finally an issue I can’t overcome even with docs so I need to ask here. In my application, I have functionality that allows the user to drop the diagram node on another node to create a connection. I have two types of nodes - conditions and rules - so to make a valid and correct diagram, when a condition is dropped on a rule it is not added after it, but between a direct parent to rule and rule itself. I have a function which handles it:

const addNodeBetweenDestinationAndParent = (
	draggedNode: Node,
	destinationNode: Node,
	diagram: any
) => {
	const parentLink: Link | null = destinationNode.findTreeParentLink();
	const parentNode: Node | null = destinationNode.findTreeParentNode();
	if (parentLink && parentNode) {
		if (parentNode.data.key === draggedNode.data.key) return;
		diagram.model.addLinkData({ from: parentNode.data.key, to: draggedNode.data.key });
		diagram.model.addLinkData({ from: draggedNode.data.key, to: destinationNode.data.key });
		diagram.model.removeLinkData(parentLink.data);
	} else {
		diagram.model.addLinkData({ from: draggedNode.data.key, to: destinationNode.data.key });
	}
};

I have a functionality that allows editing a diagram already saved to the database. When the diagram is created it works fine, but when a diagram is edited - taken from API and converted from JSON to gojs format - there is one issue. There is a problem with removeLinkData function. It seems like it does nothing, the link is not removed. I put console.log(diagram.model.containsLinkData(parentLink.data)); before and after and I received true and false respectively like it was removed, but it is still there. I tried to find a solution and I checked this issue here: Sometimes diagram.model.removeLinkData(linkdata) not working and I tried to change my code to:

const addNodeBetweenDestinationAndParent = (
	draggedNode: Node,
	destinationNode: Node,
	diagram: any
) => {
	const parentLink: Link | null = destinationNode.findTreeParentLink();
	const parentNode: Node | null = destinationNode.findTreeParentNode();
	if (parentLink && parentNode) {
		if (parentNode.data.key === draggedNode.data.key) return;
		console.log(diagram.model.containsLinkData(parentLink.data));

		diagram.startTransaction('removing links');
		diagram.model.addLinkData({ from: parentNode.data.key, to: draggedNode.data.key });
		diagram.model.addLinkData({ from: draggedNode.data.key, to: destinationNode.data.key });
		diagram.removeParts(new Set<ObjectData>().add(parentLink));
		diagram.commitTransaction('removing links');

		console.log(diagram.model.containsLinkData(parentLink.data));
		console.log('data', diagram.model.linkDataArray);
	} else {
		diagram.model.addLinkData({ from: draggedNode.data.key, to: destinationNode.data.key });
	}
};

But it didnt help, it still does not work as expected

All model changes should occur within a transaction, but if your addNodeBetweenDestinationAndParent function is being called in some kind of “drop” event handler or listener, then a transaction was already started before, so you don’t need to. Which would explain why that made no difference. (But if it had, you’d also need to put the “else” case of calling addLinkData within a transaction. The documentation will state whether an event handler is called within a transaction or not.)

The Flowgrammer sample, Flowgrammer , demonstrates splicing a node “into” a link. See the dropOntoLink function. But that function is different from yours because it modifies the existing link and just adds one link data, rather than adding two link data and removing the old one.

I cannot explain the behavior that you are observing. How can you tell that “it is still there”? Does the original Link still exist in the Diagram? Can the user see it?
Which version of GoJS are you using?

This is an example diagram from my application.
24

This is a diagram after I dropped ‘OR’ condition on Date of birth. Link that connects ‘Date of Birth’ and ‘AND’ should be deleted. console.log(diagram.model.containsLinkData(parentLink.data)); returns false so technically it is removed from array but I still see it here.

What is interesting and I mentioned it already before, I loaded a diagram from the database to edit it, it was already saved. But now when I add a completely new rule after ‘AND’ and then I drop an ‘OR’ condition on that it works as expected, so ‘OR’ condition is added between ‘AND’ and rule and a link connecting both is deleted.

I am using latest version of gojs 2.1.6 and 1.0.5. of gojs-react.

I assume some ids are messed up, but I couldn’t find anything I could start debugging with. I will also try to implement a solution from flowgrammer, maybe this will help.

I added a solution from flowgrammer but it still does not work for me. This is strange, my part of code representing relinking looks like this:

diagram.startTransaction('adding node between destination and parent');
const it = new List<ObjectData>().addAll(destinationNode.findLinksInto()).iterator;
while (it.next()) {
	const link = it.value;
	console.log(link.data);
	link.toNode = draggedNode;
	console.log(link.data);
	console.log(diagram.model.linkDataArray);
}
// diagram.model.addLinkData({ from: parentNode.data.key, to: draggedNode.data.key });
diagram.model.addLinkData({ from: draggedNode.data.key, to: destinationNode.data.key });

diagram.commitTransaction('adding node between destination and parent');

The newly dropped condition is linked to a destination node, but is not linked to its parent:
38

I also added a few console logs to see what is going on there, here is the result:
05

So first, before the relinking link is connected from AND to rule - which is correct, after the change link is connected from AND to OR which is also correct, but when I put a whole linkDataArray this link is missing but an incorrect link is added instead - from OR to rule. I don’t understand why this is happening here.

Also, I put a breakpoint in chrome debugger to check what is going on step by step and apparently this function works correctly, linkDataArray has expected links inside, so it may be something deeper inside library itself that changes its state.

I’m wondering if the Model.toIncrementalData or Model.mergeLinkDataArray is broken in this case. We’ll investigate.

Here’s a sample that I think does what you are asking for:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
  <script src="https://unpkg.com/gojs"></script>

  <script>
    function init() {
      var $ = go.GraphObject.make;

      // initialize main Diagram
      myDiagram =
        $(go.Diagram, "myDiagramDiv",
          {
            layout: $(go.LayeredDigraphLayout),
            "undoManager.isEnabled": true
          });

      myDiagram.nodeTemplate =
        $(go.Node, "Auto",
          new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify),
          $(go.Shape,
            {
              fill: "white", stroke: "gray", strokeWidth: 2,
              portId: "", fromLinkable: true, toLinkable: true,
              fromLinkableDuplicates: true, toLinkableDuplicates: true
            },
            new go.Binding("stroke", "color")),
          $(go.Panel, "Vertical",
            $(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
              },
              new go.Binding("text").makeTwoWay()),
            $(go.Shape,
              {
                alignment: go.Spot.Bottom, stretch: go.GraphObject.Horizontal,
                height: 4, fill: "gray", strokeWidth: 0
              },
              new go.Binding("fill", "color"))
          )
        );

      myDiagram.linkTemplate =
        $(go.Link,
          $(go.Shape, { isPanelMain: true, strokeWidth: 10, stroke: "transparent" }),
          $(go.Shape, { isPanelMain: true, strokeWidth: 1.5 }),
          $(go.Shape, { toArrow: "Standard" }),
          {
            mouseDragEnter: function(e, link) { link.elt(0).stroke = "magenta"; },
            mouseDragLeave: function(e, link) { link.elt(0).stroke = "transparent"; },
            mouseDrop: function(e, link) { spliceNodeIntoLink(link); }
          }
        );
      
      function spliceNodeIntoLink(link) {
        // assume just a single Node to be spliced...
        var node = link.diagram.selection.first();
        if (!(node instanceof go.Node)) return;
        // the argument had better be a Link connecting two Nodes
        if (!(link instanceof go.Link)) return;
        var from = link.fromNode;
        var to = link.toNode;
        if (!from || !to) return;
        // cannot splice a Node into a Link that is already connecting with that Node
        if (from === node || to === node) return;
        link.diagram.commit(function(diag) {
          // make sure the node has no connected links already
          diag.removeParts(node.findLinksConnected());
          var m = diag.model;
          m.removeLinkData(link.data);
          m.addLinkData({ from: from.key, to: node.key });
          m.addLinkData({ from: node.key, to: to.key });
        }, "spliced node into link");
      }

      // initialize Palette
      myPalette =
        $(go.Palette, "myPaletteDiv",
          {
            nodeTemplateMap: myDiagram.nodeTemplateMap
          });

      // now add the initial contents of the Palette
      myPalette.model.nodeDataArray = [
        { text: "blue node", color: "blue" },
        { text: "orange node", color: "orange" }
      ];

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

      load();
    }

    // save a model to and load a model from Json text, displayed below the Diagram
    function save() {
      var str = myDiagram.model.toJson();
      document.getElementById("mySavedModel").value = str;
    }
    function load() {
      var str = document.getElementById("mySavedModel").value;
      myDiagram.model = go.Model.fromJson(str);
    }
  </script>
</head>
<body onload="init()">
  <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: whitesmoke; border: solid 1px black"></div>
      <div id="myOverviewDiv" style="margin: 2px 0 0 0; width: 100px; height: 100px; background-color: lightgray; border: solid 1px black"></div>
    </div>
    <div id="myDiagramDiv" style="flex-grow: 1; height: 400px; border: solid 1px black"></div>
  </div>
  <div id="buttons">
    <button id="loadModel" onclick="load()">Load</button>
    <button id="saveModel" onclick="save()">Save</button>
  </div>
  <textarea id="mySavedModel" style="width:100%;height:200px">
{ "class": "go.GraphLinksModel",
  "linkKeyProperty": "key",
  "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>
</body>
</html>

This seems to work well, but it does not use gojs-react. We’ll look into that next.

EDIT: merging seems to work well too. And Model.toIncrementalData looks like it is producing the correct data structures.

So I cannot explain the behavior that you are getting. Does any object in your props have any “__gohashid” properties? Those should not have been copied. They are not copied by Model.toIncrementalData or Model.toIncrementalJson or Model.toJson, and they are ignored by Model.mergeNodeDataArray and Model.mergeLinkDataArray and Model.applyIncrementalJson, but external code could have done so, which will cause problems if GoJS sees those objects, because GoJS uses that property for internal bookkeeping.

Hi walter, I just wanted to let you know that I have finally found the issue. It was a completely different piece of code that caused problems. To give a short explanation I can tell that in my application I have a graph editor which is a part of a bigger form and I need to react on every change. Data model is not updated automatically and I need to use modelChanges to update the model by myself. The function looks like this:

export const applyChangesToClassificationModel = (
	previousModel: IDataModel,
	modelChanges: IncrementalData
): IDataModel => {
	return produce(previousModel, draft => {
		if (isGraphStructureChanging(modelChanges)) {
			insertNodes(draft, modelChanges);
			insertLinks(draft, modelChanges);
			removeNodes(draft, modelChanges);
			removeLinks(draft, modelChanges);
		} else {
			updateNodeValues(draft, modelChanges);
			updateLinkValues(draft, modelChanges);
		}
	});
};

In short, I check if structure has changed, if so I have a few functions that iterate over inserted and removed nodes and links and update the model. However after debugging I found out that some on link keys are wrong, it used to be fine for creating new model, but editing existing one has different link keys than expected. I found out that I use keys for links that are automatically added by library itself. I used to have a simple function like this:

export const buildLink = (fromNode: string, toNode: string): ILink => {
	return { from: fromNode, to: toNode };
};

I have changed to this one, so I have control over how keys are generated and have no issue with editing graph:

export const buildLink = (fromNode: string, toNode: string): ILink => {
	return { from: fromNode, to: toNode, key: uuid() };
};

So issue is finally gone. Thank you for your help.