Updating NodeViews on node moved

Is there any way that a NodeView is notified when a node is moved in the document?

I’m building react backed NodeViews that are rerendered when the update function is called. When attributes are set this correctly triggers a rerender but moving a node doesn’t seem to trigger an update. So React never knows that it should rerender on move.

The reason I want to rerender is that I have “move block up” buttons that should become disabled once the block is the first in the document:


I’ve been able to force this to work by triggering a rerender in the function that moves the block:

moveNodeUp() {
	const { getPos, view, node } = reactNodeView.nodeViewProps;
	const pos = getPos();
	const { nodeBefore } = view.state.doc.resolve(pos);
	if (nodeBefore) {
		const newPos = pos - nodeBefore.nodeSize;
		const moveNodeUp = view.state.tr
			.replace(pos, pos + node.nodeSize)
			.insert(newPos, node);

but this is causing RangeErrors:

RangeError: Position undefined out of range
    at Function.resolve (index.js?6f27:945)
    at Function.resolveCached (index.js?6f27:968)
    at Node.resolve (index.js?6f27:1239)
    at Object.get canMoveNodeUp [as canMoveNodeUp] (VM9986 nodeViewActions.js:49)

the CanMoveNodeUp function for context:

get canMoveNodeUp() {
	const { getPos, view } = reactNodeView.nodeViewProps;
	const pos = getPos();
	const { nodeBefore } = view.state.doc.resolve(pos);
	return !!nodeBefore;

Am I on the right path and just have a bug in my canMoveNodeUp code causing the range errors? or is there another way I can trigger rerenders when the state changes?

Node views don’t really get to know about their position in the document—they are rendered and then left alone as long as the node stays the same, since constantly calling their update methods when something somewhere in the document changed would be expensive.

There is one way to communicate with node views, and that is to add node decorations to them from external code. When their decorations change, the update method will be called, and the decorations are passed as one of its arguments.

Ok, cool. I’m using MobX to control rendering so if I add a decoration of their current position to NodeViews then MobX can limit the rerenders to only if some computed property has changed as a result of the new position. That’ll help limit the performance cost. Thanks.

Would you be open to adding a way to notify the NodeView if it’s position has changed? Not necessarily extending the functionality of the update function as I wouldn’t want to break other implementations.

If you really want to do this, you can have your nodeviews register themselves somewhere on init (and unregister on .destroy()), and just blast out a notification every time the document changes.

Having dived a little closer into this it seems that when I move down the component is rerendered and if I move up the component is not.

So when we move a block down the canMoveNodeUp/Down functions are called but when moved up they aren’t.

The only difference between up and down is the order I’ve put for the transactions:

const moveNodeUp = view.state.tr
	.replace(pos, pos + node.nodeSize)
	.insert(newPos, node);

const moveNodeDown = view.state.tr
	.insert(newPos, node)
	.replace(pos, pos + node.nodeSize);

Does the order matter at all? If I remove at 8 then insert at 10 in the same transaction will the positions be mapped correctly even if the remove means the subsequent insert would be at 8 by the end of the transaction?

Ok, the order does matter. Switching the order on the moveUp means I replace the wrong content (text in the paragraph node above) instead of the nodeView.

The moveDown works because the nodeView is destroyed and recreated. I would need to calculate the new position after the insert of the copy nodeView in order to properly delete the original nodeView.