ProseMirror transactions: map the next edit after the first one

Build a ProseMirror transaction from the current editor state, apply its steps, and dispatch that transaction through the view. When a first step changes document length, map any later positions measured from the original document through tr.mapping before using them. Reusing an old numeric offset can replace the wrong words. Transformations and mapping

An original room-notice example says “Begin in Lab A. Continue in Lab B.” A single action changes Lab A to “Demonstration room” and Lab B to “Workshop.” The first replacement is longer, so the second room moves in the transformed document.

Establish what the positions describe

ProseMirror positions belong to the structured document, including node boundaries. They are not simply indices into exported HTML or the combined visible text. Find the two text ranges through the application's document model before constructing the operation.

For a deliberately small document containing one paragraph with exactly that sentence, the original ranges are:

  • Lab A: from 10 to 15.
  • Lab B: from 29 to 34.

The paragraph's opening boundary accounts for the offset before its first character. These numbers apply only to this exact fixture. Adding a heading or changing the sentence invalidates them. An application should derive its actual ranges and confirm that each contains the expected text.

Map the second original range through earlier steps

With view referring to the initialized EditorView for that fixture:

const state = view.state;
const first = { from: 10, to: 15 };
const second = { from: 29, to: 34 };

if (
  state.doc.textBetween(first.from, first.to) !== 'Lab A' ||
  state.doc.textBetween(second.from, second.to) !== 'Lab B'
) {
  throw new Error('The room notice no longer matches this edit.');
}

const tr = state.tr;
tr.insertText('Demonstration room', first.from, first.to);

const secondFrom = tr.mapping.map(second.from);
const secondTo = tr.mapping.map(second.to);
tr.insertText('Workshop', secondFrom, secondTo);

view.dispatch(tr.scrollIntoView());

Both original ranges were measured in state.doc. After the first insertion, tr.mapping translates the second range into the document produced so far. Here the replacements do not overlap, and the first one occurs wholly before the second. Boundary association and deleted positions need more deliberate handling for touching or overlapping edits.

The expected paragraph is:

Begin in Demonstration room. Continue in Workshop.

The first replacement adds 13 characters in this fixture. The second range therefore moves from 29 to 34 into the range from 42 to 47 before it is replaced.

Dispatch through one state-update path

The transaction carries the resulting document, selection changes, and metadata. insertText changes the transaction's document; the editor view changes when the transaction is dispatched and its new state applied. Transaction reference

If your application supplies dispatchTransaction, preserve its normal state-application path. ProseMirror's guide shows applying the transaction to the state and passing that state to view.updateState(). Calling view.dispatch() without completing a custom handler's update path can leave the displayed document unchanged.

Create a fresh transaction when the action runs. Do not retain state.tr while waiting for a network response and later apply it to a newer state. A pending server response needs an application policy for current document identity and intervening edits.

The plugin-state guide covers transaction metadata used for review labels. That metadata can accompany an operation, but it does not repair ranges measured against a different document.

Verify the whole notice after the operation

In the actual editor integration, load the exact two-room fixture, run the action, and inspect both room names and the punctuation between them. Then change the fixture beforehand. The illustrative guard should reject it instead of replacing unrelated content.

Exercise Undo through the application's configured history. The history guide explains grouping and deliberate boundaries; a transaction is not a server save or a permanent audit entry. Finish by saving the chosen notice through its normal route and reopening both room instructions together.

Keep reading