Dynamically add click listener to `ContextMenu` button

We’re using gojs within a react app and we have a few functions that return templates which roughly look like this:

function getContextMenuTemplate(onContextMenuButtonClick) {
  return go.GraphObject.make('ContextMenuButton', {
     click: (e: go.InputEvent, obj: go.GraphObject)  {
       onContextMenuButtonClick(e.part);
     }
  })
}

function getNodeTemplate(onContextMenuButtonClick) {
  return new go.Node(go.Panel.Spot, { // ... })
    .add(
      go.GraphObject.make('Button', {
         contextMenu: getContextMenuTemplate(onContextMenuButtonClick),
       }
    );
}

Upon initializing the canvas we pass a function into the template for the context menu callbacks. This function depends on a react state. Again, roughly:

function Blah() {
  let [x, setX] = useState(false);
  diagram.nodeTemplateMap = getNodeTemplate((part) => {
    if (x) {
      console.log("part was clicked");
    }
  });
}

Since the function passed into the getNodeTemplate closes over the x value it’ll only see the initial value of x and when x is updated to true, this function still sees it as false. This is classic closure problem and has nothing to do with gojs. And then reason for it is because we only set the diagram template once and at that moment the captured value of x is false.

Now one way to fix this is to use useRef and instead of if (x) use if (xRef.current) and update the ref within an effect. But I don’t like this at all.

Another way to do this, which we do for most of other event handlers, is to attach an events handler to the diagram, like:

useEffect(() => {
  diagram.addDiagramListener('some_event', callback);
  return () => {
    diagram.removeDiagramListener('some_event', callback);
  }
}, [x])

^ This way I can update the remove the listener and add a new one once the closure variables are updated.

The problem is, I haven’t been able to find an event that works with ContextMenu. The closest one I found is ObjectSingleClicked but it’s tricky to get the data off of e.subject cause the subject is sometimes the TextBox which renders the button text and sometimes the Shape behind the text.

Does this make sense? In short, I wanna attach an events listener to the diagram that fires every time a Button within ContextMenu is clicked and tells me which button it was and the data attached to that node.

I can create a small repro on stackblitz if it helps but thought throw this out here first just in case I’m doing it totally wrong.

I wish I were an expert with React so I could give a more clarifying answer. There may be an easier way to do what you want to do.

But given what you’ve outlined, the place you want to attach (and detach) would be the ContextMenuTool’s showContextMenu. You can override like this to do extra work:

myDiagram.toolManager.contextMenuTool.showContextMenu = function (contextmenu, obj) {
  // Attach something here based on contextmenu or obj?
  // obj is the current GraphObject for which there is a context menu, or `null` if the context menu is on the diagram
  go.ContextMenuTool.prototype.showContextMenu.call(this, contextmenu, obj);
};
// possibly detach in the corresponding hideContextMenu?

Thanks. Do you folks have any section for feature requests? This could be a good candidate to be added to DiagramEvents.

Sure, anytime, anywhere. Here is fine, or in a separate topic.

1 Like