Angular, undo is not working when node data contains arrays

Hi,
I have an app that is using gojs-angular components and is based on gojs-angular-basic demo (https://github.com/NorthwoodsSoftware/gojs-angular-basic). I want my node data to contain arrays which would specify previous and next nodes that are linked to given node.

  public diagramNodeData = [
    {
      id: 1,
      key: "1",
      body: "Alpha",
      prev: [],
      next: [2],
    },
    {
      id: 2,
      key: "2",
      body: "Beta",
      prev: [1],
      next: [],
    },
  ];

The problem I have is that undo works only if I change position of node with mouse. It does not work in situations such as changing text in TextBlock, copying nodes or dropping external objects from palette. When I remove prev and next arrays from objects in diagramNodeData everything seems to work just fine. diagramNodeData is synchronized with diagram nodes by DataSyncService provided by gojs-angular.

Here is my node template:

dia.nodeTemplate = $(
      go.Node,
      "Spot",
      $(
        go.Panel,
        "Auto",
        $(go.Shape, "RoundedRectangle", { stroke: null, fill: "lightblue" }),
        $(
          go.TextBlock,
          { margin: 8, editable: true },
          new go.Binding("text", "body").makeTwoWay()
        )
      ),
      // Ports
      makePort("t", go.Spot.TopCenter),
      makePort("l", go.Spot.Left),
      makePort("r", go.Spot.Right),
      makePort("b", go.Spot.BottomCenter)
    );

Thank you in advance for your help.

Have you set Model.copiesArrays to true in your initDiagram function?
https://gojs.net/latest/api/symbols/Model.html#copiesArrays

(If your array items are not just numbers or strings but might be Objects, you might also want to set this to true: https://gojs.net/latest/api/symbols/Model.html#copiesArrayObjects )

I have set it, sadly it didn’t solve my problem. Here is the minimal version of my component. Honestly I have no idea what is wrong.

export class AppComponent implements OnInit, AfterViewInit {
  @ViewChild("myDiagram", { static: true })
  public myDiagramComponent: DiagramComponent;

  public diagramNodeData = [
    {
      key: "1",
      id: 1,
      body: "Alpha",
      prev: [],
      next: [2],
    },
    {
      key: "2",
      id: 2,
      body: "Beta",
      prev: [1],
      next: [],
    },
  ];
  public diagramLinkData: Array<go.ObjectData> = [];
  public diagramDivClassName: string = "myDiagramDiv";
  public diagramModelData = { prop: "value" };

  public observedDiagram = null;

  constructor(private cdr: ChangeDetectorRef) {}

  ngOnInit() {}

  // initialize diagram / templates
  public initDiagram(): go.Diagram {
    const $ = go.GraphObject.make;
    const dia = $(go.Diagram, {
      "undoManager.isEnabled": true,
      model: $(go.GraphLinksModel, {
        linkToPortIdProperty: "toPort",
        linkFromPortIdProperty: "fromPort",
        linkKeyProperty: "key", // IMPORTANT! must be defined for merges and data sync when using GraphLinksModel,
        copiesArrays: true,
        copiesArrayObjects: true,
      }),
    });

    const makePort = function (id: string, spot: go.Spot) {
      return $(go.Shape, "Circle", {
        opacity: 0.5,
        fill: "gray",
        strokeWidth: 0,
        desiredSize: new go.Size(8, 8),
        portId: id,
        alignment: spot,
        fromLinkable: true,
        toLinkable: true,
      });
    };

    // define the Node template
    dia.nodeTemplate = $(
      go.Node,
      "Spot",
      $(
        go.Panel,
        "Auto",
        $(go.Shape, "RoundedRectangle", { stroke: null, fill: "lightblue" }),
        $(
          go.TextBlock,
          { margin: 8, editable: true },
          new go.Binding("text", "body").makeTwoWay()
        )
      ),
      // Ports
      makePort("t", go.Spot.TopCenter),
      makePort("l", go.Spot.Left),
      makePort("r", go.Spot.Right),
      makePort("b", go.Spot.BottomCenter)
    );

    return dia;
  }

  // When the diagram model changes, update app data to reflect those changes
  public diagramModelChange = function (changes: go.IncrementalData) {
    this.diagramNodeData = DataSyncService.syncNodeData(
      changes,
      this.diagramNodeData
    );
    this.diagramLinkData = DataSyncService.syncLinkData(
      changes,
      this.diagramLinkData
    );
    this.diagramModelData = DataSyncService.syncModelData(
      changes,
      this.diagramModelData
    );
  };

  public ngAfterViewInit() {
    if (this.observedDiagram) return;
    this.observedDiagram = this.myDiagramComponent.diagram;
    this.cdr.detectChanges(); // IMPORTANT: without this, Angular will throw ExpressionChangedAfterItHasBeenCheckedError (dev mode only)
  } // end ngAfterViewInit
}

(fyi, we have been looking into this, but we are still figuring out the problem)

Hello,

Thank you for reporting this issue. I have just published gojs-angular 1.0.3, which should offer the tools to handle this situation.

All you’ll need to do is:

  • Get the latest gojs-angular package using npm i gojs-angular@1.0.3
  • Create a skipsDiagramUpdate flag in your app component, and pass it to your Diagram Component (see the updated gojs-angular-basic sample for how to do this)
  • Set that flag to true when an update happens that GoJS already has (often a model update). For example your diagramModelChange function should probably set this flag to true. Events that change Angular data but are not already seen by GoJS should set that flag to false, such as editing node data from an Inspector (again, see the updated gojs-angular-basic sample for an example of that)

Please let me know if you have questions, or run into any more problems.

Thanks for answer!

I still have one problem - when I undo after dropping a node from palette (when undoing ExternalObjectsDropped event) representation of that node in diagramNodeData is not removed. Is it something I should implement manually? If so, what approach should I take?

Hmm, that is strange. If you look at the gojs-angular-basic sample on GitHub, you can see there is a Palette allowing one to drop Nodes onto their diagram. These events are undoable, and when they are undone, the nodes held in Angular app data are updated accordingly via the diagramModelChange function.

What about your application is being done differently? Does your modelChange function set skipsDiagramUpdate to true, and then merges all node / link / model data using the DataSyncService?

It turns out it was my fault - my ExternalObjectsDropped listener was changing the keys of dropped nodes. My problem is solved then, thank you very much for your help!