ReactDiagram Model.set() Not Invoking onModelChange function

Hi there! I found some similar Posts but not quite my solution.

Why would this code NOT invoke my onModelChange function callback?

Thank you!

const externalNodeDropped = (event: go.DiagramEvent) => {
		const droppedNode: go.ObjectData = event.subject.first().part.qb;

		const diagram: go.ObjectData | null | undefined =
			diagramRef.current?.getDiagram();

		// Possible NULL check, avoid Lint warnings
		if (!diagram) return;

		if (droppedNode.category === goJsCategory.ImportNode) {
			diagram.model.commit((m: go.Model) => {
				delete droppedNode.group;
				m.set(droppedNode, 'state', GoJsNodeState.Diagram);
			});

			diagram.model.addLinkData({
				from: droppedNode.key,
				to: '1f409525-7df2-4bb6-b406-3e6be8e9b347-0',
			});
		}
	};

Whenever you want to make a change to a model (including its data) or to a diagram (including its parts), you need to do so within a transaction.

Your code does have a call to Model.commit, but you also make other changes to the model outside of that transaction. I suggest you wrap all of the code within the then-clause of the category check in a single call to commit for each of the models that you are modifying.

The other possible problem is that the Palette is normally read-only, so I think the ReactPalette component doesn’t even have a model changed listener. Use a ReactDiagram component instead.

Walter, the Palette does not have onModelChange and that’s ok, I just removed that code in this example.

I could call the .addLinkData() method first and .commit() after but, what do you mean:

I suggest you wrap all of the code within the then-clause

then-clause?

I guess my point is that the onModelChange is NEVER called, not even once…

Thank you,

Oops, I meant “if” clause.

	diagram.model.commit((m: go.Model) => {
		delete droppedNode.group;
		m.set(droppedNode, 'state', GoJsNodeState.Diagram);
		m.addLinkData({
		    from: droppedNode.key,
		    to: '1f409525-7df2-4bb6-b406-3e6be8e9b347-0',
		});
	});

but how will that execute the onModelChange as opposed to what I had before?

If you look at how ReactDiagram is implemented, in gojs-react/src/components/Diagram.tsx, in the componentDidMount method:

    // initialize data change listener
    this.modelChangedListener = (e: go.ChangedEvent) => {
      if (e.isTransactionFinished && e.model && !e.model.isReadOnly && this.props.onModelChange) {
        const dataChanges = e.model.toIncrementalData(e);
        if (dataChanges !== null) this.props.onModelChange(dataChanges);
      }
    };
    diagram.addModelChangedListener(this.modelChangedListener);

So at the end of each transaction, if there are model changes and if there is a onModelChange function, it’s called.

I be damned. It won’t fire onModelChange… could it be because I’m not adding/removing a node? I’m updating one that has JUST been copied/dropped from the Palette?

I tried moving the m.set() at the very end but no luck.

<ReactDiagram
					ref={diagramRef}
					initDiagram={initDiagram}
					divClassName="goJsDiagram"
					nodeDataArray={diagramData?.nodeDataArray}
					linkDataArray={diagramData?.linkDataArray}
					onModelChange={(event: go.IncrementalData) => {
						console.log(event);
					}}
				/>
diagram.addDiagramListener('ExternalObjectsDropped', externalNodeDropped);
const externalNodeDropped = (event: go.DiagramEvent) => {
		const droppedNode: go.ObjectData = event.subject.first().part.qb;

		const palette: go.Palette | null | undefined =
			paletteRef.current?.getPalette();

		const diagram: go.ObjectData | null | undefined =
			diagramRef.current?.getDiagram();

		// Possible NULL check, avoid Lint warnings
		if (!diagram || !palette) return;

		if (droppedNode.category === goJsCategory.ImportNode) {
			diagram.model.commit((m: go.Model) => {
				delete droppedNode.group;
				m.set(droppedNode, 'state', GoJsNodeState.Diagram);

				diagram.model.addLinkData({
					from: droppedNode.key,
					to: '1f409525-7df2-4bb6-b406-3e6be8e9b347-0',
				});

				palette.model.nodeDataArray.forEach((pNode: any) => {
					if (pNode.text === droppedNode.text) {
						palette.model.commit((m: go.Model) => {
							m.set(pNode, 'state', GoJsNodeState.Copied);
						});
					}
				});
			});
		}
	};

Try setting a breakpoint on the if statement in the model changed listener that I quoted above.

That was a great tip for troubleshooting.

I found the issue. I threw in this hack to catch the Transaction events and look at the nodeDataArray.

const modelChangedListener = (e: any) => {
			if (e.af?.includes('Transaction')) {
				const dataChanges = e.model?.toIncrementalData(e);
				if (dataChanges !== null) {
					externalNodeDropped(dataChanges);
				}
			}
		};

This is the sequence in which the functions get called AFTER I copied/dropped the Palette Node.

  1. modelChangedListener → the copied/dropped Node is not in the Model yet.
  2. externalNodeDropped → change the Node prop via m.set().
  3. modelChangedListener → the copied/dropped Node is now in the Model with the latest prop value and so the toIncrementalData(e) method does not detect any changes…

Maybe I need to place my logic in a different event handler so it gets executed AFTER the copied/dropped Node is in the Model and BEFORE I call m.set()…

I mean, I guess I could instead listen to the diagram.addModelChangedListener(modelChangedListener)…

Unless you suggest a better alternative?

or not… lol…

Your code is depending on a minified property name, “af”. Don’t do that. Only use properties declared in the API or go.d.ts file.

Compare with particular Transaction type names for the ChangedEvent.propertyName. Searching for “Transaction” in the name will get ChangedEvents that you probably don’t want, and get executed twice each time.

I have removed both event listeners modelChangedListener() and 'ExternalObjectsDropped' and when I drop a Node from the Palette, it does not trigger the onModelChange when clearly, the Model has changed, a new Node has been copied into it…

Let’s focus on this. I need the onModelChange to execute when a Node is dropped…

Is the ReactDiagram.componentDidMount method being called?
Is its modelChangedListener ever called, and is it called when a node is dragged from another Diagram/Palette into your diagram?
Does it call your onModelChanged function?

modelChangedListener is called but it does not detect any changes hence, onModelChange does not get called

That is surprising. Tomorrow we’ll try to reproduce the situation.

Here’s an example using gojs-react with a ReactPalette and ReactDiagram, where the onModelChanged event is called when a node is dropped into the diagram.

https://codesandbox.io/p/sandbox/gojs-react-palette-hooks-3w5rqk

Morning! thank you for that. This is VERY puzzling… I’ll continue to investigate my implementation to find out what’s going on…

Found it! SMH… this prop was missing from my initDiagram logic :-(

'undoManager.isEnabled': true, // must be set to allow for model change listening

Thanks for reminding us of a possible configuration problem that we don’t normally think about.

If you do not want to support undo/redo for users, you can also set UndoManager.maxHistoryLength to zero.

1 Like