One of the goals of
firstname.lastname@example.org is to make consuming commands easier. Chainable commands was identified as one way in which this could be achieved.
Elegant as it may seem, implementing this posed some challenges.
By default Prosemirror commands use the following type signature.
Each command receives the current editor state and is expected to create a new transaction by calling
When the dispatch function is provided the command applies that transaction to the view by calling
dispatch(tr). The command is expected to return
true when successful and
false when no update is possible.
The transaction created via
state.tr is responsible for updating the state and describing the desired updates. In ProseMirror the
EditorState is immutable and the
Transaction is mutable with methods that change the internal state.
When these methods are called, steps are added to the
tr.steps property. When the transaction is dispatched, ProseMirror reads the steps and other updated properties to create a new immutable
Examples of transaction methods are,
tr.removeMark. This is how updates are managed within every ProseMirror editor.
Since each command creates it's internal
Transaction instance updates are expected to occur sequentially in separate synchronous steps. This poses a challenge. Without a shared transaction it's not possible to chain the commands since many things can interfere with the update process.
remirror for chainable commands
The first step was to provide a shared transaction for all internal remirror commands.
Since the command functions already receive three positional arguments adding a fourth for the shared transaction made the function call overly complex. To make things easier to consume, all the arguments have been squashed into one parameter, giving the remirror command function the following type signature.
After providing all commands with a shared transaction another problem arises. Each command still calls a dispatch function.
dispatch function typically uses the
view.dispatch function to trigger updates in the editor. If we use this method within a chained command, the shared transaction is no longer valid. This leads to multiple
The simplest way to fix this is to provide a fake dispatch function. It does nothing to the editor and thus maintains the sanctity of the shared transaction.
Additionally, a check is made to ensure that the transaction passed into the
dispatch(tr) method is the expected
shared transaction. If not, it throws an error.
Finally, at some point the chain needs to come to an end. For this, I defined a methods called
run method which uses
view.dispatch to update the state with the shared transaction.
This is the setup required for remirror to make any internal command chainable.
The following example is how commands created in remirror are automatically chainable. It uses the shared
tr property to accomplish this.
While this solves the chainable problem for internally created commands, it doesn't do much for external commands. Libraries like
prosemirror-schema-list provide useful commands which aren't chainable in the ways described above.
In order to mitigate this we need to pass in a state, that uses the shared transaction. This can be accomplished with the following function which replaces the
state.tr with the passed in transaction. Please note the returned function is not an actual state object and wouldn't pass any
With this method it becomes possible to make ProseMirror commands chainable.
When the commands call
state.tr, they will be accessing the shared transaction that is provided by remirror.
So converting the
deleteTable command from
prosemirror-tables is possible with the with the following code snippet.
Just because a command can be made chainable does not mean it should be made chainable.
prosemirror-history provides the
redo commands. While making them chainable would work in theory, in practice, what does it actually mean for
undo to be chainable.
Are we undoing the current transaction or last action before this transaction? It's not clear what the expected behavior should be in every situation.
There are also commands like
prosemirror-tables which are also non-chainable. The command uses
state.tr to check if any of the tables need fixing. If they do, it dispatches them, if they don't it doesn't. Unfortunately this breaks the
isEnabled command checks which rely on the
Transaction not being updated unless a dispatch is provided.
As a result remirror also supports declaring commands as non-chainable.
However, for the vast majority of instances chainable commands are a joy to use.
You can try them out for yourself in by installing
remirror@next and following the getting started guide.
- Behind the scenes
state.tris a getter property which returns a
new Transaction()every time it is accessed.↩