Can I clean up all these event listeners?

Have you enabled the UndoManager?

Yes I have - here is my config:

    goDiagram.isReadOnly = me.readOnly;
    goDiagram.undoManager.isEnabled = true;
    goDiagram.allowClipboard = true;
    goDiagram.animationManager.isEnabled = false;
    goDiagram.toolManager.mouseWheelBehavior = go.ToolManager.WheelZoom;

I just added this listener:

    "ModelChanged": function(e) {
      if (e.isTransactionFinished) console.log(e.model.toIncrementalJson(e));
    },

and set the GraphLinksModel.linkKeyProperty in two different sample apps: Flow Chart and Logic Circuit.

In both cases the resulting output was what I expected. And there was no error. So I cannot explain what is wrong in your app.

BTW, the default value of Model.nodeKeyProperty is “key”, so Model.toJson doesn’t bother writing that out.

Also, I tried it when UndoManager.isEnabled is false, and one gets a completely different result from Model.toIncrementalJson, making it clear that there’s nothing available.

OK I got an example that reproduces the problem! Our diagram using images (font icons) and so does this (SVG) so maybe that could be the culprit. Move nodes around and resize them and you’ll see the console output has minimal information. My example is based on this example.

Here is mine.

<!DOCTYPE html>
<html>
<head>
<title>toIncrementalJson</title>
<meta charset="UTF-8">
<script src="go.js"></script>
<script src="icons.js"></script>
<script id="code">

    function init() {
        if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
        var $ = go.GraphObject.make;  // for conciseness in defining templates

        // "icons" is defined in icons.js
        // SVG paths have no flag for being filled or not, but GoJS Geometry paths do.
        // We want to mark each SVG path as filled:
        for (var k in icons) {
            icons[k] = go.Geometry.fillPath(icons[k]);
        }

        // a collection of colors
        var colors = {
            blue:   "#00B5CB",
            orange: "#F47321",
            green:  "#C8DA2B",
            gray:   "#888",
            white:  "#F5F5F5"
        }

        // The first Diagram showcases what the Nodes might look like "in action"
        myDiagram = $(go.Diagram, "myDiagramDiv",
                {
                    initialContentAlignment: go.Spot.Center,
                    "undoManager.isEnabled": true,
                    layout: $(go.TreeLayout)
                });

        // A data binding conversion function. Given an icon name, return the icon's Path string.
        function geoFunc(geoname) {
            if (icons[geoname]) return icons[geoname];
            else return icons["heart"]; // default icon
        }

        // Define a simple template consisting of the icon surrounded by a filled circle
        myDiagram.nodeTemplate =
                $(go.Node, "Auto", { resizable: true },
                        $(go.Shape, "Circle",
                                { fill: "lightcoral", strokeWidth: 4, stroke: colors["gray"], width: 60, height: 60 },
                                new go.Binding("fill", "color")),
                        $(go.Panel, "Vertical",
                                { margin: 3 },
                                $(go.Shape,
                                        { fill: colors["white"], strokeWidth: 0 },
                                        new go.Binding("geometryString", "geo", geoFunc))
                        ),
                        // Each node has a tooltip that reveals the name of its icon
                        { toolTip:
                                $(go.Adornment, "Auto",
                                        $(go.Shape, { fill: "LightYellow", stroke: colors["gray"], strokeWidth: 2 }),
                                        $(go.TextBlock, { margin: 8, stroke: colors["gray"], font: "bold 16px sans-serif" },
                                                new go.Binding("text", "geo")))
                        }
                );

        // Define a Link template that routes orthogonally, with no arrowhead
        myDiagram.linkTemplate =
                $(go.Link,
                        { routing: go.Link.Orthogonal, corner: 5, toShortLength: -2, fromShortLength: -2 },
                        $(go.Shape, { strokeWidth: 5, stroke: colors["gray"] })); // the link shape

        // Create the model data that will be represented by Nodes and Links
        myDiagram.model = new go.GraphLinksModel(
                [
                    { key: 1, geo: "file"          , color: colors["blue"]   },
                    { key: 2, geo: "alarm"         , color: colors["orange"] },
                    { key: 3, geo: "lab"           , color: colors["blue"]   },
                    { key: 4, geo: "earth"         , color: colors["blue"]   },
                    { key: 5, geo: "heart"         , color: colors["green"]  },
                    { key: 6, geo: "arrow-up-right", color: colors["blue"]   },
                    { key: 7, geo: "html5"         , color: colors["orange"] },
                    { key: 8, geo: "twitter"       , color: colors["orange"] }
                ],
                [
                    { from: 1, to: 2 },
                    { from: 1, to: 3 },
                    { from: 3, to: 4 },
                    { from: 4, to: 5 },
                    { from: 4, to: 6 },
                    { from: 3, to: 7 },
                    { from: 3, to: 8 }
                ]);


        myDiagram.model.linkKeyProperty = 'key';
        myDiagram.addModelChangedListener(function(e) {
            if (e.isTransactionFinished) console.log(e.model.toIncrementalJson(e));
        });

    }
</script>
</head>
<body onload="init()">

<div id="myDiagramDiv" style="border: solid 1px black; width:450px; height:300px"></div>

</body>
</html>

The behavior is correct in the sample app that you just provided. The reason is that model does not change when the user moves or resizes a node. Notice that there are no TwoWay Bindings on the node template. So the node data in the model is not changing – no data property has changed value. Hence there’s nothing interesting to report for the transaction.

Two way binding made all the difference!

My next challenge is to handle copy and paste. First I need to create a record for our data for the new node then set the model key to the id for that record (instead of using auto generated key). I need to do this before the transaction commits because that causes a selection event which our code crashes because the auto gen key isn’t valid for our app. Can change the key on a StartedTransaction: Paste? If not what is your recommended way to modify model data before the transaction commits?

Also is there an offset setting anywhere for pasting parts? If not adding a pasteOffset to the CommandHandler would be a nice enhancement since you can’t tell anything happened when pasting in place. I’m doing this but a property would be nicer.

    goDiagram.moveParts(goDiagram.selection, new go.Point(90, 90));

The obvious answer to your first question is to supply a custom Model.makeUniqueKeyFunction. BUT that is only appropriate if your code that cares about the format of the key being present there where your model code is running. If the source of the key generation is on the server, you really should not be waiting on getting a response from the server within a GoJS transaction.

Regarding pasting with an offset: look at the overrides of CommandHandler.copyToClipboard and CommandHandler.pasteFromClipboard that are in the DrawCommandHandler defined in extensions/DrawCommandHandler.js.

I’m creating an Ext JS model locally so there won’t be any server lag. So are either of these solutions OK?

  1. Overriding CommandHandler.pasteSelection, adding the nodes & links myself, and don’t call CommandHandler.prototype.pasteSelection.call in the override?

  2. Changing the key and other model data in StartedTransaction: paste?

I suppose so. It’s hard for me to judge.