Gojs-angular's gojs-diagram component's modelChange event causes infinite change detection loop

I’m using the gojs-angular library in an Angular application. There is a modelChange event handler for the gojs-diagram component which fires on changes in the diagram. In the handler function I’m using the DataSyncService provided to make sure the state remains immutable and I’m updating the application’s state with the changes from GoJS diagram. It has been working fine until I needed to store link points. Now it causes an infinite change detection cycle where when the application state is changed the gojs-diagram’s modelChange fires again and again. Would you be able to suggest what may be causing that?

I’m not sure what would be causing that. Do you have a two-way data binding on Link points?

Can you determine what the specific model change is, that is causing the loop?

Yes, new Binding(“points”).makeTwoWay(). Is it true that the points are being constantly updated while the link is being drawn? In that case it could be triggering modelChange event.

Yeah. You may need to remove that two-way binding, and instead save them some other way that is safer for your particular immutability needs.

Any suggestions how to do it the best way? I can confirm when I remove the points binding there is no infinite cycle.

If the points are constantly updated during the process of link drawing and the skipsDiagramUpdate is set to true, shouldn’t it prevent the modelChange to be triggered by diagram changes?

How would one not keep points in the app state but store them when the model is saved to the back end as well as restore them from the back end on load?

I think the general problem is that Link points get updated in lots of different ways: During link drawing, during animations, during layouts, during Node moves, etc. If you have to be very careful about their book-keeping because you have potentially other sources of truth, it’s probably the most difficult property to contend with.

You could try manually saving the Link.points when you save the Diagram model. Right before model save you’d want to do something like:

const linkDataArray = myDiagram.model.linkDataArray;
for (const x of linkDataArray) {
  myDiagram.model.set(x, 'points', myDiagram.findLinkForData(x).points)
}

That should work, I think

And when loading? In the LayoutCompleted strip off the points property from all the link objects?

I wonder why the modelChange event keeps firing even though it should only do that when isTransactionFinished is true. If transaction is finished, doesn’t it mean the link has already been drawn?

I tried it. First it complained it has to be within a transaction. I made it within a commit function and the loop started again as soon as it got updated. Isn’t it a bug in GoJS?

It’s possible, or a bug in gojs-angular. We’ll have to investigate.

What versions are you using for GoJS and Angular?

Using Angular 20 and GoJS 3.0.8 (what the gojs-angular-basic project was last updated with) and adding a two-way binding on ‘points’, I didn’t encounter any issues. The diagramModelChange listener does not encounter any loops. But my example may be too simple to replicate what you’re doing.

Angular: 19.2.14, gojs-angular: 2.1.2, gojs: 3.0.24

It seems like I’ll need more to be able to reproduce it. At least, adding a two-way binding on points and inspecting the changes in the gojs-angular-basic sample does not cause any loop to happen. Can you modify that sample, or tell me more about your setup so that I can, until I can get a reproduction?

My suspicion is that something else is triggering the loop, but I can’t really guess at what.

https://codesandbox.io/p/devbox/hlyyts

I see, thank you.

The problem seems to be from the go.List in the Link data that we are not dealing with (I suspect the Angular code doesn’t deal with any Object because it has no way to compare values within)

So one simple solution would be to be sure you are not saving a go.List<go.Point> in your Link data, but instead always saving something that you can compare against simply, like a string. As an example, here’s a points binding that takes a data value in the format "x, y, x, y, x, y" and also has the two-way back-converter to turn a go.List<go.Point> into that string.

      new Binding('points', 'points', (val) => val.split(', ').map(Number)).makeTwoWay(
        (pts: go.List<Point>) =>
          pts
            .toArray()
            .flatMap((pt: Point) => [pt.x, pt.y])
            .join(', ')
      ),

This code is a little absurd in terms of excess data structures (you probably should just build a string for efficiency instead) but I hope you get the idea of what it’s doing.

If you bind like this, the infinite loop should get resolved.

As an alternative, you maybe could modify the gojs-angular’s syncLinkData to be smarter. And maybe we should do that just to special case points. But I think you will run into this for anything in the node or link data that you do not serialize, in general.

Thank you Simon. This indeed fixed the problem. Definitely it would be great to have this built into the DataSyncService because, as you mentioned, it would apply to many more cases like this which means they all have to use the same workaround which isn’t the best solution.

Yes. For most simple cases like Point and Size, you can use our built-in string conversion to avoid the issue, like

.bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify)

But anything more complex and right now you have to handle it yourself.