standardMouseMove on Node Deletion while hovering

Hi,

in our software we have a bunch of templates which implement mouseEnter, mouseLeave and mouseOver bindings. Inside them, we set e.g. data._hovered.

Like this:

template binding:
mouseEnter: (e, obj) => setHovered(obj, true),

function:
setHovered = (g: go.GraphObject, hovered: boolean): void => {
    if (!g.diagram) {
        return;
    }
    g.diagram.model.setDataProperty((g as go.Panel).data, '_hovered', hovered);
};

We have other bindings that bind the stroke, width, etc. based on the _hovered property that we set above.
In the documentation it is explained that a binding should have no side-effects. I assume setting a data property of a node would be a side effect. Question now is, how to do it correctly?

While investigating an issue with these bindings, I found out that the diagram calls standardMouseMove inside the modelChange callback, when a node gets deleted while being hovered.
This causes the binding inside the node that is being deleted to be triggered one last time. The obj.diagram is null in that callback context, which causes problems in our code.
One solution to that would be to use the event parameter (e.diagram instead of g.diagram) of the event callback but the general question why this happens remains.

Is this intended behavior? Triggering the bindings of a node that is already or about to be deleted?
Any best practices to avoid these kind of problems?

My first reaction is that there’s no Binding in your code. GraphObject.mouseEnter is a property that is an event handler. The documentation for each event handler describes:

  • what changes are allowed in the event handler
  • whether there’s a transaction that you can count on, or if you need to start/commit your own transaction
  • other conditions, such as whether skipsUndoManager is temporarily true

GraphObject | GoJS API

In your case, skipsUndoManager is indeed true and you do not need to conduct a transaction, so the change you are making (including the evaluation of any Bindings depending on that data property change) will not be recorded in the UndoManager.

I can experiment with the situation you describe.

I’m not encountering any extra event handler calls when defining either the mouseHover or the mouseEnter event handler on a Node.

Here’s my app:

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
const $ = go.GraphObject.make;

const myDiagram =
  new go.Diagram("myDiagramDiv",
    {
      "undoManager.isEnabled": true,
      "ModelChanged": e => {     // just for demonstration purposes,
        if (e.isTransactionFinished) {  // show the model data in the page's TextArea
          document.getElementById("mySavedModel").textContent = e.model.toJson();
        }
      }
    });

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    {
      //mouseHover:
      mouseEnter:
        (e, node) => {
          console.log("mouse");
          e.diagram.commit(d => {
            d.remove(node);
          });
        }
    },
    $(go.Shape, { fill: "white" },
      new go.Binding("fill", "color")),
    $(go.TextBlock, { margin: 8 },
      new go.Binding("text"))
  );

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, text: "Alpha", color: "lightblue" },
  { key: 2, text: "Beta", color: "orange" },
  { key: 3, text: "Gamma", color: "lightgreen" },
  { key: 4, text: "Delta", color: "pink" }
],
[
  { from: 1, to: 2 },
  { from: 1, to: 3 },
  { from: 2, to: 2 },
  { from: 3, to: 4 },
  { from: 4, to: 1 }
]);
  </script>
</body>
</html>

My first reaction is that there’s no Binding in your code. GraphObject.mouseEnter is a property that is an event handler.

You are right. I referred to these event handlers as bindings.

I modified the code you provided to show the example I mean.
Select Alpha and stay over it with the mouse. Then delete it via pressing ‘del’.
console.log(node.diagram);:33 will log null as described.

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
const $ = go.GraphObject.make;

const myDiagram =
  new go.Diagram("myDiagramDiv",
    {
      "undoManager.isEnabled": true,
      "ModelChanged": e => {     // just for demonstration purposes,
        if (e.isTransactionFinished) {  // show the model data in the page's TextArea
          document.getElementById("mySavedModel").textContent = e.model.toJson();
        }
      }
    });

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    {
      //mouseHover:
      mouseLeave:
        (e, node) => {
          console.log("mouse");
		  console.log(node.diagram);
          //e.diagram.commit(d => {
          //  d.remove(node);
          //});
        }
    },
    $(go.Shape, { fill: "white" },
      new go.Binding("fill", "color")),
    $(go.TextBlock, { margin: 8 },
      new go.Binding("text"))
  );

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, text: "Alpha", color: "lightblue" },
  { key: 2, text: "Beta", color: "orange" },
  { key: 3, text: "Gamma", color: "lightgreen" },
  { key: 4, text: "Delta", color: "pink" }
],
[
  { from: 1, to: 2 },
  { from: 1, to: 3 },
  { from: 2, to: 2 },
  { from: 3, to: 4 },
  { from: 4, to: 1 }
]);
  </script>
</body>
</html>

I updated the code further to replicate the error I am investigating. Same reproduction steps.

<!DOCTYPE html>
<html>
<head>
    <title>Minimal GoJS Sample</title>
    <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
<textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

<script src="https://unpkg.com/gojs"></script>
<script id="code">
    const $ = go.GraphObject.make;

    const myDiagram =
        new go.Diagram("myDiagramDiv",
            {
                "undoManager.isEnabled": true,
                "ModelChanged": e => {     // just for demonstration purposes,
                    if (e.isTransactionFinished) {  // show the model data in the page's TextArea
                        document.getElementById("mySavedModel").textContent = e.model.toJson();
                    }
                }
            });

    myDiagram.nodeTemplate =
        $(go.Node, "Auto",
            {
                mouseEnter:
                    (e, node) => {
                        console.log("mouse enter");
                        console.log(node.diagram);
						console.log(node.data);
                        node.diagram.model.setDataProperty(node.data, "_hovered", true);
                    },
                mouseLeave:
                    (e, node) => {
                        console.log("mouse leave");
						console.log(node.diagram);
                        console.log(node.data);
                        node.diagram.model.setDataProperty(node.data, "_hovered", false);
                    }
            },
            $(go.Shape, {fill: "white"},
                new go.Binding("fill", "color"),
                new go.Binding("fill", "_hovered", (value, obj) => {
                    return value ? 'red' : obj.part.data.color;
                })
            ),
            $(go.TextBlock, {margin: 8},
                new go.Binding("text"))
        );

    myDiagram.model = new go.GraphLinksModel(
        [
            {key: 1, text: "Alpha", color: "lightblue"},
            {key: 2, text: "Beta", color: "orange"},
            {key: 3, text: "Gamma", color: "lightgreen"},
            {key: 4, text: "Delta", color: "pink"}
        ],
        [
            {from: 1, to: 2},
            {from: 1, to: 3},
            {from: 2, to: 2},
            {from: 3, to: 4},
            {from: 4, to: 1}
        ]);
</script>
</body>
</html>

That makes sense. If the mouse stays within the selected Node, and you perform a Delete command, the Node will be removed from the Diagram, as one would expect.

But the removal of the Node from the Diagram does mean that the mouse leaves the Node, even though the mouse hasn’t moved at all. So it’s reasonable to get that mouseLeave event. And node.diagram will be null because the Node is no longer in the Diagram.

So what I take from this discussion is two things:

  • Setting data properties inside an Event handler is not bad practice
  • The node still being there and the event triggering is expected. Solution then would be to rather work with the inputEvent property from the event to ensure diagram is always accessible or check if node.diagram is null to exit early.

Yes and yes. It is unusual for a GraphObject that is passed to a mouse event handler to have a GraphObject.diagram or layer property value that is null, but it makes sense when it has been removed from the visual tree of the diagram.